@steerable/agent-shell-web 0.6.27 → 0.6.29

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 (53) hide show
  1. package/package.json +4 -4
  2. package/src/components/AgentSidebar.tsx +27 -27
  3. package/src/components/BrandLockup.tsx +20 -4
  4. package/src/components/ChatHeader.tsx +5 -15
  5. package/src/components/LocalLlmSettingsModal.tsx +5 -5
  6. package/src/components/TerminalPanel.tsx +1 -1
  7. package/src/components/chat/ApprovalModal.tsx +5 -5
  8. package/src/components/chat/AskUserQuestionMenu.tsx +13 -13
  9. package/src/components/chat/AssistantMessage.tsx +122 -36
  10. package/src/components/chat/ChatInput.tsx +34 -26
  11. package/src/components/chat/ExecPolicyPicker.tsx +4 -4
  12. package/src/components/chat/ExecutedActionsCard.tsx +398 -29
  13. package/src/components/chat/FilePathCode.tsx +2 -1
  14. package/src/components/chat/LocalChatPanel.tsx +42 -11
  15. package/src/components/chat/Markdown.tsx +15 -14
  16. package/src/components/chat/MessageList.tsx +23 -5
  17. package/src/components/chat/ModelPicker.tsx +6 -6
  18. package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
  19. package/src/components/chat/SessionTodoList.tsx +234 -0
  20. package/src/components/chat/SessionTreeModal.tsx +3 -3
  21. package/src/components/chat/SuggestedReplies.tsx +14 -5
  22. package/src/components/chat/TaskPanelModal.tsx +3 -3
  23. package/src/components/chat/TaskProcessPanel.tsx +67 -21
  24. package/src/components/chat/TurnFilesCard.tsx +2 -2
  25. package/src/components/chat/TurnProcessGroup.tsx +252 -99
  26. package/src/components/chat/UserMessage.tsx +7 -7
  27. package/src/components/chat/executed-actions-model.ts +469 -0
  28. package/src/components/chat/next-steps-tags.ts +28 -0
  29. package/src/components/chat/orchestration-children-model.ts +120 -0
  30. package/src/components/chat/process-status.ts +125 -53
  31. package/src/components/chat/todo-list-model.ts +167 -0
  32. package/src/components/chat/turn-timeline.ts +123 -11
  33. package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
  34. package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
  35. package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
  36. package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
  37. package/src/components/settings/LlmSettingsPanel.tsx +10 -10
  38. package/src/components/settings/McpSettingsPanel.tsx +3 -3
  39. package/src/components/settings/ModelIdCombobox.tsx +2 -2
  40. package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
  41. package/src/components/settings/SettingsSaveButton.tsx +1 -1
  42. package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
  43. package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
  44. package/src/components/settings/UsagePanel.tsx +14 -10
  45. package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
  46. package/src/layouts/AgentLayout.tsx +3 -3
  47. package/src/lib/chat-transport.ts +68 -12
  48. package/src/lib/local-api.ts +11 -0
  49. package/src/lib/show-thinking-content.ts +4 -4
  50. package/src/pages/AgentPage.tsx +82 -11
  51. package/src/pages/ChatPanelPreviewPage.tsx +26 -4
  52. package/src/pages/SettingsPage.tsx +39 -39
  53. package/src/styles/index.css +12 -2
@@ -5,7 +5,9 @@ import type { ChatMessage } from '@steerable/agent-protocol';
5
5
  import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
6
6
  import { Markdown } from './Markdown';
7
7
  import { ExecutedActionsCard, type ExecutedAction } from './ExecutedActionsCard';
8
- import { OrchestrationChildrenCard, type ChildInfo } from './OrchestrationChildrenCard';
8
+ import type { ChildInfo } from './OrchestrationChildrenCard';
9
+ import { collectTurnParticipants, extractMentionedAgentIds } from './executed-actions-model';
10
+ import type { InspectTaskInput } from './executed-actions-model';
9
11
  import { StreamingStatus } from './StreamingStatus';
10
12
  import { getFriendlyDate } from './timestamp';
11
13
  import { useCopy } from './useCopy';
@@ -13,6 +15,12 @@ import type { TurnBlock } from './turn-timeline';
13
15
  import { TurnProcessGroup } from './TurnProcessGroup';
