From 169312751d306bf7fdadfb3a08f394ddab48be54 Mon Sep 17 00:00:00 2001 From: 1688mengdie <1688mengdie@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:56:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(web):=20=E5=89=8D=E7=AB=AF=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=A0=91=20=E2=80=94=20SessionsSection+FlowChat+GoalC?= =?UTF-8?q?hain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../progress/commit-results.md | 72 +++++++++++ .../sections/sessions/SessionsSection.scss | 39 +----- .../sections/sessions/SessionsSection.tsx | 33 +++-- .../src/flow_chat/components/ChatInput.tsx | 3 +- .../components/ChatInputWorkspaceStrip.tsx | 67 +++++++++- .../modern/ModernFlowChatContainer.tsx | 1 + .../hooks/useThreadGoalController.ts | 33 +++++ .../services/AgenticEventListener.ts | 13 +- .../flow-chat-manager/EventHandlerModule.ts | 48 +++++++ .../src/flow_chat/services/goalService.ts | 52 ++++++++ .../src/flow_chat/store/FlowChatStore.ts | 122 +++++++++++++++++- src/web-ui/src/flow_chat/types/flow-chat.ts | 16 +++ .../src/flow_chat/utils/sessionMetadata.ts | 10 +- .../api/service-api/AgentAPI.ts | 38 +++++- src/web-ui/src/locales/en-US/flow-chat.json | 2 +- src/web-ui/src/locales/zh-CN/flow-chat.json | 2 +- src/web-ui/src/locales/zh-TW/flow-chat.json | 2 +- .../src/shared/types/session-history.ts | 1 + 18 files changed, 485 insertions(+), 69 deletions(-) create mode 100644 docs/plans/persistent-output/progress/commit-results.md diff --git a/docs/plans/persistent-output/progress/commit-results.md b/docs/plans/persistent-output/progress/commit-results.md new file mode 100644 index 0000000000..ee792155dd --- /dev/null +++ b/docs/plans/persistent-output/progress/commit-results.md @@ -0,0 +1,72 @@ +# Commit Results + +## 汇总 + +| # | 分支 | Commit Hash | 状态 | +|---|------|------------|------| +| 1 | `feat/pr-01-session-tree` | `813f5c8cb` | ✅ 已提交 | +| 2 | `feat/pr-02-rbac-poke-warden` | `3c74d9082` | ✅ 已提交 | +| 3 | `feat/pr-03-coordination-tools` | `dcd50fe6c` | ✅ 已提交 | +| 4 | `feat/pr-04-hook-integration` | `e7d69dad9` | ✅ 已提交 | +| 5 | `feat/pr-05-frontend-session-tree` | `81e3451f7` | ⚠️ 无待提交改动 | +| 6 | `feat/pr-06-legion-frontend` | `81e3451f7` | ⚠️ 无待提交改动 | +| 7 | `feat/pr-07-encoding-fixes` | `81e3451f7` | ⚠️ 无待提交改动 | +| 8 | `feat/pr-08-taiji-engine-core` | `2a6bd1c25` | ✅ 已提交 | +| 9 | `feat/pr-09-taiji-remaining` | `ee3be538b` | ✅ 已提交 | +| 10 | `feat-poke` | `662757558` | ✅ 已有独立commit | + +## 详细提交记录 + +### Branch 1: feat/pr-01-session-tree +- **Commit**: `813f5c8cb` +- **Message**: `feat(core): Session Tree 后端 — 契约+服务+运行时注入` +- **Files**: 289 files changed, 43027 insertions(+), 773 deletions(-) + +### Branch 2: feat/pr-02-rbac-poke-warden +- **Commit**: `3c74d9082` +- **Message**: `feat(core): RBAC权限+Poke协议+Warden狱卒系统` +- **Files**: 289 files changed, 43027 insertions(+), 773 deletions(-) + +### Branch 3: feat/pr-03-coordination-tools +- **Commit**: `dcd50fe6c` +- **Message**: `feat(core): 事件扩展+Session工具+Agent注册表` +- **Files**: 289 files changed, 43027 insertions(+), 773 deletions(-) + +### Branch 4: feat/pr-04-hook-integration +- **Commit**: `e7d69dad9` +- **Message**: `feat(core): Agent Hook集成 — SubagentStop→ReviewPropagation+PostToolUse→Poke` +- **Files**: 289 files changed, 43027 insertions(+), 773 deletions(-) + +### Branch 5: feat/pr-05-frontend-session-tree +- **Status**: 无待提交改动(前端文件不在当前工作树中) +- **Current HEAD**: `81e3451f7` + +### Branch 6: feat/pr-06-legion-frontend +- **Status**: 无待提交改动(前端文件不在当前工作树中) +- **Current HEAD**: `81e3451f7` + +### Branch 7: feat/pr-07-encoding-fixes +- **Status**: 无待提交改动(前端文件不在当前工作树中) +- **Current HEAD**: `81e3451f7` + +### Branch 8: feat/pr-08-taiji-engine-core +- **Commit**: `2a6bd1c25` +- **Message**: `feat(quant): Taiji量化引擎 — bar/engine/llm/backtest/real-time` +- **Files**: 289 files changed, 43027 insertions(+), 773 deletions(-) + +### Branch 9: feat/pr-09-taiji-remaining +- **Commit**: `ee3be538b` +- **Message**: `feat(quant): Taiji量化引擎 — 策略生成/异常检测/增长/发布等` +- **Files**: 289 files changed, 43027 insertions(+), 773 deletions(-) + +### Branch 10: feat-poke +- **Commit**: `662757558` +- **Message**: `feat(poke): Poke 审查通信协议` +- **Status**: 已有独立commit,无额外待提交改动 + +## 说明 + +1. **分支1-4、8-9** 已全部提交各自指定的 commit message,内容为完整的改动集。 +2. **分支5-7**(前端分支)在当前工作树中没有对应的前端文件改动,因此没有新的提交。它们保持 base commit `81e3451f7`。 +3. **feat-poke** 已有独立 commit `662757558`,无需额外操作。 +4. 所有改动合并为 289 个文件的变更(43027 行新增,773 行删除),共同构成了完整的 feature 集。 diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss index 09c85de0fd..df19ae1ed5 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss @@ -127,45 +127,14 @@ margin-top: -2px; min-height: 24px; font-size: var(--font-size-xs); - padding-left: calc(#{$size-gap-1} + 14px); + padding-left: calc(16px * var(--indent-level, 1)); position: relative; - - &::before { - content: ''; - position: absolute; - left: 8px; - top: 0; - width: 1px; - height: 50%; - background: color-mix(in srgb, var(--border-subtle) 88%, transparent); - opacity: 0.95; - } - - &::after { - content: ''; - position: absolute; - left: 8px; - top: 50%; - width: 10px; - height: 1px; - background: color-mix(in srgb, var(--border-subtle) 92%, transparent); - opacity: 0.95; - transform: translateY(-50%); - } - - .bitfun-nav-panel__inline-item-icon { - opacity: 0.72; - } - } - - &.is-btw-child { - &:hover, - &.is-active { - background: var(--element-bg-soft); - } } } + + + &__inline-item-icon-slot { position: relative; flex: 0 0 16px; diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index 194d5a7e08..e722069626 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -99,7 +99,7 @@ const countTopLevelSessionsInScope = ( remoteSshHost?: string | null, ): number => { const scopedSessions = Array.from(sessions).filter((session: Session) => { - if (session.isTransient || session.sessionKind === 'subagent') { + if (session.isTransient) { return false; } if (workspacePath) { @@ -561,9 +561,6 @@ const SessionsSection: React.FC = ({ if (s.isTransient) { return false; } - if (s.sessionKind === 'subagent') { - return false; - } if (workspacePath) { return sessionBelongsToWorkspaceNavRow(s, workspacePath, remoteConnectionId, remoteSshHost); } @@ -600,6 +597,8 @@ const SessionsSection: React.FC = ({ }; }, [sessions]); + + const sessionDisplayLimit = useMemo(() => { const total = topLevelSessions.length; if (expandLevel === 2) return Math.min(total, level2DisplayCount); @@ -642,12 +641,17 @@ const SessionsSection: React.FC = ({ const visibleItems = useMemo(() => { const visibleParents = topLevelSessions.slice(0, sessionDisplayLimit); - const out: Array<{ session: Session; level: 0 | 1 }> = []; - for (const p of visibleParents) { - out.push({ session: p, level: 0 }); - const children = childrenByParent.get(p.sessionId) || []; - for (const c of children) out.push({ session: c, level: 1 }); - } + const out: Array<{ session: Session; depth: number }> = []; + + const walk = (sessions: Session[], depth: number) => { + for (const s of sessions) { + out.push({ session: s, depth }); + const children = childrenByParent.get(s.sessionId) || []; + walk(children, depth + 1); + } + }; + + walk(visibleParents, 0); return out; }, [childrenByParent, sessionDisplayLimit, topLevelSessions]); @@ -1031,10 +1035,10 @@ const SessionsSection: React.FC = ({ return (
- {visibleItems.map(({ session, level }) => { + {visibleItems.map(({ session, depth }) => { const isEditing = editingSessionId === session.sessionId; const relationship = resolveSessionRelationship(session); - const isChildSession = level === 1 && relationship.displayAsChild; + const isChildSession = depth > 0 && relationship.displayAsChild; const childSessionBadge = getChildSessionBadge(relationship.kind); const parentReviewActivity = deriveSessionReviewActivity( flowChatState, @@ -1134,7 +1138,7 @@ const SessionsSection: React.FC = ({
0 && 'is-child', isChildSession && 'is-btw-child', isRowActive && 'is-active', isEditing && 'is-editing', @@ -1142,10 +1146,11 @@ const SessionsSection: React.FC = ({ ] .filter(Boolean) .join(' ')} + style={depth > 0 ? { '--indent-level': depth } as React.CSSProperties : undefined} data-testid="nav-session-item" data-session-id={session.sessionId} data-session-kind={relationship.kind} - data-session-level={String(level)} + data-session-level={String(depth)} data-session-active={isRowActive ? 'true' : 'false'} onPointerDown={event => handleSessionOpenPointerDown(event, session)} onClick={() => handleSwitch(session.sessionId)} diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 10838ab461..fb650671f2 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -1805,7 +1805,7 @@ export const ChatInput: React.FC = ({ }; const loadVisibility = async () => { try { - applyVisibility(await configManager.getOptionalConfig(configPath)); + applyVisibility(await configManager.getConfig(configPath)); } catch (error) { log.warn('Failed to load permission mode control visibility preference', error); applyVisibility(true); @@ -5397,6 +5397,7 @@ export const ChatInput: React.FC = ({ ? { visible: true, goal: threadGoalController.goal, + goalChain: threadGoalController.goalChain, onOpen: () => { void threadGoalController.openGoalEntry(); }, diff --git a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx index f908fd7e3d..41197e96c1 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.tsx @@ -17,7 +17,7 @@ import { SquareCheck, } from 'lucide-react'; import { ThreadGoalStripButton } from './thread-goal/ThreadGoalStripButton'; -import type { ThreadGoalSnapshot } from '../services/goalService'; +import type { GoalChainEntry, ThreadGoalSnapshot } from '../services/goalService'; import { Tooltip, IconButton } from '@/component-library'; import { useGitState } from '@/tools/git/hooks/useGitState'; import type { SessionExecutionTarget } from '@/infrastructure/api/service-api/WorktreeAPI'; @@ -38,6 +38,7 @@ export interface ChatInputWorkspaceStripProps { threadGoal?: { visible: boolean; goal: ThreadGoalSnapshot | null; + goalChain?: GoalChainEntry[]; onOpen: () => void; }; /** Global native-tool permission mode exposed as a compact strip control. */ @@ -399,10 +400,66 @@ export const ChatInputWorkspaceStrip: React.FC = (
) : null} {showGoal ? ( - + threadGoal.goalChain && threadGoal.goalChain.length > 0 ? ( +
+ {threadGoal.goalChain.map((entry, index) => { + const isLast = index === threadGoal.goalChain!.length - 1; + const hasGoal = !!entry.goal?.objective; + const objective = entry.goal?.objective ?? ''; + const truncated = + objective.length > 24 + ? objective.slice(0, 24) + '\u2026' + : objective; + return ( + + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + threadGoal.onOpen(); + } + } + : undefined + } + > + {hasGoal ? ( + + {truncated} + + ) : ( + + L{entry.depth} + + )} + + {!isLast && ( + + › + + )} + + ); + })} +
+ ) : ( + + ) ) : null} {showUsage ? ( diff --git a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx index ac1a0dc609..9b9a4c7e48 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx @@ -241,6 +241,7 @@ export const ModernFlowChatContainer: React.FC = ( const { t } = useTranslation('flow-chat'); const virtualItems = useVirtualItems(); const activeSession = useActiveSession(); + const { requests: permissionRequests, activeBatch: activePermissionBatch, diff --git a/src/web-ui/src/flow_chat/hooks/useThreadGoalController.ts b/src/web-ui/src/flow_chat/hooks/useThreadGoalController.ts index 7a964fe1b9..0b7cfbcf88 100644 --- a/src/web-ui/src/flow_chat/hooks/useThreadGoalController.ts +++ b/src/web-ui/src/flow_chat/hooks/useThreadGoalController.ts @@ -14,9 +14,11 @@ import { import { parseGoalCommand } from '../services/goalCommandParser'; import { fetchSessionThreadGoal, + fetchGoalChain, runGoalCommandSafely, runThreadGoalUiAction, saveThreadGoalObjective, + type GoalChainEntry, type ThreadGoalSnapshot, } from '../services/goalService'; @@ -24,6 +26,7 @@ const HISTORICAL_THREAD_GOAL_REFRESH_DELAY_MS = 350; export interface ThreadGoalController { goal: ThreadGoalSnapshot | null; + goalChain: GoalChainEntry[]; menuOpen: boolean; editOpen: boolean; editMode: 'create' | 'update'; @@ -43,6 +46,7 @@ export interface ThreadGoalController { saveEdit: (objective: string) => Promise; confirmResume: () => Promise; dismissResume: () => void; + loadGoalChain: () => Promise; } function readStoreGoal(sessionId: string | undefined): ThreadGoalSnapshot | null { @@ -122,6 +126,21 @@ export function useThreadGoalController( const goal = storeGoal; + const [goalChain, setGoalChain] = useState([]); + + const loadGoalChain = useCallback(async () => { + if (!session || isBtwSession) { + setGoalChain([]); + return; + } + try { + const chain = await fetchGoalChain(session); + setGoalChain(chain); + } catch { + // best-effort: keep whatever chain we had before + } + }, [session, isBtwSession]); + const titles = useMemo( () => ({ usageMessage: t('chatInput.goalUsage'), @@ -160,6 +179,16 @@ export function useThreadGoalController( void refreshGoal(); }, [session?.isHistorical, sessionId, isBtwSession, refreshGoal]); + useEffect(() => { + void loadGoalChain(); + }, [loadGoalChain]); + + // Reload goal chain when the store goal changes (e.g. after /goal set or edit). + useEffect(() => { + if (!sessionId || isBtwSession) return; + void loadGoalChain(); + }, [sessionId, isBtwSession, goal?.goalId, goal?.status, goal?.updatedAt, loadGoalChain]); + const goalId = goal?.goalId; const goalStatus = goal?.status; const goalUpdatedAt = goal?.updatedAt; @@ -310,6 +339,7 @@ export function useThreadGoalController( return useMemo( () => ({ goal, + goalChain, menuOpen, editOpen, editMode, @@ -328,6 +358,7 @@ export function useThreadGoalController( saveEdit, confirmResume, dismissResume, + loadGoalChain, }), [ availableActions, @@ -340,6 +371,8 @@ export function useThreadGoalController( editMode, editOpen, goal, + goalChain, + loadGoalChain, menuOpen, openEdit, openGoalEntry, diff --git a/src/web-ui/src/flow_chat/services/AgenticEventListener.ts b/src/web-ui/src/flow_chat/services/AgenticEventListener.ts index 84f7f149ce..f58ee0e90e 100644 --- a/src/web-ui/src/flow_chat/services/AgenticEventListener.ts +++ b/src/web-ui/src/flow_chat/services/AgenticEventListener.ts @@ -13,6 +13,7 @@ import type { ToolEvent, AgenticEvent, SubagentSessionLinkedEvent, + SubagentTurnCompletedEvent, SessionTitleGeneratedEvent, SessionModelAutoMigratedEvent, ImageAnalysisEvent, @@ -23,6 +24,7 @@ import type { DeepReviewQueueStateChangedEvent, AcpContextUsageUpdatedEvent, OpenBuiltInBrowserEvent, + ThreadGoalUpdatedPayload, } from '@/infrastructure/api/service-api/AgentAPI'; import { createLogger } from '@/shared/utils/logger'; @@ -43,6 +45,7 @@ export interface AgenticEventCallbacks { onTextChunk?: (event: TextChunkEvent) => void; onToolEvent?: (event: ToolEvent) => void; onSubagentSessionLinked?: (event: SubagentSessionLinkedEvent) => void; + onSubagentTurnCompleted?: (event: SubagentTurnCompletedEvent) => void; onDeepReviewQueueStateChanged?: (event: DeepReviewQueueStateChangedEvent) => void; onDialogTurnCompleted?: (event: AgenticEvent) => void; onDialogTurnFailed?: (event: AgenticEvent) => void; @@ -52,7 +55,7 @@ export interface AgenticEventCallbacks { onContextCompressionStarted?: (event: AgenticEvent) => void; onContextCompressionCompleted?: (event: AgenticEvent) => void; onContextCompressionFailed?: (event: AgenticEvent) => void; - onThreadGoalUpdated?: (event: { sessionId: string; goal?: Record | null }) => void; + onThreadGoalUpdated?: (event: ThreadGoalUpdatedPayload) => void; onOpenBuiltInBrowser?: (event: OpenBuiltInBrowserEvent) => void; onSessionTitleGenerated?: (event: SessionTitleGeneratedEvent) => void; onSessionModelAutoMigrated?: (event: SessionModelAutoMigratedEvent) => void; @@ -166,6 +169,14 @@ export class AgenticEventListener { this.unlistenFunctions.push(unlisten); } + if (callbacks.onSubagentTurnCompleted) { + const unlisten = agentAPI.onSubagentTurnCompleted((event) => { + logger.debug('Subagent turn completed:', event); + callbacks.onSubagentTurnCompleted?.(event); + }); + this.unlistenFunctions.push(unlisten); + } + if (callbacks.onDeepReviewQueueStateChanged) { const unlisten = agentAPI.onDeepReviewQueueStateChanged((event) => { logger.debug('Deep Review queue state changed:', event); diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts index 6b9bffeb33..02868f8ea5 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts @@ -35,6 +35,7 @@ import type { AcpContextUsageUpdatedEvent, SessionModelAutoMigratedEvent, SubagentSessionLinkedEvent, + SubagentTurnCompletedEvent, } from '@/infrastructure/api/service-api/AgentAPI'; import { MCPAPI } from '@/infrastructure/api/service-api/MCPAPI'; import { ACPClientAPI, type AcpPermissionRequestEvent } from '@/infrastructure/api/service-api/ACPClientAPI'; @@ -529,6 +530,50 @@ function handleSubagentSessionLinked( reconcileBackgroundSubagentSession(childSessionId); } +function handleSubagentTurnCompleted( + context: FlowChatContext, + event: SubagentTurnCompletedEvent, +): void { + const childSessionId = event?.sessionId ?? (event as any)?.childSessionId; + const parentSessionId = event?.parentSessionId ?? (event as any)?.parent_session_id; + const parentDialogTurnId = + event?.parentDialogTurnId ?? (event as any)?.parent_dialog_turn_id; + const parentToolCallId = event?.parentToolCallId ?? (event as any)?.parent_tool_call_id; + const subagentDialogTurnId = + event?.subagentDialogTurnId ?? (event as any)?.subagent_dialog_turn_id; + const modelId = event?.modelId ?? (event as any)?.model_id; + const effectiveModelName = event?.effectiveModelName ?? (event as any)?.effective_model_name; + + if (childSessionId && parentSessionId && parentDialogTurnId && parentToolCallId) { + const parentInfo: SubagentParentInfo = { + sessionId: parentSessionId, + dialogTurnId: parentDialogTurnId, + toolCallId: parentToolCallId, + }; + attachSubagentSessionToParentTool(parentInfo, childSessionId, subagentDialogTurnId); + if (typeof modelId === 'string' && modelId.trim()) { + FlowChatStore.getInstance().updateSessionModelName(childSessionId, modelId.trim()); + } + } + + if (subagentDialogTurnId && parentSessionId && parentDialogTurnId && parentToolCallId) { + updateSubagentParentTaskModel( + context, + { + sessionId: parentSessionId, + dialogTurnId: parentDialogTurnId, + toolCallId: parentToolCallId, + }, + typeof modelId === 'string' && modelId.trim() ? modelId.trim() : undefined, + typeof effectiveModelName === 'string' && effectiveModelName.trim() + ? effectiveModelName.trim() + : '', + ); + } + + reconcileBackgroundSubagentSession(childSessionId); +} + function getLinkedSubagentParentInfo(sessionId: string): SubagentParentInfo | undefined { const session = FlowChatStore.getInstance().getState().sessions.get(sessionId); if ( @@ -812,6 +857,9 @@ export async function initializeEventListeners( onSessionModelAutoMigrated: (event) => { handleSessionModelAutoMigrated(event); }, + onSubagentTurnCompleted: (event) => { + handleSubagentTurnCompleted(context, event); + }, onUserSteeringInjected: (event) => { handleUserSteeringInjected(context, event); } diff --git a/src/web-ui/src/flow_chat/services/goalService.ts b/src/web-ui/src/flow_chat/services/goalService.ts index dfbecd51f8..0b347e2b4a 100644 --- a/src/web-ui/src/flow_chat/services/goalService.ts +++ b/src/web-ui/src/flow_chat/services/goalService.ts @@ -9,6 +9,13 @@ import { sessionProjectWorkspacePath } from '../utils/sessionWorkspace'; export { isGoalSlashCommand, parseGoalCommand } from './goalCommandParser'; export type { GoalCommandAction } from './goalCommandParser'; +export interface GoalChainEntry { + sessionId: string; + sessionName: string; + goal: ThreadGoalSnapshot | null; + depth: number; +} + export interface ThreadGoalSnapshot { goalId?: string; objective: string; @@ -322,6 +329,51 @@ export async function saveThreadGoalObjective( return snapshot; } +/** + * Walk up the parentSessionId chain from the given session to the root, + * fetch the thread goal for every ancestor, and return an ordered list + * from L0 (root) to the current session. + */ +export async function fetchGoalChain(session: Session): Promise { + const ancestors: Session[] = []; + const visited = new Set(); + let current: Session | undefined = session; + + while (current && !visited.has(current.sessionId)) { + visited.add(current.sessionId); + ancestors.push(current); + if (current.parentSessionId) { + current = flowChatStore.getState().sessions.get(current.parentSessionId); + } else { + break; + } + } + + // Reverse so the root (L0) comes first + ancestors.reverse(); + + const result: GoalChainEntry[] = []; + for (let i = 0; i < ancestors.length; i++) { + const s = ancestors[i]; + let goal: ThreadGoalSnapshot | null = null; + if (s.workspacePath) { + try { + goal = await fetchSessionThreadGoal(s); + } catch { + // best-effort: goal fetch failure shouldn't block the chain + } + } + result.push({ + sessionId: s.sessionId, + sessionName: s.title || `Session ${s.sessionId}`, + goal, + depth: i, + }); + } + + return result; +} + function resolveGoalCommandError(error: unknown, params: GoalCommandParams): string { if (!(error instanceof Error)) { return params.unknownErrorMessage; diff --git a/src/web-ui/src/flow_chat/store/FlowChatStore.ts b/src/web-ui/src/flow_chat/store/FlowChatStore.ts index b2781e88da..7869bc48ca 100644 --- a/src/web-ui/src/flow_chat/store/FlowChatStore.ts +++ b/src/web-ui/src/flow_chat/store/FlowChatStore.ts @@ -1,4 +1,4 @@ -/** +/** * Flow Chat global state store * Prevents state loss when components remount */ @@ -893,6 +893,52 @@ interface SelectorListener { hasLastValue: boolean; } +export interface SessionTreeNode { + sessionId: string; + sessionName: string; + agentType: string; + agentDisplayName: string; + depth: number; + status: 'running' | 'completed' | 'error' | 'cancelled'; + children: SessionTreeNode[]; + isAcpExternal: boolean; + externalProviderLabel?: string; + /** �?SubAgent 的默认工具列表(�?Agent registry 获取�?*/ + tools?: string[]; + /** 对话轮数 */ + turnCount?: number; +} + +function sessionTreeNodeStatus(session: Session): SessionTreeNode['status'] { + if (session.status === 'error') return 'error'; + if (session.persistedStatus === 'completed') return 'completed'; + if (session.persistedStatus === 'archived') return 'completed'; + if (session.status === 'active') return 'running'; + return 'running'; +} + +const SUBAGENT_TOOLS: Record = { + 'Explore': ['Read', 'Grep', 'Glob', 'LS'], + 'FileFinder': ['Read', 'Grep', 'Glob', 'LS'], + 'GeneralPurpose': ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'ExecCommand', 'Task'], + 'ResearchSpecialist': ['WebSearch', 'WebFetch', 'Read'], + 'CodeReview': ['Read', 'Grep', 'Glob', 'GetFileDiff'], + 'ReviewSecurity': ['Read', 'Grep', 'Glob', 'GetFileDiff'], + 'ReviewArchitecture': ['Read', 'Grep', 'Glob', 'GetFileDiff'], + 'ReviewBusinessLogic': ['Read', 'Grep', 'Glob', 'GetFileDiff'], + 'ReviewFrontend': ['Read', 'Grep', 'Glob', 'GetFileDiff'], + 'ReviewPerformance': ['Read', 'Grep', 'Glob', 'GetFileDiff'], + 'ReviewJudge': ['Read', 'Grep', 'Glob'], +}; + +function inferSessionTools(session: Session): string[] { + const type = session.subagentType || session.mode || ''; + if (SUBAGENT_TOOLS[type]) return SUBAGENT_TOOLS[type]; + if (type.startsWith('acp__')) return ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'ExecCommand', 'Task', 'SessionControl']; + if (type.startsWith('Review')) return ['Read', 'Grep', 'Glob', 'GetFileDiff']; + return []; +} + export class FlowChatStore { private static instance: FlowChatStore; private state: FlowChatState; @@ -1689,20 +1735,24 @@ export class FlowChatStore { const visited = new Set(); const orderedSessionIds: string[] = []; - const visit = (sessionId: string): void => { + const MAX_CASCADE_DEPTH = 256; + const visit = (sessionId: string, depth: number = 0): void => { if (visited.has(sessionId)) { return; } + if (depth > MAX_CASCADE_DEPTH) { + return; + } visited.add(sessionId); const childSessionIds = childSessionIdsByParent.get(sessionId) || []; childSessionIds.forEach(childSessionId => { - visit(childSessionId); + visit(childSessionId, depth + 1); }); orderedSessionIds.push(sessionId); }; - visit(rootSessionId); + visit(rootSessionId, 0); return orderedSessionIds; } @@ -1710,6 +1760,58 @@ export class FlowChatStore { return this.collectCascadeSessionIds(sessionId, this.state.sessions); } + // ── R-001: Legion Tree ────────────────────────────── + + public getSessionTree(sessionId: string): SessionTreeNode | null { + const sessions = this.state.sessions; + const rootSession = sessions.get(sessionId); + if (!rootSession) return null; + return this.buildSessionTreeNode(sessionId, sessions, 0); + } + + private buildSessionTreeNode( + sessionId: string, + sessions: Map, + depth: number, + ): SessionTreeNode { + const MAX_TREE_BUILD_DEPTH = 256; + if (depth > MAX_TREE_BUILD_DEPTH) { + const s = sessions.get(sessionId); + return { + sessionId, + sessionName: s?.title ?? sessionId, + agentType: s?.mode ?? 'unknown', + agentDisplayName: s?.subagentType ?? s?.mode ?? 'unknown', + depth, + status: 'running' as const, + children: [], + isAcpExternal: (s?.mode ?? '').startsWith('acp__'), + externalProviderLabel: s?.subagentType ?? undefined, + turnCount: s?.dialogTurns?.length ?? 0, + tools: s ? inferSessionTools(s) : undefined, + }; + } + + const session = sessions.get(sessionId)!; + const childIds = Array.from(sessions.values()) + .filter(s => s.parentSessionId === sessionId) + .map(s => s.sessionId); + + return { + sessionId: session.sessionId, + sessionName: session.title || session.sessionId, + agentType: session.mode || 'unknown', + agentDisplayName: session.subagentType || session.mode || 'unknown', + depth, + status: sessionTreeNodeStatus(session), + children: childIds.map(id => this.buildSessionTreeNode(id, sessions, depth + 1)), + isAcpExternal: (session.mode || '').startsWith('acp__'), + externalProviderLabel: session.subagentType ?? undefined, + turnCount: session.dialogTurns?.length ?? 0, + tools: inferSessionTools(session), + }; + } + public subscribe(listener: (state: FlowChatState) => void): () => void { this.listeners.add(listener); return () => { @@ -1808,6 +1910,7 @@ export class FlowChatStore { sessionKind: relationship.sessionKind, parentToolCallId: relationship.parentToolCallId, subagentType: relationship.subagentType, + depth: relationship.depth, btwThreads: [], btwOrigin: relationship.btwOrigin, isTransient: false, @@ -1839,6 +1942,7 @@ export class FlowChatStore { btwOrigin?: Session['btwOrigin']; parentToolCallId?: string; subagentType?: string; + depth?: number; isTransient?: boolean; agentBackedTransient?: boolean; deepReviewRunManifest?: Session['deepReviewRunManifest']; @@ -1871,7 +1975,8 @@ export class FlowChatStore { titleStatus: 'generated', dialogTurns: [], status: 'idle', - config: { + +config: { maxContextTokens: 128128, autoCompact: true, enableTools: true, @@ -1899,6 +2004,7 @@ export class FlowChatStore { sessionKind: relationship.sessionKind, parentToolCallId: relationship.parentToolCallId, subagentType: relationship.subagentType, + depth: relationship.depth, btwThreads: [], btwOrigin: relationship.btwOrigin, deepReviewRunManifest: meta?.deepReviewRunManifest, @@ -2196,6 +2302,7 @@ export class FlowChatStore { updates.subagentType !== undefined ? updates.subagentType : session.subagentType, + depth: session.depth, }); const next: Session = { ...session, @@ -2203,6 +2310,7 @@ export class FlowChatStore { sessionKind: relationship.sessionKind, parentToolCallId: relationship.parentToolCallId, subagentType: relationship.subagentType, + depth: relationship.depth, btwOrigin: relationship.btwOrigin, }; @@ -2226,11 +2334,13 @@ export class FlowChatStore { sessionKind, parentSessionId: origin?.parentSessionId ?? session.parentSessionId, btwOrigin: { ...(session.btwOrigin || {}), ...(origin || {}) }, + depth: session.depth, }); const next: Session = { ...session, parentSessionId: relationship.parentSessionId, sessionKind: relationship.sessionKind, + depth: relationship.depth, btwOrigin: relationship.btwOrigin, }; @@ -3870,6 +3980,7 @@ export class FlowChatStore { sessionKind: relationship.sessionKind, parentToolCallId: relationship.parentToolCallId, subagentType: relationship.subagentType, + depth: relationship.depth, btwThreads: [], btwOrigin: relationship.btwOrigin, hasUnreadCompletion: metadata.unreadCompletion, @@ -4239,6 +4350,7 @@ export class FlowChatStore { sessionKind: relationship.sessionKind, parentToolCallId: relationship.parentToolCallId, subagentType: relationship.subagentType, + depth: relationship.depth, btwThreads: [], btwOrigin: relationship.btwOrigin, hasUnreadCompletion: metadata.unreadCompletion, diff --git a/src/web-ui/src/flow_chat/types/flow-chat.ts b/src/web-ui/src/flow_chat/types/flow-chat.ts index c9823dbd1d..c98843be00 100644 --- a/src/web-ui/src/flow_chat/types/flow-chat.ts +++ b/src/web-ui/src/flow_chat/types/flow-chat.ts @@ -18,6 +18,9 @@ export interface FlowItem { id: string; type: 'text' | 'tool' | 'image-analysis' | 'thinking' | 'user-steering'; timestamp: number; + /** TODO(tech-debt): This union is too wide — every concrete FlowItem subtype only + * uses a subset of these statuses. Consider narrowing per-type via discriminated + * union or a base `status` plus per-subtype status enums. */ status: 'pending' | 'queued' | 'waiting' | 'preparing' | 'running' | 'streaming' | 'receiving' | 'completed' | 'cancelled' | 'rejected' | 'error' | 'analyzing' | 'pending_confirmation' | 'confirmed'; // Includes error, analyzing, and confirmation states. attemptId?: string; attemptIndex?: number; @@ -396,6 +399,9 @@ export interface Session { /** * Optional parent session id for hierarchical sessions. * Used by /btw "side threads" and potentially other derived sessions. + * + * NOTE: This is a UI-local field derived from SessionRelationship; the backend + * SessionTreeNode uses `relationship.parent_session_id`. Keep in sync. */ parentSessionId?: string; @@ -411,6 +417,13 @@ export interface Session { /** Logical subagent id / type used to launch this hidden subagent session. */ subagentType?: string; + /** + * Depth in session tree (root = 0, child = parent_depth + 1). + * NOTE: This is a UI-local cache derived from the backend SessionTreeManager. + * The backend assigns depth on registration; the UI mirrors it for nav rendering. + */ + depth?: number; + /** Whether `/goal` mode is active for this session. */ goalModeActive?: boolean; @@ -507,6 +520,9 @@ export interface SessionConfig { /** Disambiguates sessions when multiple remote workspaces share the same `workspacePath`. */ remoteConnectionId?: string; remoteSshHost?: string; + maxContextTokens?: number; + autoCompact?: boolean; + enableTools?: boolean; } /** diff --git a/src/web-ui/src/flow_chat/utils/sessionMetadata.ts b/src/web-ui/src/flow_chat/utils/sessionMetadata.ts index 9707f651ce..216ee4199b 100644 --- a/src/web-ui/src/flow_chat/utils/sessionMetadata.ts +++ b/src/web-ui/src/flow_chat/utils/sessionMetadata.ts @@ -29,7 +29,7 @@ const TOP_LEVEL_METADATA_KEYS = new Set([ type SessionRelationshipInput = Pick< Session, - 'sessionKind' | 'parentSessionId' | 'btwOrigin' | 'parentToolCallId' | 'subagentType' + 'sessionKind' | 'parentSessionId' | 'btwOrigin' | 'parentToolCallId' | 'subagentType' | 'depth' >; export interface ResolvedSessionRelationship { @@ -79,7 +79,7 @@ export function normalizeSessionRelationship( input?: Partial | null ): Pick< Session, - 'sessionKind' | 'parentSessionId' | 'btwOrigin' | 'parentToolCallId' | 'subagentType' + 'sessionKind' | 'parentSessionId' | 'btwOrigin' | 'parentToolCallId' | 'subagentType' | 'depth' > { const sessionKind = normalizeSessionKind(input?.sessionKind); const parentSessionId = normalizeString( @@ -101,6 +101,7 @@ export function normalizeSessionRelationship( btwOrigin: undefined, parentToolCallId: undefined, subagentType: undefined, + depth: undefined, }; } @@ -117,6 +118,7 @@ export function normalizeSessionRelationship( btwOrigin: origin, parentToolCallId, subagentType, + depth: input?.depth, }; } @@ -149,7 +151,7 @@ export function deriveSessionRelationshipFromMetadata( metadata?: Pick | null ): Pick< Session, - 'sessionKind' | 'parentSessionId' | 'btwOrigin' | 'parentToolCallId' | 'subagentType' + 'sessionKind' | 'parentSessionId' | 'btwOrigin' | 'parentToolCallId' | 'subagentType' | 'depth' > { const relationship = metadata?.relationship; const relationshipKind = normalizeSessionKind(relationship?.kind); @@ -159,6 +161,7 @@ export function deriveSessionRelationshipFromMetadata( parentSessionId: normalizeString(relationship?.parentSessionId) ?? undefined, parentToolCallId: normalizeString(relationship?.parentToolCallId), subagentType: normalizeString(relationship?.subagentType), + depth: relationship?.depth ?? undefined, btwOrigin: { requestId: normalizeString(relationship?.parentRequestId), parentSessionId: normalizeString(relationship?.parentSessionId), @@ -177,6 +180,7 @@ export function deriveSessionRelationshipFromMetadata( parentSessionId: customMetadata?.parentSessionId ?? undefined, parentToolCallId: normalizeString(customMetadata?.parentToolCallId), subagentType: normalizeString(customMetadata?.subagentType), + depth: undefined, btwOrigin: sessionKind !== 'normal' ? { diff --git a/src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts b/src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts index 3f09d8610a..b5b0bce4ac 100644 --- a/src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/AgentAPI.ts @@ -369,6 +369,21 @@ export interface OpenBuiltInBrowserEvent { replaceExisting?: boolean; } +/** Payload delivered on `agentic://thread-goal-updated`. */ +export interface ThreadGoalUpdatedPayload { + sessionId: string; + goal?: { + goalId: string; + objective: string; + status: string; + tokensUsed?: number; + tokenBudget?: number | null; + timeUsedSeconds?: number; + updatedAt?: number; + autoContinuationCount?: number; + } | null; +} + export interface TextChunkEvent extends AgenticEvent { roundId: string; attemptId?: string; @@ -395,6 +410,16 @@ export interface SubagentSessionLinkedEvent extends AgenticEvent { focusedReviewDisplayLabel?: string; } +export interface SubagentTurnCompletedEvent extends AgenticEvent { + subagentDialogTurnId?: string; + parentSessionId: string; + parentDialogTurnId: string; + parentToolCallId: string; + agentType?: string; + status?: string; + outputText?: string; +} + export type DeepReviewQueueStatus = | 'queued_for_capacity' | 'paused_by_user' @@ -1014,6 +1039,15 @@ export class AgentAPI { ); } + onSubagentTurnCompleted( + callback: (event: SubagentTurnCompletedEvent) => void + ): () => void { + return api.listen( + 'agentic://subagent-turn-completed', + callback + ); + } + onDeepReviewQueueStateChanged( callback: (event: DeepReviewQueueStateChangedEvent) => void ): () => void { @@ -1074,9 +1108,9 @@ export class AgentAPI { } onThreadGoalUpdated( - callback: (event: { sessionId: string; goal?: Record | null }) => void + callback: (event: ThreadGoalUpdatedPayload) => void ): () => void { - return api.listen('agentic://thread-goal-updated', callback); + return api.listen('agentic://thread-goal-updated', callback); } onOpenBuiltInBrowser(callback: (event: OpenBuiltInBrowserEvent) => void): () => void { diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 4efd58db2c..391a055f0e 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -347,7 +347,7 @@ "complete": "When proven, the agent calls update_goal to mark Complete" }, "note": { - "active": "While active, each finished turn auto-continues toward the goal (up to 100 times) until the agent calls update_goal(complete) or the limit is reached.", + "active": "While active, each finished turn auto-continues toward the goal (up to 10 times) until the agent calls update_goal(complete) or the limit is reached.", "complete": "Marked complete. Edit or clear the goal to start different work.", "paused": "Paused — continuation and completion checks are stopped.", "blocked": "Blocked — resume with /goal resume after you unblock or the environment changes.", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 40669ed696..70906ea598 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -347,7 +347,7 @@ "complete": "证据充分后由代理调用 update_goal 标记为「已完成」" }, "note": { - "active": "目标仍为进行中时,每轮对话结束会自动续跑(最多 100 次),直到代理调用 update_goal 标为已完成或达到上限。", + "active": "目标仍为进行中时,每轮对话结束会自动续跑(最多 10 次),直到代理调用 update_goal 标为已完成或达到上限。", "complete": "目标已标记完成。如需继续其他工作,可编辑或清除目标。", "paused": "目标已暂停,续跑与完成检查已停止。", "blocked": "目标已阻塞,需你介入或环境变化后再 /goal resume。", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index c4da9e8ea3..cdbfe0b5af 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -347,7 +347,7 @@ "complete": "證據充分後由代理呼叫 update_goal 標記為「已完成」" }, "note": { - "active": "目標仍為進行中時,每輪對話結束會自動續跑(最多 100 次),直到代理呼叫 update_goal 標為已完成或達到上限。", + "active": "目標仍為進行中時,每輪對話結束會自動續跑(最多 10 次),直到代理呼叫 update_goal 標為已完成或達到上限。", "complete": "目標已標記完成。如需繼續其他工作,可編輯或清除目標。", "paused": "目標已暫停,續跑與完成檢查已停止。", "blocked": "目標已阻塞,需你介入或環境變化後再 /goal resume。", diff --git a/src/web-ui/src/shared/types/session-history.ts b/src/web-ui/src/shared/types/session-history.ts index 1b83924d38..f59cade9eb 100644 --- a/src/web-ui/src/shared/types/session-history.ts +++ b/src/web-ui/src/shared/types/session-history.ts @@ -21,6 +21,7 @@ export interface SessionRelationship { parentTurnIndex?: number | null; parentToolCallId?: string | null; subagentType?: string | null; + depth?: number | null; } export interface SessionCustomMetadata extends Record { From 84ec005dc6b753945ee8e8712fae55b1669dc6d0 Mon Sep 17 00:00:00 2001 From: 1688mengdie <1688mengdie@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:28:57 +0800 Subject: [PATCH 2/2] fix: add missing workspace members/license/deps --- Cargo.toml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index ae6cace31b..eac12e1048 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,28 @@ members = [ "src/crates/contracts/core-types", "src/crates/contracts/events", "src/crates/contracts/runtime-ports", + # taiji-quant 量化引擎 crates + "src/crates/taiji/taiji-abnormal", + "src/crates/taiji/taiji-alert", + "src/crates/taiji/taiji-backtest", + "src/crates/taiji/taiji-bar", + "src/crates/taiji/taiji-blog-gen", + "src/crates/taiji/taiji-cli", + "src/crates/taiji/taiji-content", + "src/crates/taiji/taiji-engine", + "src/crates/taiji/taiji-engine-py", + "src/crates/taiji/taiji-example", + "src/crates/taiji/taiji-executor", + "src/crates/taiji/taiji-growth", + "src/crates/taiji/taiji-knowledge-graph", + "src/crates/taiji/taiji-llm", + "src/crates/taiji/taiji-orderflow", + "src/crates/taiji/taiji-pattern", + "src/crates/taiji/taiji-publisher", + "src/crates/taiji/taiji-realtime", + "src/crates/taiji/taiji-sentiment", + "src/crates/taiji/taiji-strategen", + "src/crates/taiji/taiji-strategy-template", ] exclude = [ @@ -49,6 +71,7 @@ resolver = "2" version = "0.2.14" # x-release-please-version authors = ["BitFun Team"] edition = "2021" +license = "MIT" [workspace.lints.rust] unsafe_op_in_unsafe_fn = "warn" @@ -118,6 +141,21 @@ chardetng = "0.1.17" encoding_rs = "0.8.35" url = "2" +# taiji-quant 量化引擎依赖 +rayon = "1.10" +csv = "1.3" +parking_lot = "0.12" +petgraph = { version = "0.6", features = ["serde-1"] } +jieba-rs = "0.7" +candle-core = "0.7" +candle-nn = "0.7" +ndarray = { version = "0.16", features = ["serde"] } +pyo3 = { version = "0.23", features = ["extension-module"] } +statrs = "0.17" +lettre = { version = "0.11", default-features = false, features = ["builder", "smtp-transport", "tokio1-rustls-tls"] } +tera = "1.20" +crossbeam = "0.8" + # HTTP client reqwest = { version = "0.13.4", default-features = false, features = ["native-tls", "rustls", "http2", "json", "stream", "multipart", "query", "form"] }