@steerable/agent-shell-web 0.6.19 → 0.6.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +15 -8
  2. package/src/components/chat/ApprovalModal.tsx +21 -41
  3. package/src/components/chat/ApprovalPromptProvider.tsx +98 -0
  4. package/src/components/chat/AskUserPromptProvider.tsx +99 -0
  5. package/src/components/chat/AssistantMessage.tsx +25 -2
  6. package/src/components/chat/ChatInput.tsx +116 -13
  7. package/src/components/chat/FilePathCode.tsx +65 -0
  8. package/src/components/chat/LocalChatPanel.tsx +19 -0
  9. package/src/components/chat/Markdown.tsx +54 -25
  10. package/src/components/chat/MessageList.tsx +66 -18
  11. package/src/components/chat/ModelPicker.tsx +5 -3
  12. package/src/components/chat/SuggestedReplies.tsx +34 -0
  13. package/src/components/chat/TurnFilesCard.tsx +113 -0
  14. package/src/components/chat/TurnProcessGroup.tsx +7 -2
  15. package/src/components/chat/path-mentions.ts +148 -0
  16. package/src/components/chat/suggested-replies-model.ts +40 -0
  17. package/src/components/chat/turn-files.ts +50 -0
  18. package/src/layouts/AgentLayout.tsx +15 -26
  19. package/src/lib/browser-dev-electron-mock.ts +15 -0
  20. package/src/lib/electron-bridge.ts +13 -7
  21. package/src/lib/http-bridge.ts +8 -2
  22. package/src/lib/local-api.ts +31 -0
  23. package/src/pages/AgentPage.tsx +126 -1
  24. package/src/pages/ChatPanelPreviewPage.tsx +4 -0
  25. package/vite.base.d.ts +7 -0
  26. package/vite.base.js +83 -0
  27. package/src/brand.test.ts +0 -22
  28. package/src/components/AgentSidebar.test.tsx +0 -791
  29. package/src/components/ChatHeader.test.tsx +0 -230
  30. package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
  31. package/src/components/chat/ApprovalModal.test.tsx +0 -83
  32. package/src/components/chat/AskUserModalHost.test.tsx +0 -176
  33. package/src/components/chat/AskUserModalHost.tsx +0 -61
  34. package/src/components/chat/AskUserQuestionMenu.test.tsx +0 -190
  35. package/src/components/chat/AssistantMessage.test.tsx +0 -68
  36. package/src/components/chat/ChatInput.test.tsx +0 -609
  37. package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
  38. package/src/components/chat/Markdown.test.tsx +0 -170
  39. package/src/components/chat/ModelPicker.test.tsx +0 -149
  40. package/src/components/chat/SessionTreeModal.test.tsx +0 -161
  41. package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
  42. package/src/components/chat/TaskPanelModal.test.tsx +0 -198
  43. package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
  44. package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
  45. package/src/components/chat/elapsed.test.ts +0 -42
  46. package/src/components/chat/executed-actions-model.test.ts +0 -134
  47. package/src/components/chat/orchestration-children-model.test.ts +0 -115
  48. package/src/components/chat/turn-timeline.test.ts +0 -97
  49. package/src/components/chat/useChatTasks.test.ts +0 -136
  50. package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
  51. package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
  52. package/src/components/settings/LlmSettingsPanel.test.tsx +0 -461
  53. package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
  54. package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
  55. package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
  56. package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
  57. package/src/components/settings/compat-flags-model.test.ts +0 -71
  58. package/src/components/settings/llm-vendors.test.ts +0 -171
  59. package/src/components/settings/model-capabilities.test.ts +0 -88
  60. package/src/components/settings/preset-choice-model.test.ts +0 -96
  61. package/src/hooks/useChatsAndAgents.test.ts +0 -165
  62. package/src/lib/attachments.test.ts +0 -127
  63. package/src/lib/chat-title.test.ts +0 -53
  64. package/src/lib/chat-transport.test.ts +0 -282
  65. package/src/lib/date-groups.test.ts +0 -62
  66. package/src/lib/demo-flag.test.ts +0 -17
  67. package/src/lib/electron-bridge.test.ts +0 -54
  68. package/src/lib/exec-policy.test.ts +0 -71
  69. package/src/lib/http-bridge.test.ts +0 -283
  70. package/src/lib/insights.test.ts +0 -56
  71. package/src/lib/local-api.test.ts +0 -353
  72. package/src/lib/pending-first-message.test.ts +0 -17
  73. package/src/lib/slash-sources.test.ts +0 -161
  74. package/src/pages/AgentPage.test.tsx +0 -627
  75. package/src/pages/SettingsPage.test.tsx +0 -102
  76. package/vite.base.ts +0 -94
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.19",
3
+ "version": "0.6.21",
4
4
  "type": "module",