14
16
  import { TurnFilesCard } from './TurnFilesCard';
15
17
  import type { TurnFile } from './turn-files';
18
+ import { stripNextStepsTags } from './next-steps-tags';
19
+ import {
20
+ formatTokenSpeed,
21
+ llmRequestElapsedMs,
22
+ type LlmSpeedSnapshot,
23
+ } from './process-status';
16
24
 
17
25
  /**
18
26
  * AssistantMessage — Tier-1 port of `deeppath`'s assistant-bubble.
@@ -77,8 +85,15 @@ interface AssistantMessageProps {
77
85
  * `content` keep the legacy stacked layout.
78
86
  */
79
87
  timeline?: TurnBlock[];
80
- /** P3.1: live child-agent list for this turn (orchestration card). */
88
+ /**
89
+ * P3.1 子代理生命周期。看板卡片已不再渲染——每个 `委派 · X` 工具行从
90
+ * 「执行中」到「已完成」本身就是同一份进度,还能展开看任务书与回报。
91
+ * 这份列表现在喂两处:顶栏参与者徽章(没有 `@提及` 时的回落来源),
92
+ * 以及委派行跳转右侧过程栏所需的子代理 record。
93
+ */
81
94
  orchestrationChildren?: ChildInfo[];
95
+ /** 上一轮用户消息:顶栏按里面的 `@提及` 画参与的智能体。 */
96
+ previousUser?: Pick<ChatMessage, 'content'> & { messageMetadata?: string | null };
82
97
  /**
83
98
  * Current round number for the in-flight turn (1-based). Only meaningful
84
99
  * when `isStreaming === true` and this is the tail message — used by
@@ -98,6 +113,12 @@ interface AssistantMessageProps {
98
113
  startedAtMs?: number;
99
114
  /** Frozen wall-clock of a finished turn. */
100
115
  durationMs?: number;
116
+ /**
117
+ * Model-request generation speed (reasoning + reply + any other LLM
118
+ * output). Elapsed excludes tool waits. Live snapshots keep ticking
119
+ * while `live` is true.
120
+ */
121
+ llmSpeed?: LlmSpeedSnapshot;
101
122
  /**
102
123
  * 本回合产生/修改的文件列表(local-backend 回合收尾时收集,经
103
124
  * `turn_files` SSE + messageMetadata.turnFiles 持久化)。渲染在回答
@@ -109,37 +130,42 @@ interface AssistantMessageProps {
109
130
  * 行上,跟复制 / 重新生成同一排。
110
131
  */
111
132
  onShare?: () => Promise<boolean>;
133
+ onInspectTask?: (task: InspectTaskInput) => void;
112
134
  }
113
135
 
114
- function agentInitial(agent: LocalChatAgent | null): string {
115
- if (!agent?.name) return 'A';
116
- return agent.name.trim()[0]?.toUpperCase() ?? 'A';
136
+ function agentInitial(name: string): string {
137
+ return name.trim()[0]?.toUpperCase() ?? 'A';
117
138
  }
118
139
 
119
140
  function AgentBadge({
120
- agent,
141
+ name,
142
+ color,
143
+ title,
121
144
  isStreaming,
122
145
  }: {
123
- agent: LocalChatAgent;
124
- isStreaming: boolean;
146
+ name: string;
147
+ color: string;
148
+ title?: string;
149
+ isStreaming?: boolean;
125
150
  }) {
126
151
  return (
127
152
  <div
128
153
  className="inline-flex items-center gap-1.5 text-[11px] leading-none text-agent-muted-foreground"
129
- title={agent.description || agent.name}
154
+ title={title || name}
130
155
  >
131
156
  <span
157
+ data-agent-color-dot=""
132
158
  className="inline-flex items-center justify-center rounded-full text-[9px] font-semibold text-white"
133
159
  style={{
134
160
  width: 14,
135
161
  height: 14,
136
- backgroundColor: agent.color || '#7c3aed',
162
+ backgroundColor: color,
137
163
  }}
138
164
  >
139
- {agentInitial(agent)}
165
+ {agentInitial(name)}
140
166
  </span>
141
167
  <span className="max-w-[160px] truncate rounded bg-agent-canvas/90 px-1">
142
- {agent.name}
168
+ {name}
143
169
  </span>
144
170
  {isStreaming && (
145
171
  <span className="ml-1 inline-flex h-1.5 w-1.5 animate-pulse rounded-full bg-agent-foreground/30" />
@@ -152,11 +178,7 @@ const META_ACTION =
152
178
  'inline-flex items-center gap-0.5 rounded transition-all duration-200 hover:text-agent-foreground opacity-0 focus:opacity-100 group-hover/message:opacity-100';
153
179
 
154
180
  function bubbleClass(isPlanMode: boolean): string {
155
- return `rounded-agent-lg border p-3 shadow-sm transition-all duration-200 ${
156
- isPlanMode
157
- ? 'border-amber-400/50 dark:border-amber-500/30 bg-amber-50/10 dark:bg-amber-950/5 shadow-amber-500/5'
158
- : 'border-agent-border bg-agent-canvas'
159
- }`;
181
+ return isPlanMode ? 'rounded-agent-lg bg-amber-50/10 dark:bg-amber-950/5' : '';
160
182
  }
161
183
 
162
184
  // 框架 useChatStream 收到 SSE error 事件时把最后一条助手消息的内容打成
@@ -192,9 +214,20 @@ function readTurnFailure(
192
214
  return null;
193
215
  }
194
216
 
217
+ function readPersistedAgentId(message: ChatMessage): string | undefined {
218
+ const raw = (message as { messageMetadata?: unknown }).messageMetadata;
219
+ if (typeof raw !== 'string' || !raw) return undefined;
220
+ try {
221
+ const agentId = (JSON.parse(raw) as { agentId?: unknown }).agentId;
222
+ return typeof agentId === 'string' && agentId ? agentId : undefined;
223
+ } catch {
224
+ return undefined;
225
+ }
226
+ }
227
+
195
228
  function TurnErrorBubble({ reason }: { reason: string }) {
196
229
  return (
197
- <div className="rounded-agent-lg border border-agent-destructive/40 bg-agent-destructive/5 p-3 text-sm leading-relaxed text-agent-destructive shadow-sm">
230
+ <div className="rounded-agent-lg border border-agent-destructive/40 bg-agent-destructive/5 px-2.5 py-2 text-xs leading-relaxed text-agent-destructive shadow-sm">
198
231
  请求失败:{reason}
199
232
  </div>
200
233
  );
@@ -206,6 +239,17 @@ function StreamingCursor() {
206
239
  );
207
240
  }
208
241
 
242
+ function useLiveNow(enabled: boolean): number {
243
+ const [now, setNow] = useState(() => Date.now());
244
+ useEffect(() => {
245
+ if (!enabled) return;
246
+ setNow(Date.now());
247
+ const id = window.setInterval(() => setNow(Date.now()), 1000);
248
+ return () => window.clearInterval(id);
249
+ }, [enabled]);
250
+ return now;
251
+ }
252
+
209
253
  export function AssistantMessage({
210
254
  message,
211
255
  isStreaming,
@@ -216,34 +260,56 @@ export function AssistantMessage({
216
260
  executedActions,
217
261
  timeline,
218
262
  orchestrationChildren,
263
+ previousUser,
219
264
  currentRound = 1,
220
265
  isPlanMode = false,
221
266
  onRegenerate,
222
267
  startedAtMs,
223
268
  durationMs,
269
+ llmSpeed,
224
270
  turnFiles,
225
271
  onShare,
272
+ onInspectTask,
226
273
  }: AssistantMessageProps) {
227
274
  const content = message.content || '';
228
275
  const failure = readTurnFailure(message, content);
229
276
  // live 错误文本("[stream error] ...")不当正文渲染;落库的失败回合可能
230
277
  // 留有部分正文,此时正文与错误气泡都展示。
231
278
  const displayContent = failure?.contentIsError ? '' : content;
232
- const { copied, copy } = useCopy(content);
279
+ const { copied, copy } = useCopy(stripNextStepsTags(content));
233
280
  const [regenerating, setRegenerating] = useState(false);
234
281
  const [regenerateError, setRegenerateError] = useState<string | null>(null);
235
- const persistedAgent = message.agentId
236
- ? agents.find((a) => a.id === message.agentId) ?? null
282
+ const persistedAgentId =
283
+ typeof message.agentId === 'string' && message.agentId
284
+ ? message.agentId
285
+ : readPersistedAgentId(message);
286
+ const persistedAgent = persistedAgentId
287
+ ? agents.find((a) => a.id === persistedAgentId) ?? null
237
288
  : null;
238
289
  // Stream-time fallback: only allow the chat-level agent to "claim" a turn
239
290
  // while it's actively streaming and hasn't flushed agentId to the DB yet.
240
291
  const displayAgent = persistedAgent ?? (isStreaming ? currentAgent : null);
241
292
  const useTimeline = timeline !== undefined;
242
293
  const blocks = timeline ?? [];
294
+ const toolActions = [
295
+ ...(executedActions ?? []),
296
+ ...blocks.flatMap((block) => (block.type === 'tools' ? block.actions : [])),
297
+ ];
298
+ const participants = collectTurnParticipants(displayAgent, {
299
+ agents,
300
+ children: orchestrationChildren,
301
+ actions: toolActions,
302
+ mentionedAgentIds: extractMentionedAgentIds(previousUser, agents),
303
+ });
304
+ const now = useLiveNow(Boolean(isStreaming && llmSpeed?.live));
305
+ const tokenSpeed = formatTokenSpeed(
306
+ llmSpeed?.tokens ?? 0,
307
+ llmRequestElapsedMs(llmSpeed, now),
308
+ );
243
309
 
244
310
  return (
245
311
  <motion.div
246
- className="group/message mb-2"
312
+ className="group/message"
247
313
  data-message-role="assistant"
248
314
  data-message-id={message.id}
249
315
  initial={{ opacity: 0, y: 8 }}
@@ -251,10 +317,22 @@ export function AssistantMessage({
251
317
  transition={{ duration: 0.25 }}
252
318
  >
253
319
  <div className="mx-auto w-full max-w-[var(--chat-input-box-width)] px-1">
254
- {(displayAgent || isPlanMode) && (
255
- <div className="flex items-center justify-between mb-1.5 min-h-[18px]">
256
- {displayAgent ? (
257
- <AgentBadge agent={displayAgent} isStreaming={isStreaming} />
320
+ {(participants.length > 0 || isPlanMode) && (
321
+ <div className="mb-1 flex min-h-4 items-center justify-between gap-2">
322
+ {participants.length > 0 ? (
323
+ <div
324
+ data-testid="turn-agent-badges"
325
+ className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1"
326
+ >
327
+ {participants.map((agent, index) => (
328
+ <AgentBadge
329
+ key={agent.key}
330
+ name={agent.name}
331
+ color={agent.color}
332
+ isStreaming={isStreaming && index === 0}
333
+ />
334
+ ))}
335
+ </div>
258
336
  ) : (
259
337
  <div />
260
338
  )}
@@ -266,9 +344,6 @@ export function AssistantMessage({
266
344
  )}
267
345
  </div>
268
346
  )}
269
- {orchestrationChildren && orchestrationChildren.length > 0 && (
270
- <OrchestrationChildrenCard children={orchestrationChildren} />
271
- )}
272
347
  {useTimeline ? (
273
348
  <>
274
349
  <TurnProcessGroup
@@ -279,6 +354,8 @@ export function AssistantMessage({
279
354
  agents={agents}
280
355
  chats={chats}
281
356
  chatId={chatId}
357
+ onInspectTask={onInspectTask}
358
+ orchestrationChildren={orchestrationChildren}
282
359
  emptyFallback={
283
360
  failure ? (
284
361
  <TurnErrorBubble reason={failure.reason} />
@@ -291,7 +368,7 @@ export function AssistantMessage({
291
368
  />
292
369
  </div>
293
370
  ) : (
294
- <div className={`${bubbleClass(isPlanMode)} text-sm italic text-agent-muted-foreground`}>
371
+ <div className={`${bubbleClass(isPlanMode)} text-xs italic text-agent-muted-foreground`}>
295
372
  (空消息)
296
373
  </div>
297
374
  )
@@ -305,7 +382,7 @@ export function AssistantMessage({
305
382
  }
306
383
  renderAnswer={(block, isLast) => (
307
384
  <div className={bubbleClass(isPlanMode)}>
308
- <div className="markdown-content text-sm leading-relaxed text-agent-foreground">
385
+ <div className="markdown-content text-xs leading-relaxed text-agent-foreground">
309
386
  <Markdown agents={agents} chats={chats} chatId={chatId}>{block.content}</Markdown>
310
387
  </div>
311
388
  {isStreaming && isLast && <StreamingCursor />}
@@ -323,11 +400,17 @@ export function AssistantMessage({
323
400
  ) : (
324
401
  <>
325
402
  {executedActions && executedActions.length > 0 && (
326
- <ExecutedActionsCard actions={executedActions} />
403
+ <ExecutedActionsCard
404
+ actions={executedActions}
405
+ agents={agents}
406
+ chatId={chatId}
407
+ onInspectTask={onInspectTask}
408
+ orchestrationChildren={orchestrationChildren}
409
+ />
327
410
  )}
328
411
  {displayContent ? (
329
412
  <div className={bubbleClass(isPlanMode)}>
330
- <div className="markdown-content text-sm leading-relaxed text-agent-foreground">
413
+ <div className="markdown-content text-xs leading-relaxed text-agent-foreground">
331
414
  <Markdown agents={agents} chats={chats} chatId={chatId}>{displayContent}</Markdown>
332
415
  </div>
333
416
  {isStreaming && <StreamingCursor />}
@@ -343,7 +426,7 @@ export function AssistantMessage({
343
426
  />
344
427
  </div>
345
428
  ) : (
346
- <div className={`${bubbleClass(isPlanMode)} text-sm italic text-agent-muted-foreground`}>
429
+ <div className={`${bubbleClass(isPlanMode)} text-xs italic text-agent-muted-foreground`}>
347
430
  (空消息)
348
431
  </div>
349
432
  )}
@@ -357,16 +440,19 @@ export function AssistantMessage({
357
440
  {/* 回合产物文件列表:钉在回答之下、时间戳行之上(Codex 式收尾)。
358
441
  回合收尾才有数据,流式期间天然为空。 */}
359
442
  {!isStreaming && turnFiles && turnFiles.length > 0 && (
360
- <div className="mt-1.5">
443
+ <div className="mt-2.5 mb-1">
361
444
  <TurnFilesCard files={turnFiles} />
362
445
  </div>
363
446
  )}
364
- <div className="mt-1 flex items-center gap-2 text-[11px] text-agent-muted-foreground">
447
+ <div className="mt-2 flex items-center gap-2 text-[11px] text-agent-muted-foreground/80">
365
448
  <span>
366
449
  {message.createdAt
367
450
  ? getFriendlyDate(new Date(message.createdAt))
368
451
  : ''}
369
452
  </span>
453
+ {tokenSpeed ? (
454
+ <span data-testid="turn-token-speed">{tokenSpeed}</span>
455
+ ) : null}
370
456
  {content && !isStreaming && (
371
457
  <button
372
458
  type="button"
@@ -81,7 +81,7 @@ export type { ExecPolicy, McpToolItem, SkillItem };
81
81
  * - Compact-mode responsive layout.
82
82
  */
83
83
 
84
- const MIN_HEIGHT_PX = 44;
84
+ const MIN_HEIGHT_PX = 36;
85
85
  const MAX_HEIGHT_PX = 220;
86
86
  const MENTION_PATTERN = /(@[^\s@]+)/g;
87
87
  const MAX_MENTION_SUGGESTIONS = 8;
@@ -141,6 +141,8 @@ export interface ChatInputProps {
141
141
  /** Slot in the composer meta row above the input box, before the agent
142
142
  * picker (e.g. the project badge). */
143
143
  leadingChrome?: ReactNode;
144
+ /** Slot in the composer meta row above the input box, right-aligned (e.g. SessionTodoList). */
145
+ trailingChrome?: ReactNode;
144
146
  /** Current chat mode. When unset the toggle is hidden (defaults to 'agent'). */
145
147
  mode?: ChatMode;
146
148
  /** Called when the user switches between Agent / Plan mode. */
@@ -696,6 +698,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
696
698
  onOpenSettings,
697
699
  toolbarExtras,
698
700
  leadingChrome,
701
+ trailingChrome,
699
702
  mode,
700
703
  onModeChange,
701
704
  execPolicy,
@@ -1512,7 +1515,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1512
1515
 
1513
1516
  if (approvalPrompt?.current) {
1514
1517
  return (
1515
- <div className="chat-input-container @container relative px-3 pb-3 pt-1">
1518
+ <div className="chat-input-container @container relative px-2.5 pb-2 pt-0.5">
1516
1519
  <ApprovalPromptMenu
1517
1520
  key={approvalPrompt.current.requestId}
1518
1521
  request={approvalPrompt.current}
@@ -1527,7 +1530,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1527
1530
  const request = askUserPrompt.current;
1528
1531
  return (
1529
1532
  <div
1530
- className="chat-input-container @container relative px-3 pb-3 pt-1"
1533
+ className="chat-input-container @container relative px-2.5 pb-2 pt-0.5"
1531
1534
  data-testid="ask-user-composer"
1532
1535
  >
1533
1536
  <AskUserQuestionMenu
@@ -1549,10 +1552,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1549
1552
  return (
1550
1553
  // @container:底栏元素的显隐/宽度用容器查询(@sm = 384px)而不是
1551
1554
  // 视口断点——侧边栏占宽后,视口够宽但输入框本身可能已经很窄。
1552
- <div className="chat-input-container @container relative px-3 pb-3 pt-1">
1553
- {(leadingChrome || selectedAgent) && (
1555
+ <div className="chat-input-container @container relative px-2.5 pb-2.5 pt-1">
1556
+ {(leadingChrome || selectedAgent || trailingChrome) && (
1554
1557
  <div
1555
- className="relative mb-1 flex min-w-0 items-center gap-1.5"
1558
+ className="relative mb-1.5 flex min-w-0 items-center gap-1.5"
1556
1559
  data-testid="composer-meta-row"
1557
1560
  >
1558
1561
  {leadingChrome}
@@ -1568,6 +1571,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1568
1571
  mentionReferences={mentionReferences}
1569
1572
  />
1570
1573
  )}
1574
+ {trailingChrome && (
1575
+ <div className="ml-auto flex items-center min-w-0">
1576
+ {trailingChrome}
1577
+ </div>
1578
+ )}
1571
1579
  </div>
1572
1580
  )}
1573
1581
  {mentionQuery && mentionSuggestions.length > 0 && (
@@ -1712,7 +1720,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1712
1720
  onDrop={handleDrop}
1713
1721
  >
1714
1722
  {pendingFollowUps.length > 0 && (
1715
- <div className="flex flex-col gap-1 border-b border-agent-border bg-agent-muted/30 px-3 py-2">
1723
+ <div className="flex flex-col gap-1 border-b border-agent-border bg-agent-muted/30 px-2.5 py-1.5">
1716
1724
  <div className="text-[11px] font-medium text-agent-muted-foreground">
1717
1725
  排队中({pendingFollowUps.length})· 本轮结束后自动发出 · 停止将丢弃
1718
1726
  </div>
@@ -1739,14 +1747,14 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1739
1747
  {steerNotice && (
1740
1748
  <div
1741
1749
  role="status"
1742
- className="border-b border-agent-border bg-agent-muted/20 px-3 py-1.5 text-[11px] text-agent-muted-foreground"
1750
+ className="border-b border-agent-border bg-agent-muted/20 px-2.5 py-1 text-[11px] text-agent-muted-foreground"
1743
1751
  >
1744
1752
  {steerNotice}
1745
1753
  </div>
1746
1754
  )}
1747
1755
  <div className="chat-input-surface relative overflow-hidden">
1748
1756
  <div
1749
- className="chat-input-placeholder pointer-events-none absolute inset-x-0 top-0 px-3 pt-3 text-sm text-agent-muted-foreground"
1757
+ className="chat-input-placeholder pointer-events-none absolute inset-x-0 top-0 px-2.5 pt-2 text-xs text-agent-muted-foreground"
1750
1758
  style={{ visibility: value ? 'hidden' : undefined }}
1751
1759
  >
1752
1760
  {placeholder}
@@ -1769,7 +1777,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1769
1777
  onCompositionStart={handleCompositionStart}
1770
1778
  onCompositionEnd={handleCompositionEnd}
1771
1779
  onScroll={handleEditorScroll}
1772
- className="chat-input-editor relative z-10 block w-full overflow-y-auto whitespace-pre-wrap break-words border-0 bg-transparent px-3 pt-3 text-sm text-agent-foreground caret-agent-foreground outline-none selection:bg-agent-foreground/20"
1780
+ className="chat-input-editor relative z-10 block w-full overflow-y-auto whitespace-pre-wrap break-words border-0 bg-transparent px-2.5 pt-2 text-xs text-agent-foreground caret-agent-foreground outline-none selection:bg-agent-foreground/20"
1773
1781
  style={{
1774
1782
  minHeight: MIN_HEIGHT_PX,
1775
1783
  maxHeight: MAX_HEIGHT_PX,
@@ -1777,7 +1785,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1777
1785
  />
1778
1786
  </div>
1779
1787
  {actualFiles.length > 0 && (
1780
- <div className="flex flex-wrap gap-1.5 px-3 pb-2 pt-0.5">
1788
+ <div className="flex flex-wrap gap-1.5 px-2.5 pb-1.5 pt-0.5">
1781
1789
  {actualFiles.map((file, idx) => (
1782
1790
  <div
1783
1791
  key={idx}
@@ -1802,7 +1810,7 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1802
1810
  )}
1803
1811
  <div
1804
1812
  ref={toolbarRowRef}
1805
- className="flex items-center justify-between gap-2 px-2 pb-2 pt-1"
1813
+ className="flex items-center justify-between gap-1.5 px-2 py-1 text-[12px] leading-[1.45]"
1806
1814
  >
1807
1815
  {/* Left toolbar: mode + exec sandbox + host extras + settings.
1808
1816
  Agent picker lives in the meta row above the box, next to
@@ -1827,11 +1835,11 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1827
1835
  type="button"
1828
1836
  onClick={handlePickFiles}
1829
1837
  disabled={disabled || isStreaming}
1830
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:cursor-not-allowed disabled:opacity-50"
1838
+ className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground disabled:cursor-not-allowed disabled:opacity-50"
1831
1839
  title="上传文件(可多选)"
1832
1840
  aria-label="上传文件"
1833
1841
  >
1834
- <LuPaperclip className="h-3.5 w-3.5" />
1842
+ <LuPaperclip className="h-3 w-3" />
1835
1843
  </button>
1836
1844
  {/* 隐藏的原生多选文件输入:点击回形针按钮触发系统选择框。 */}
1837
1845
  <input
@@ -1847,12 +1855,12 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1847
1855
  <button
1848
1856
  type="button"
1849
1857
  onClick={onOpenSettings}
1850
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
1858
+ className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
1851
1859
  title="LLM 设置"
1852
1860
  aria-label="LLM 设置"
1853
1861
  data-testid="chat-llm-settings"
1854
1862
  >
1855
- <LuSettings className="h-3.5 w-3.5" />
1863
+ <LuSettings className="h-3 w-3" />
1856
1864
  </button>
1857
1865
  )}
1858
1866
  </div>
@@ -1921,10 +1929,10 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1921
1929
  disabled={!isStreaming && !canSend}
1922
1930
  className={
1923
1931
  isStreaming
1924
- ? 'flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-agent-destructive text-white transition hover:opacity-90'
1932
+ ? 'flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-agent-destructive text-white transition hover:opacity-90'
1925
1933
  : canSend
1926
- ? 'flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-agent-foreground text-agent-canvas transition hover:opacity-90'
1927
- : 'flex h-8 w-8 shrink-0 cursor-not-allowed items-center justify-center rounded-full bg-agent-muted text-agent-muted-foreground'
1934
+ ? 'flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-agent-foreground text-agent-canvas transition hover:opacity-90'
1935
+ : 'flex h-6 w-6 shrink-0 cursor-not-allowed items-center justify-center rounded-full bg-agent-muted text-agent-muted-foreground'
1928
1936
  }
1929
1937
  title={
1930
1938
  isStreaming
@@ -1937,9 +1945,9 @@ export const ChatInput = forwardRef<ChatInputHandle, ChatInputProps>(
1937
1945
  data-testid="chat-send"
1938
1946
  >
1939
1947
  {isStreaming ? (
1940
- <LuSquare className="h-3.5 w-3.5 fill-current" />
1948
+ <LuSquare className="h-3 w-3 fill-current" />
1941
1949
  ) : (
1942
- <LuArrowUp className="h-4 w-4" />
1950
+ <LuArrowUp className="h-3.5 w-3.5" />
1943
1951
  )}
1944
1952
  </button>
1945
1953
  </div>
@@ -1971,7 +1979,7 @@ function ModeToggle({
1971
1979
  // shrink-0:分段控件被 flex 压缩会压扁文字;极窄容器(<@sm)退化为
1972
1980
  // 纯图标(title 兜底语义),把收缩量让给可截断的选择器。
1973
1981
  <div
1974
- className="inline-flex h-7 shrink-0 items-center rounded-full border border-agent-border bg-agent-canvas p-0.5"
1982
+ className="inline-flex h-6 shrink-0 items-center rounded-full border border-agent-border bg-agent-canvas p-0.5"
1975
1983
  role="radiogroup"
1976
1984
  aria-label="对话模式"
1977
1985
  data-testid="mode-toggle"
@@ -1985,13 +1993,13 @@ function ModeToggle({
1985
1993
  title="Agent 模式:直接执行任务"
1986
1994
  data-testid="mode-agent"
1987
1995
  className={[
1988
- 'inline-flex h-6 items-center gap-1 rounded-full px-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-70',
1996
+ 'inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
1989
1997
  mode === 'agent'
1990
1998
  ? 'bg-agent-foreground/10 text-agent-foreground'
1991
1999
  : 'text-agent-muted-foreground hover:text-agent-foreground',
1992
2000
  ].join(' ')}
1993
2001
  >
1994
- <LuInfinity className="h-3.5 w-3.5" />
2002
+ <LuInfinity className="h-3 w-3" />
1995
2003
  <span className="@max-sm:hidden">Agent</span>
1996
2004
  </button>
1997
2005
  <button
@@ -2003,13 +2011,13 @@ function ModeToggle({
2003
2011
  title="Plan 模式:先制定计划,只读不执行"
2004
2012
  data-testid="mode-plan"
2005
2013
  className={[
2006
- 'inline-flex h-6 items-center gap-1 rounded-full px-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-70',
2014
+ 'inline-flex h-5 items-center gap-1 rounded-full px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
2007
2015
  mode === 'plan'
2008
2016
  ? 'bg-amber-400/20 text-amber-600 dark:text-amber-400'
2009
2017
  : 'text-agent-muted-foreground hover:text-agent-foreground',
2010
2018
  ].join(' ')}
2011
2019
  >
2012
- <LuListChecks className="h-3.5 w-3.5" />
2020
+ <LuListChecks className="h-3 w-3" />
2013
2021
  <span className="@max-sm:hidden">Plan</span>
2014
2022
  </button>
2015
2023
  </div>
@@ -64,16 +64,16 @@ export function ExecPolicyPicker({
64
64
  title={current.description}
65
65
  onClick={() => setOpen((next) => !next)}
66
66
  className={[
67
- 'inline-flex h-7 max-w-[140px] items-center gap-1 rounded-full border px-2 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-70',
67
+ 'inline-flex h-6 max-w-[140px] items-center gap-1 rounded-full border px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
68
68
  isFull
69
69
  ? 'border-amber-400/50 bg-amber-400/10 text-amber-700 dark:text-amber-400'
70
70
  : 'border-agent-border bg-agent-canvas text-agent-foreground hover:bg-agent-foreground/5',
71
71
  ].join(' ')}
72
72
  >
73
73
  {isFull ? (
74
- <LuLockOpen className="h-3.5 w-3.5 shrink-0" />
74
+ <LuLockOpen className="h-3 w-3 shrink-0" />
75
75
  ) : (
76
- <LuFolderLock className="h-3.5 w-3.5 shrink-0" />
76
+ <LuFolderLock className="h-3 w-3 shrink-0" />
77
77
  )}
78
78
  <span className="truncate">{current.label}</span>
79
79
  <LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
@@ -104,7 +104,7 @@ export function ExecPolicyPicker({
104
104
  : 'hover:bg-agent-foreground/5',
105
105
  ].join(' ')}
106
106
  >
107
- <span className="text-xs font-medium text-agent-foreground">
107
+ <span className="text-[12px] font-medium leading-[1.45] text-agent-foreground">
108
108
  {option.label}
109
109
  </span>
110
110
  <span className="text-[11px] leading-snug text-agent-muted-foreground">