5
- "description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/ + vite.base.ts), not a build artifact.",
5
+ "description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://steerableframework.com/",
8
8
  "repository": {
@@ -14,13 +14,19 @@
14
14
  "url": "https://github.com/pathlyapp/steerable-framework/issues"
15
15
  },
16
16
  "exports": {
17
- "./vite.base": "./vite.base.ts",
17
+ "./vite.base": {
18
+ "types": "./vite.base.d.ts",
19
+ "default": "./vite.base.js"
20
+ },
18
21
  "./package.json": "./package.json"
19
22
  },
20
23
  "files": [
21
24
  "src",
25
+ "!src/**/*.test.ts",
26
+ "!src/**/*.test.tsx",
22
27
  "public",
23
- "vite.base.ts",
28
+ "vite.base.js",
29
+ "vite.base.d.ts",
24
30
  "README.md"
25
31
  ],
26
32
  "publishConfig": {
@@ -39,8 +45,8 @@
39
45
  "react-router-dom": "^6.30.0",
40
46
  "rehype-raw": "^7.0.0",
41
47
  "remark-gfm": "^4.0.1",
42
- "@steerable/agent-protocol": "0.6.19",
43
- "@steerable/agent-ui": "0.6.19"
48
+ "@steerable/agent-ui": "0.6.21",
49
+ "@steerable/agent-protocol": "0.6.21"
44
50
  },
45
51
  "devDependencies": {
46
52
  "@tailwindcss/vite": "^4.3.0",
@@ -53,11 +59,12 @@
53
59
  "typescript": "^5.8.3",
54
60
  "vite": "^5.4.21",
55
61
  "vitest": "^3.2.4",
56
- "@steerable/pack-sdk": "0.6.19"
62
+ "@steerable/pack-sdk": "0.6.21"
57
63
  },
58
64
  "scripts": {
59
65
  "gen": "echo agent-shell-web",
60
- "build": "tsc --noEmit",
66
+ "build": "tsc --noEmit && npm run build:vite-base",
67
+ "build:vite-base": "tsc vite.base.ts --module esnext --target es2022 --moduleResolution bundler --declaration --skipLibCheck --outDir .",
61
68
  "typecheck": "tsc --noEmit",
62
69
  "test": "vitest run",
63
70
  "lint": "tsc --noEmit",
@@ -1,25 +1,21 @@
1
- import { useEffect, useState } from 'react';
2
1
  import { LuShieldAlert, LuTriangleAlert } from 'react-icons/lu';
3
2
  import {
4
- getElectronBridge,
5
3
  type ApprovalDecisionKind,
6
4
  type ApprovalPromptRequest,
7
5
  } from '@/lib/electron-bridge';
8
6
 
9
7
  /**
10
- * ApprovalModalHost — W4-1 审批代数的 Electron UI 半侧。
8
+ * ApprovalPromptMenu — W4-1 审批代数的输入区 UI。
11
9
  *
12
- * 挂载一次(AgentPage),订阅主进程广播的 `approval:request`,弹出模态
13
- * 让用户对工具调用做 7 变体决策(允许/拒绝 × 一次性/会话/持久 + 中止)。
14
- * 决定经 `approval:decide` IPC 回到主进程,再应答 sidecar 的反向调用。
10
+ * 由 ApprovalPromptProvider 提供当前请求,在普通输入框的位置让用户对
11
+ * 工具调用做 7 变体决策(允许/拒绝 × 一次性/会话/持久 + 中止)。
15
12
  *
16
13
  * 持久化语义(与框架 ApprovalExecutor 对齐):
17
14
  * - 一次性:只作用于本次调用,不缓存。
18
15
  * - 会话:按 category(默认=工具名)缓存在本 chat 的会话级 cache。
19
16
  * - 持久:写入 ~/.steerable/approvals.json,跨会话生效。
20
17
  *
21
- * 队列为 FIFO:模型一批并发多个工具调用时逐个请示(sidecar 串行等待
22
- * 每个应答)。组件卸载/无窗口时主进程 fail-closed 为 deny_once。
18
+ * 请求队列及刷新恢复由 Provider 管理。
23
19
  */
24
20
 
25
21
  const MODE_LABEL: Record<string, string> = {
@@ -88,26 +84,15 @@ function DecisionRow({
88
84
  );
89
85
  }
90
86
 
91
- export function ApprovalModalHost() {
92
- const [queue, setQueue] = useState<ApprovalPromptRequest[]>([]);
93
-
94
- useEffect(() => {
95
- const bridge = getElectronBridge();
96
- if (!bridge?.approval) return;
97
- return bridge.approval.onRequest((request) => {
98
- setQueue((prev) => [...prev, request]);
99
- });
100
- }, []);
101
-
102
- const current = queue[0] ?? null;
103
- if (!current) return null;
104
-
105
- const decide = (kind: ApprovalDecisionKind) => {
106
- const bridge = getElectronBridge();
107
- setQueue((prev) => prev.slice(1));
108
- void bridge?.approval?.decide({ requestId: current.requestId, kind });
109
- };
110
-
87
+ export function ApprovalPromptMenu({
88
+ request: current,
89
+ pendingCount,
90
+ onDecide,
91
+ }: {
92
+ request: ApprovalPromptRequest;
93
+ pendingCount: number;
94
+ onDecide: (kind: ApprovalDecisionKind) => void;
95
+ }) {
111
96
  const destructive = current.mode === 'destructive';
112
97
  // 网络出口拓宽(W-egress-ask):sidecar 的 web 工具被 egress 代理 403
113
98
  // 后发起。白名单是代理进程级的(会话寿命),所以「始终」变体对网络
@@ -121,13 +106,9 @@ export function ApprovalModalHost() {
121
106
 
122
107
  return (
123
108
  <div
124
- className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
125
- role="dialog"
126
- aria-modal="true"
127
- aria-label="工具调用审批"
128
- data-testid="approval-dialog"
109
+ className="overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas text-sm text-agent-foreground shadow-xl"
110
+ data-testid="approval-composer"
129
111
  >
130
- <div className="w-full max-w-lg rounded-agent-lg border border-agent-border bg-agent-canvas shadow-xl">
131
112
  <div className="flex items-center gap-2 border-b border-agent-border px-4 py-3">
132
113
  {destructive ? (
133
114
  <LuTriangleAlert className="h-4 w-4 shrink-0 text-agent-destructive" />
@@ -154,9 +135,9 @@ export function ApprovalModalHost() {
154
135
  </>
155
136
  )}
156
137
  </div>
157
- {queue.length > 1 && (
138
+ {pendingCount > 0 && (
158
139
  <div className="mt-0.5 text-[11px] text-agent-muted-foreground">
159
- 还有 {queue.length - 1} 个待审批
140
+ 还有 {pendingCount} 个待审批
160
141
  </div>
161
142
  )}
162
143
  </div>
@@ -195,7 +176,7 @@ export function ApprovalModalHost() {
195
176
  ? ALLOW_BUTTONS.filter((b) => b.kind !== 'allow_always')
196
177
  : ALLOW_BUTTONS
197
178
  }
198
- onPick={decide}
179
+ onPick={onDecide}
199
180
  />
200
181
  <div className="flex items-center justify-between gap-2">
201
182
  <DecisionRow
@@ -204,11 +185,11 @@ export function ApprovalModalHost() {
204
185
  ? DENY_BUTTONS.filter((b) => b.kind !== 'deny_always')
205
186
  : DENY_BUTTONS
206
187
  }
207
- onPick={decide}
188
+ onPick={onDecide}
208
189
  />
209
190
  <button
210
191
  type="button"
211
- onClick={() => decide('abort')}
192
+ onClick={() => onDecide('abort')}
212
193
  className="shrink-0 rounded-agent-md bg-agent-destructive px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
213
194
  data-testid="approval-abort"
214
195
  >
@@ -221,9 +202,8 @@ export function ApprovalModalHost() {
221
202
  : '「会话」决定在本对话内记住;「始终」决定写入本机 ~/.steerable/approvals.json,跨对话生效。不操作约 2 分钟后按拒绝处理。'}
222
203
  </p>
223
204
  </div>
224
- </div>
225
205
  </div>
226
206
  );
227
207
  }
228
208
 
229
- export default ApprovalModalHost;
209
+ export default ApprovalPromptMenu;
@@ -0,0 +1,98 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import {
12
+ getElectronBridge,
13
+ type ApprovalDecisionKind,
14
+ type ApprovalPromptRequest,
15
+ } from '@/lib/electron-bridge';
16
+
17
+ interface ApprovalPromptContextValue {
18
+ current: ApprovalPromptRequest | null;
19
+ pendingCount: number;
20
+ decide: (kind: ApprovalDecisionKind) => void;
21
+ }
22
+
23
+ const ApprovalPromptContext = createContext<ApprovalPromptContextValue | null>(null);
24
+
25
+ /** Keeps approval prompts alive while the active composer presents them. */
26
+ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
27
+ const [queue, setQueue] = useState<ApprovalPromptRequest[]>([]);
28
+ const decidedRequestIds = useRef(new Set<string>());
29
+
30
+ useEffect(() => {
31
+ const bridge = getElectronBridge();
32
+ if (!bridge?.approval) return;
33
+ let active = true;
34
+ const addRequests = (
35
+ requests: ApprovalPromptRequest[],
36
+ placement: 'append' | 'prepend' = 'append',
37
+ ) => {
38
+ setQueue((previous) => {
39
+ const known = new Set(previous.map(({ requestId }) => requestId));
40
+ const additions = requests.filter(
41
+ ({ requestId }) =>
42
+ !known.has(requestId) && !decidedRequestIds.current.has(requestId),
43
+ );
44
+ if (additions.length === 0) return previous;
45
+ return placement === 'prepend'
46
+ ? [...additions, ...previous]
47
+ : [...previous, ...additions];
48
+ });
49
+ };
50
+ const unsubscribe = bridge.approval.onRequest((request) => {
51
+ addRequests([request]);
52
+ });
53
+ void bridge.approval
54
+ .pending()
55
+ .then((requests) => {
56
+ if (active) addRequests(requests, 'prepend');
57
+ })
58
+ .catch((error) => {
59
+ console.error('恢复待审批请求失败:', error);
60
+ });
61
+ return () => {
62
+ active = false;
63
+ unsubscribe();
64
+ };
65
+ }, []);
66
+
67
+ const current = queue[0] ?? null;
68
+ const decide = useCallback(
69
+ (kind: ApprovalDecisionKind) => {
70
+ if (!current) return;
71
+ const bridge = getElectronBridge();
72
+ decidedRequestIds.current.add(current.requestId);
73
+ setQueue((previous) => previous.slice(1));
74
+ void bridge?.approval?.decide({ requestId: current.requestId, kind });
75
+ },
76
+ [current],
77
+ );
78
+
79
+ const value = useMemo(
80
+ () => ({
81
+ current,
82
+ pendingCount: Math.max(0, queue.length - 1),
83
+ decide,
84
+ }),
85
+ [current, decide, queue.length],
86
+ );
87
+
88
+ return (
89
+ <ApprovalPromptContext.Provider value={value}>
90
+ {children}
91
+ </ApprovalPromptContext.Provider>
92
+ );
93
+ }
94
+
95
+ /** Returns the approval prompt currently assigned to the chat composer. */
96
+ export function useApprovalPrompt(): ApprovalPromptContextValue | null {
97
+ return useContext(ApprovalPromptContext);
98
+ }
@@ -0,0 +1,99 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import { getElectronBridge, type AskUserPromptRequest } from '@/lib/electron-bridge';
12
+
13
+ type AnswerValue = string | string[];
14
+
15
+ interface AskUserPromptContextValue {
16
+ current: AskUserPromptRequest | null;
17
+ pendingCount: number;
18
+ answer: (answers: Record<string, AnswerValue>) => void;
19
+ }
20
+
21
+ const AskUserPromptContext = createContext<AskUserPromptContextValue | null>(null);
22
+
23
+ /**
24
+ * Keeps structured user prompts alive across chat navigation while allowing
25
+ * the active composer to present the current prompt.
26
+ */
27
+ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
28
+ const [queue, setQueue] = useState<AskUserPromptRequest[]>([]);
29
+ const answeredRequestIds = useRef(new Set<string>());
30
+
31
+ useEffect(() => {
32
+ const bridge = getElectronBridge();
33
+ if (!bridge?.askUser) return;
34
+ let active = true;
35
+ const addRequests = (
36
+ requests: AskUserPromptRequest[],
37
+ placement: 'append' | 'prepend' = 'append',
38
+ ) => {
39
+ setQueue((previous) => {
40
+ const known = new Set(previous.map(({ requestId }) => requestId));
41
+ const additions = requests.filter(
42
+ ({ requestId }) =>
43
+ !known.has(requestId) && !answeredRequestIds.current.has(requestId),
44
+ );
45
+ if (additions.length === 0) return previous;
46
+ return placement === 'prepend'
47
+ ? [...additions, ...previous]
48
+ : [...previous, ...additions];
49
+ });
50
+ };
51
+ const unsubscribe = bridge.askUser.onRequest((request) => {
52
+ addRequests([request]);
53
+ });
54
+ void bridge.askUser
55
+ .pending()
56
+ .then((requests) => {
57
+ if (active) addRequests(requests, 'prepend');
58
+ })
59
+ .catch((error) => {
60
+ console.error('恢复待回答问题失败:', error);
61
+ });
62
+ return () => {
63
+ active = false;
64
+ unsubscribe();
65
+ };
66
+ }, []);
67
+
68
+ const current = queue[0] ?? null;
69
+ const answer = useCallback(
70
+ (answers: Record<string, AnswerValue>) => {
71
+ if (!current) return;
72
+ const bridge = getElectronBridge();
73
+ answeredRequestIds.current.add(current.requestId);
74
+ setQueue((previous) => previous.slice(1));
75
+ void bridge?.askUser?.answer({ requestId: current.requestId, answers });
76
+ },
77
+ [current],
78
+ );
79
+
80
+ const value = useMemo(
81
+ () => ({
82
+ current,
83
+ pendingCount: Math.max(0, queue.length - 1),
84
+ answer,
85
+ }),
86
+ [answer, current, queue.length],
87
+ );
88
+
89
+ return (
90
+ <AskUserPromptContext.Provider value={value}>
91
+ {children}
92
+ </AskUserPromptContext.Provider>
93
+ );
94
+ }
95
+
96
+ /** Returns the structured prompt currently assigned to the chat composer. */
97
+ export function useAskUserPrompt(): AskUserPromptContextValue | null {
98
+ return useContext(AskUserPromptContext);
99
+ }
@@ -11,6 +11,8 @@ import { getFriendlyDate } from './timestamp';
11
11
  import { useCopy } from './useCopy';
12
12
  import type { TurnBlock } from './turn-timeline';
13
13
  import { TurnProcessGroup } from './TurnProcessGroup';
14
+ import { TurnFilesCard } from './TurnFilesCard';
15
+ import type { TurnFile } from './turn-files';
14
16
 
15
17
  /**
16
18
  * AssistantMessage — Tier-1 port of `deeppath`'s assistant-bubble.
@@ -55,6 +57,11 @@ interface AssistantMessageProps {
55
57
  */
56
58
  agents: LocalChatAgent[];
57
59
  chats?: LocalChat[];
60
+ /**
61
+ * 当前对话。正文行内代码里的路径按它绑定的项目根解析相对路径后,
62
+ * 确认存在的会变成可点击(见 FilePathCode)。
63
+ */
64
+ chatId?: string | null;
58
65
  /** Active chat-level agent — used as a fallback during streaming. */
59
66
  currentAgent: LocalChatAgent | null;
60
67
  /**
@@ -91,6 +98,12 @@ interface AssistantMessageProps {
91
98
  startedAtMs?: number;
92
99
  /** Frozen wall-clock of a finished turn. */
93
100
  durationMs?: number;
101
+ /**
102
+ * 本回合产生/修改的文件列表(local-backend 回合收尾时收集,经
103
+ * `turn_files` SSE + messageMetadata.turnFiles 持久化)。渲染在回答
104
+ * 气泡之下,点击用系统默认应用打开。空/undefined = 不渲染。
105
+ */
106
+ turnFiles?: TurnFile[];
94
107
  /**
95
108
  * 分享当前对话(截图复制到剪贴板)。只传给最近一条助手消息,画在时间戳
96
109
  * 行上,跟复制 / 重新生成同一排。
@@ -198,6 +211,7 @@ export function AssistantMessage({
198
211
  isStreaming,
199
212
  agents,
200
213
  chats = [],
214
+ chatId = null,
201
215
  currentAgent,
202
216
  executedActions,
203
217
  timeline,
@@ -207,6 +221,7 @@ export function AssistantMessage({
207
221
  onRegenerate,
208
222
  startedAtMs,
209
223
  durationMs,
224
+ turnFiles,
210
225
  onShare,
211
226
  }: AssistantMessageProps) {
212
227
  const content = message.content || '';
@@ -263,6 +278,7 @@ export function AssistantMessage({
263
278
  durationMs={durationMs}
264
279
  agents={agents}
265
280
  chats={chats}
281
+ chatId={chatId}
266
282
  emptyFallback={
267
283
  failure ? (
268
284
  <TurnErrorBubble reason={failure.reason} />
@@ -290,7 +306,7 @@ export function AssistantMessage({
290
306
  renderAnswer={(block, isLast) => (
291
307
  <div className={bubbleClass(isPlanMode)}>
292
308
  <div className="markdown-content text-sm leading-relaxed text-agent-foreground">
293
- <Markdown agents={agents} chats={chats}>{block.content}</Markdown>
309
+ <Markdown agents={agents} chats={chats} chatId={chatId}>{block.content}</Markdown>
294
310
  </div>
295
311
  {isStreaming && isLast && <StreamingCursor />}
296
312
  </div>
@@ -312,7 +328,7 @@ export function AssistantMessage({
312
328
  {displayContent ? (
313
329
  <div className={bubbleClass(isPlanMode)}>
314
330
  <div className="markdown-content text-sm leading-relaxed text-agent-foreground">
315
- <Markdown agents={agents} chats={chats}>{displayContent}</Markdown>
331
+ <Markdown agents={agents} chats={chats} chatId={chatId}>{displayContent}</Markdown>
316
332
  </div>
317
333
  {isStreaming && <StreamingCursor />}
318
334
  </div>
@@ -338,6 +354,13 @@ export function AssistantMessage({
338
354
  ) : null}
339
355
  </>
340
356
  )}
357
+ {/* 回合产物文件列表:钉在回答之下、时间戳行之上(Codex 式收尾)。
358
+ 回合收尾才有数据,流式期间天然为空。 */}
359
+ {!isStreaming && turnFiles && turnFiles.length > 0 && (
360
+ <div className="mt-1.5">
361
+ <TurnFilesCard files={turnFiles} />
362
+ </div>
363
+ )}
341
364
  <div className="mt-1 flex items-center gap-2 text-[11px] text-agent-muted-foreground">
342
365
  <span>
343
366
  {message.createdAt