@steerable/agent-shell-web 0.6.27 → 0.6.28

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 (49) hide show
  1. package/package.json +4 -4
  2. package/src/components/AgentSidebar.tsx +24 -24
  3. package/src/components/BrandLockup.tsx +1 -1
  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 +41 -13
  10. package/src/components/chat/ChatInput.tsx +24 -24
  11. package/src/components/chat/ExecPolicyPicker.tsx +4 -4
  12. package/src/components/chat/ExecutedActionsCard.tsx +10 -3
  13. package/src/components/chat/FilePathCode.tsx +2 -1
  14. package/src/components/chat/LocalChatPanel.tsx +39 -4
  15. package/src/components/chat/Markdown.tsx +15 -14
  16. package/src/components/chat/MessageList.tsx +16 -3
  17. package/src/components/chat/ModelPicker.tsx +6 -6
  18. package/src/components/chat/SessionTodoList.tsx +137 -0
  19. package/src/components/chat/SessionTreeModal.tsx +3 -3
  20. package/src/components/chat/SuggestedReplies.tsx +5 -4
  21. package/src/components/chat/TaskPanelModal.tsx +3 -3
  22. package/src/components/chat/TaskProcessPanel.tsx +5 -4
  23. package/src/components/chat/TurnFilesCard.tsx +2 -2
  24. package/src/components/chat/TurnProcessGroup.tsx +226 -99
  25. package/src/components/chat/UserMessage.tsx +4 -4
  26. package/src/components/chat/next-steps-tags.ts +28 -0
  27. package/src/components/chat/process-status.ts +125 -53
  28. package/src/components/chat/todo-list-model.ts +167 -0
  29. package/src/components/chat/turn-timeline.ts +100 -11
  30. package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
  31. package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
  32. package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
  33. package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
  34. package/src/components/settings/LlmSettingsPanel.tsx +10 -10
  35. package/src/components/settings/McpSettingsPanel.tsx +3 -3
  36. package/src/components/settings/ModelIdCombobox.tsx +2 -2
  37. package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
  38. package/src/components/settings/SettingsSaveButton.tsx +1 -1
  39. package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
  40. package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
  41. package/src/components/settings/UsagePanel.tsx +14 -10
  42. package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
  43. package/src/layouts/AgentLayout.tsx +3 -3
  44. package/src/lib/chat-transport.ts +68 -12
  45. package/src/lib/show-thinking-content.ts +4 -4
  46. package/src/pages/AgentPage.tsx +56 -6
  47. package/src/pages/ChatPanelPreviewPage.tsx +26 -4
  48. package/src/pages/SettingsPage.tsx +39 -39
  49. package/src/styles/index.css +12 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.27",
3
+ "version": "0.6.28",
4
4
  "type": "module",
5
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",
@@ -46,8 +46,8 @@
46
46
  "react-router-dom": "^6.30.0",
47
47
  "rehype-raw": "^7.0.0",
48
48
  "remark-gfm": "^4.0.1",
49
- "@steerable/agent-protocol": "0.6.27",
50
- "@steerable/agent-ui": "0.6.27"
49
+ "@steerable/agent-protocol": "0.6.28",
50
+ "@steerable/agent-ui": "0.6.28"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@tailwindcss/vite": "^4.3.0",
@@ -60,7 +60,7 @@
60
60
  "typescript": "^5.8.3",
61
61
  "vite": "^5.4.21",
62
62
  "vitest": "^3.2.4",
63
- "@steerable/pack-sdk": "0.6.27"
63
+ "@steerable/pack-sdk": "0.6.28"
64
64
  },
65
65
  "scripts": {
66
66
  "gen": "echo agent-shell-web",
@@ -477,7 +477,7 @@ export function AgentSidebar({
477
477
  navigate(`/agent/${chat.id}`);
478
478
  }}
479
479
  className={[
480
- 'flex h-8 w-full min-w-0 items-center gap-1.5 rounded-full px-3 text-sm transition-colors duration-200',
480
+ 'flex h-7 w-full min-w-0 items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors duration-200',
481
481
  isCurrent
482
482
  ? 'bg-agent-foreground/10 font-medium text-agent-foreground'
483
483
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
@@ -620,7 +620,7 @@ export function AgentSidebar({
620
620
  return (
621
621
  <div className="flex h-full w-full flex-col border-r border-agent-border/60 bg-agent-muted/70 backdrop-blur-md">
622
622
  {/* ───── Brand + actions ───── */}
623
- <div className="flex h-12 flex-shrink-0 items-center justify-between px-3">
623
+ <div className="flex h-9 flex-shrink-0 items-center justify-between px-2.5">
624
624
  <BrandLockup />
625
625
  <div className="flex items-center gap-1">
626
626
  <button
@@ -638,12 +638,12 @@ export function AgentSidebar({
638
638
  {/* ───── 会话 ───── */}
639
639
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
640
640
  {/* 新对话 + 智能体 / Skill / MCP 设置入口 — 横版行按钮,各自打开独立设置页 */}
641
- <div className="flex-shrink-0 space-y-0.5 px-3 pb-0.5">
641
+ <div className="flex-shrink-0 space-y-0.5 px-2.5 pb-0.5">
642
642
  <button
643
643
  type="button"
644
644
  onClick={() => handleOpenNewChat()}
645
645
  className={[
646
- 'flex h-8 w-full items-center gap-2 rounded-full px-3 text-sm transition-colors',
646
+ 'flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors',
647
647
  onNewChatHome
648
648
  ? 'bg-agent-foreground/10 font-medium text-agent-foreground'
649
649
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
@@ -651,14 +651,14 @@ export function AgentSidebar({
651
651
  title="新建对话"
652
652
  data-testid="sidebar-new-chat"
653
653
  >
654
- <LuPlus className="h-4 w-4" />
654
+ <LuPlus className="h-3.5 w-3.5" />
655
655
  <span>新对话</span>
656
656
  </button>
657
657
  <button
658
658
  type="button"
659
659
  onClick={() => navigate('/settings?section=agents')}
660
660
  className={[
661
- 'flex h-8 w-full items-center gap-2 rounded-full px-3 text-sm transition-colors',
661
+ 'flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors',
662
662
  onSettingsPage && settingsSection === 'agents'
663
663
  ? 'bg-agent-foreground/10 font-medium text-agent-foreground'
664
664
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
@@ -666,14 +666,14 @@ export function AgentSidebar({
666
666
  title="智能体管理"
667
667
  data-testid="sidebar-agent-settings"
668
668
  >
669
- <LuBot className="h-4 w-4" />
669
+ <LuBot className="h-3.5 w-3.5" />
670
670
  <span>智能体管理</span>
671
671
  </button>
672
672
  <button
673
673
  type="button"
674
674
  onClick={() => navigate('/settings?section=skills')}
675
675
  className={[
676
- 'flex h-8 w-full items-center gap-2 rounded-full px-3 text-sm transition-colors',
676
+ 'flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors',
677
677
  onSettingsPage && settingsSection === 'skills'
678
678
  ? 'bg-agent-foreground/10 font-medium text-agent-foreground'
679
679
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
@@ -681,14 +681,14 @@ export function AgentSidebar({
681
681
  title="Skill 设置"
682
682
  data-testid="sidebar-skill-settings"
683
683
  >
684
- <LuBlocks className="h-4 w-4" />
684
+ <LuBlocks className="h-3.5 w-3.5" />
685
685
  <span>Skill 设置</span>
686
686
  </button>
687
687
  <button
688
688
  type="button"
689
689
  onClick={() => navigate('/settings?section=mcp')}
690
690
  className={[
691
- 'flex h-8 w-full items-center gap-2 rounded-full px-3 text-sm transition-colors',
691
+ 'flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors',
692
692
  onSettingsPage && settingsSection === 'mcp'
693
693
  ? 'bg-agent-foreground/10 font-medium text-agent-foreground'
694
694
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
@@ -696,11 +696,11 @@ export function AgentSidebar({
696
696
  title="MCP 设置"
697
697
  data-testid="sidebar-mcp-settings"
698
698
  >
699
- <LuPlug className="h-4 w-4" />
699
+ <LuPlug className="h-3.5 w-3.5" />
700
700
  <span>MCP 设置</span>
701
701
  </button>
702
702
  </div>
703
- <div className="flex flex-shrink-0 items-center justify-between py-1.5 pl-4 pr-3">
703
+ <div className="flex flex-shrink-0 items-center justify-between py-1 pl-2.5 pr-2.5">
704
704
  <button
705
705
  type="button"
706
706
  onClick={() => setChatsExpanded((v) => !v)}
@@ -747,7 +747,7 @@ export function AgentSidebar({
747
747
  加载中...
748
748
  </div>
749
749
  ) : chatGroups.length === 0 && projectGroups.length === 0 ? (
750
- <div className="flex flex-col items-center gap-1.5 py-6 text-xs text-agent-muted-foreground">
750
+ <div className="flex flex-col items-center gap-1.5 py-4 text-xs text-agent-muted-foreground">
751
751
  <LuMessageSquare className="h-4 w-4 text-agent-muted-foreground/60" />
752
752
  暂无会话
753
753
  </div>
@@ -759,7 +759,7 @@ export function AgentSidebar({
759
759
  <div className="group/proj relative">
760
760
  {renamingProjectId === project.id ? (
761
761
  <form
762
- className="flex items-center px-3 pb-1 pt-2"
762
+ className="flex items-center px-2.5 pb-1 pt-1.5"
763
763
  onSubmit={(event) => {
764
764
  event.preventDefault();
765
765
  void handleRenameProject(project.id);
@@ -781,7 +781,7 @@ export function AgentSidebar({
781
781
  <button
782
782
  type="button"
783
783
  onClick={() => toggleProjectCollapsed(project.id)}
784
- className="flex w-full min-w-0 items-center px-3 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wider text-agent-muted-foreground/80 transition-colors hover:text-agent-foreground"
784
+ className="flex w-full min-w-0 items-center px-2.5 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-agent-muted-foreground/80 transition-colors hover:text-agent-foreground"
785
785
  title={`${project.name}\n${project.folderPath}`}
786
786
  >
787
787
  <span className="mr-1">
@@ -856,7 +856,7 @@ export function AgentSidebar({
856
856
  {!collapsedProjectIds.has(project.id) && (
857
857
  <div className="space-y-0.5">
858
858
  {items.length === 0 ? (
859
- <div className="px-3 py-1 text-[11px] text-agent-muted-foreground/60">
859
+ <div className="px-2.5 py-0.5 text-[11px] text-agent-muted-foreground/60">
860
860
  暂无会话 — hover 项目名点 + 新建
861
861
  </div>
862
862
  ) : (
@@ -869,7 +869,7 @@ export function AgentSidebar({
869
869
  {/* 无项目对话排在项目分组之后:保持原有日期分组 */}
870
870
  {chatGroups.map((group) => (
871
871
  <div key={group.label} className="mb-1">
872
- <div className="px-3 pb-1 pt-2 text-[10px] font-medium uppercase tracking-wider text-agent-muted-foreground/80">
872
+ <div className="px-2.5 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-agent-muted-foreground/80">
873
873
  {group.label}
874
874
  </div>
875
875
  <div className="space-y-0.5">
@@ -899,7 +899,7 @@ export function AgentSidebar({
899
899
 
900
900
  {(error || projectError) && (
901
901
  <div
902
- className="flex-shrink-0 border-t border-agent-destructive/40 bg-agent-destructive/10 px-3 py-2 text-[11px] text-agent-destructive"
902
+ className="flex-shrink-0 border-t border-agent-destructive/40 bg-agent-destructive/10 px-2.5 py-1.5 text-[11px] text-agent-destructive"
903
903
  role="alert"
904
904
  >
905
905
  {error ?? projectError}
@@ -907,13 +907,13 @@ export function AgentSidebar({
907
907
  )}
908
908
 
909
909
  {/* ───── Footer: 右侧面板切换(终端 | 包槽位)+ 设置 ───── */}
910
- <div className="flex-shrink-0 border-t border-agent-border/40 px-3 py-2">
910
+ <div className="flex-shrink-0 border-t border-agent-border/40 px-2.5 py-1.5">
911
911
  {chatSlots.length === 0 ? (
912
912
  <button
913
913
  type="button"
914
914
  onClick={() => onToggleRightPanel('terminal')}
915
915
  className={[
916
- 'flex h-8 w-full items-center gap-2 rounded-full px-3 text-sm transition-colors',
916
+ 'flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors',
917
917
  rightPanel === 'terminal'
918
918
  ? 'bg-agent-foreground/10 font-medium text-agent-foreground'
919
919
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-foreground',
@@ -921,7 +921,7 @@ export function AgentSidebar({
921
921
  title={`${rightPanel === 'terminal' ? '关闭' : '打开'}终端面板 (${isMac ? '⌘T' : 'Ctrl+T'})`}
922
922
  data-testid="sidebar-terminal"
923
923
  >
924
- <LuTerminal className="h-4 w-4" />
924
+ <LuTerminal className="h-3.5 w-3.5" />
925
925
  <span>终端</span>
926
926
  <span className="ml-auto text-[10px] text-agent-muted-foreground/70">
927
927
  {isMac ? '⌘T' : 'Ctrl+T'}
@@ -931,7 +931,7 @@ export function AgentSidebar({
931
931
  <div
932
932
  role="group"
933
933
  aria-label="右侧面板切换"
934
- className="flex h-8 w-full items-center gap-0.5 rounded-full bg-agent-foreground/5 p-0.5"
934
+ className="flex h-7 w-full items-center gap-0.5 rounded-full bg-agent-foreground/5 p-0.5"
935
935
  >
936
936
  <button
937
937
  type="button"
@@ -972,7 +972,7 @@ export function AgentSidebar({
972
972
  type="button"
973
973
  onClick={() => navigate('/settings')}
974
974
  className={[
975
- 'mt-0.5 flex h-8 w-full items-center gap-2 rounded-full px-3 text-sm transition-colors',
975
+ 'mt-0.5 flex h-7 w-full items-center gap-1.5 rounded-full px-2.5 text-xs transition-colors',
976
976
  onGeneralSettings
977
977
  ? 'bg-agent-foreground/10 font-medium text-agent-foreground'
978
978
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
@@ -980,7 +980,7 @@ export function AgentSidebar({
980
980
  title="设置"
981
981
  data-testid="sidebar-llm-settings"
982
982
  >
983
- <LuSettings className="h-4 w-4" />
983
+ <LuSettings className="h-3.5 w-3.5" />
984
984
  <span>设置</span>
985
985
  </button>
986
986
  </div>
@@ -15,7 +15,7 @@ export function BrandLockup() {
15
15
  draggable={false}
16
16
  />
17
17
  {title ? (
18
- <span className="truncate text-sm font-semibold tracking-tight text-agent-foreground">
18
+ <span className="truncate text-xs font-semibold tracking-tight text-agent-foreground">
19
19
  {title}
20
20
  </span>
21
21
  ) : null}
@@ -127,7 +127,7 @@ export function ChatHeader({
127
127
  data-branch-menu
128
128
  >
129
129
  {!branches || branchCount === 0 ? (
130
- <div className="px-3 py-2 text-xs text-agent-muted-foreground">
130
+ <div className="px-2.5 py-1.5 text-xs text-agent-muted-foreground">
131
131
  暂无分支 — 重新生成回复后,旧版本会保留在这里。
132
132
  </div>
133
133
  ) : (
@@ -159,7 +159,7 @@ export function ChatHeader({
159
159
  setBranchMenuOpen(false);
160
160
  setTreeModalOpen(true);
161
161
  }}
162
- className="flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
162
+ className="flex w-full items-center gap-2 rounded px-2.5 py-1 text-left text-xs text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
163
163
  data-action="branch-tree"
164
164
  >
165
165
  <LuListTree className="h-3 w-3 shrink-0" />
@@ -174,19 +174,14 @@ export function ChatHeader({
174
174
  : null;
175
175
 
176
176
  return (
177
- <header className="flex w-full min-w-0 items-center gap-3 bg-agent-canvas px-3 py-2">
177
+ <header className="flex w-full min-w-0 items-center gap-2 bg-agent-canvas px-2.5 py-1.5">
178
178
  <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-0.5">
179
179
  <span
180
- className="break-words text-sm font-medium text-agent-foreground"
180
+ className="break-words text-xs font-medium text-agent-foreground"
181
181
  title={chat?.title}
182
182
  >
183
183
  {chat?.title ?? '未选择对话'}
184
184
  </span>
185
- {chat && (
186
- <span className="hidden shrink-0 font-mono text-[10px] text-agent-muted-foreground/70 md:inline">
187
- {shortenId(chat.id)}
188
- </span>
189
- )}
190
185
  </div>
191
186
  {chat && onBranchSwitched && (
192
187
  <div className="shrink-0">
@@ -367,11 +362,6 @@ function taskButtonTitle(
367
362
  : `${base} · 点击处理 worktree`;
368
363
  }
369
364
 
370
- function shortenId(id: string): string {
371
- if (id.length <= 12) return id;
372
- return `${id.slice(0, 6)}…${id.slice(-4)}`;
373
- }
374
-
375
365
  /** Pin the branch menu to the trigger, clamped inside the chat panel so
376
366
  * `overflow-hidden` ancestors cannot clip the left side of a `w-72` sheet. */
377
367
  function placeBranchMenu(button: HTMLElement): { top: number; left: number; width: number } {
@@ -403,7 +393,7 @@ function BranchRow({
403
393
  onClick={onSelect}
404
394
  aria-current={active ? 'true' : undefined}
405
395
  aria-disabled={disabled || active || undefined}
406
- className={`flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-xs transition-colors ${
396
+ className={`flex w-full items-center gap-2 rounded px-2.5 py-1 text-left text-xs transition-colors ${
407
397
  active
408
398
  ? 'cursor-default bg-agent-foreground/5 font-medium text-agent-foreground'
409
399
  : 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground'
@@ -42,8 +42,8 @@ export function LocalLlmSettingsModal({ open, onClose, onSaved }: LocalLlmSettin
42
42
  <div
43
43
  className="flex h-[560px] max-h-[90vh] w-[540px] max-w-[92vw] flex-col overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-2xl animate-fade-in"
44
44
  >
45
- <div className="flex h-12 flex-shrink-0 items-center justify-between border-b border-agent-border px-5">
46
- <h2 className="text-sm font-semibold text-agent-foreground">本地模型设置</h2>
45
+ <div className="flex h-9 flex-shrink-0 items-center justify-between border-b border-agent-border px-3">
46
+ <h2 className="text-xs font-semibold text-agent-foreground">本地模型设置</h2>
47
47
  <button
48
48
  type="button"
49
49
  onClick={onClose}
@@ -53,14 +53,14 @@ export function LocalLlmSettingsModal({ open, onClose, onSaved }: LocalLlmSettin
53
53
  <LuX className="h-4 w-4" />
54
54
  </button>
55
55
  </div>
56
- <div className="flex-1 overflow-y-auto p-5">
56
+ <div className="flex-1 overflow-y-auto p-3">
57
57
  <LlmSettingsPanel onSaved={onSaved} />
58
58
  </div>
59
- <div className="flex h-12 flex-shrink-0 items-center justify-end border-t border-agent-border px-5 bg-agent-muted/10">
59
+ <div className="flex h-9 flex-shrink-0 items-center justify-end border-t border-agent-border px-3 bg-agent-muted/10">
60
60
  <button
61
61
  type="button"
62
62
  onClick={onClose}
63
- className="h-8 rounded-full px-4 text-xs font-medium text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
63
+ className="h-7 rounded-full px-3 text-xs font-medium text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
64
64
  >
65
65
  关闭
66
66
  </button>
@@ -40,7 +40,7 @@ export function TerminalPanel({
40
40
  }
41
41
  if (!hasElectron) {
42
42
  return (
43
- <div className="flex h-full w-full items-center justify-center bg-black p-6 text-center font-mono text-sm text-white">
43
+ <div className="flex h-full w-full items-center justify-center bg-black p-4 text-center font-mono text-xs text-white">
44
44
  <div>
45
45
  <p className="mb-2">⚠️ 此面板需要桌面客户端或 BS server 连接。</p>
46
46
  <p className="text-white/60">
@@ -106,17 +106,17 @@ export function ApprovalPromptMenu({
106
106
 
107
107
  return (
108
108
  <div
109
- className="overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas text-sm text-agent-foreground shadow-xl"
109
+ className="overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas text-xs text-agent-foreground shadow-xl"
110
110
  data-testid="approval-composer"
111
111
  >
112
- <div className="flex items-center gap-2 border-b border-agent-border px-4 py-3">
112
+ <div className="flex items-center gap-2 border-b border-agent-border px-3 py-2">
113
113
  {destructive ? (
114
114
  <LuTriangleAlert className="h-4 w-4 shrink-0 text-agent-destructive" />
115
115
  ) : (
116
116
  <LuShieldAlert className="h-4 w-4 shrink-0 text-amber-500" />
117
117
  )}
118
118
  <div className="min-w-0 flex-1">
119
- <div className="text-sm font-medium text-agent-foreground">
119
+ <div className="text-xs font-medium text-agent-foreground">
120
120
  {isEgress ? (
121
121
  <>
122
122
  Agent 请求访问外网
@@ -143,7 +143,7 @@ export function ApprovalPromptMenu({
143
143
  </div>
144
144
  </div>
145
145
 
146
- <div className="max-h-48 overflow-auto px-4 py-3">
146
+ <div className="max-h-48 overflow-auto px-3 py-2">
147
147
  <pre className="whitespace-pre-wrap break-all rounded-agent-md bg-agent-muted/50 px-3 py-2 font-mono text-xs text-agent-foreground">
148
148
  {summarizeArguments(current.arguments)}
149
149
  </pre>
@@ -169,7 +169,7 @@ export function ApprovalPromptMenu({
169
169
  )}
170
170
  </div>
171
171
 
172
- <div className="space-y-2 border-t border-agent-border px-4 py-3">
172
+ <div className="space-y-2 border-t border-agent-border px-3 py-2">
173
173
  <DecisionRow
174
174
  buttons={
175
175
  isEgress
@@ -120,7 +120,7 @@ function Footer({
120
120
  children?: React.ReactNode;
121
121
  }) {
122
122
  return (
123
- <div className="mt-4 flex items-center justify-between gap-3 border-t border-agent-border px-4 py-3">
123
+ <div className="mt-3 flex items-center justify-between gap-2 border-t border-agent-border px-3 py-2">
124
124
  <div className="flex min-w-0 items-center gap-2">
125
125
  {canGoBack && onBack && (
126
126
  <button
@@ -279,7 +279,7 @@ function SelectQuestionStep({
279
279
  role={question.multiSelect ? 'checkbox' : 'radio'}
280
280
  aria-checked={active}
281
281
  className={[
282
- 'flex w-full items-center gap-2 rounded-agent-md border px-3 py-2 text-left text-sm transition',
282
+ 'flex w-full items-center gap-2 rounded-agent-md border px-3 py-2 text-left text-xs transition',
283
283
  active
284
284
  ? 'border-agent-foreground bg-agent-muted text-agent-foreground'
285
285
  : 'border-agent-border bg-agent-canvas text-agent-foreground hover:bg-agent-muted',
@@ -335,7 +335,7 @@ function SelectQuestionStep({
335
335
  role={question.multiSelect ? 'checkbox' : 'radio'}
336
336
  aria-checked={active}
337
337
  className={[
338
- 'flex w-full items-center gap-2 rounded-agent-md border border-dashed px-3 py-2 text-left text-sm transition',
338
+ 'flex w-full items-center gap-2 rounded-agent-md border border-dashed px-3 py-2 text-left text-xs transition',
339
339
  active
340
340
  ? 'border-agent-foreground bg-agent-muted text-agent-foreground'
341
341
  : 'border-agent-border bg-agent-canvas text-agent-muted-foreground hover:bg-agent-muted hover:text-agent-foreground',
@@ -359,13 +359,13 @@ function SelectQuestionStep({
359
359
 
360
360
  return (
361
361
  <div>
362
- <div className="space-y-2 px-4 py-3">
362
+ <div className="space-y-2 px-3 py-2">
363
363
  {question.header && (
364
364
  <p className="text-[11px] font-medium uppercase tracking-wide text-agent-muted-foreground">
365
365
  {question.header}
366
366
  </p>
367
367
  )}
368
- <p className="text-sm font-medium text-agent-foreground">{question.text}</p>
368
+ <p className="text-xs font-medium text-agent-foreground">{question.text}</p>
369
369
  {question.detail && (
370
370
  <p className="whitespace-pre-line text-xs text-agent-muted-foreground">
371
371
  {question.detail}
@@ -396,7 +396,7 @@ function SelectQuestionStep({
396
396
  }
397
397
  }}
398
398
  placeholder={question.placeholder || '输入你的补充...'}
399
- className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-sm text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
399
+ className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
400
400
  />
401
401
  )}
402
402
 
@@ -414,7 +414,7 @@ function SelectQuestionStep({
414
414
  }
415
415
  }}
416
416
  placeholder={question.placeholder || '输入你的回答...'}
417
- className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-sm text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
417
+ className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
418
418
  />
419
419
  <button
420
420
  type="button"
@@ -488,13 +488,13 @@ function TextQuestionStep({
488
488
 
489
489
  return (
490
490
  <div>
491
- <div className="space-y-2 px-4 py-3">
491
+ <div className="space-y-2 px-3 py-2">
492
492
  {question.header && (
493
493
  <p className="text-[11px] font-medium uppercase tracking-wide text-agent-muted-foreground">
494
494
  {question.header}
495
495
  </p>
496
496
  )}
497
- <p className="text-sm font-medium text-agent-foreground">{question.text}</p>
497
+ <p className="text-xs font-medium text-agent-foreground">{question.text}</p>
498
498
  {question.detail && (
499
499
  <p className="whitespace-pre-line text-xs text-agent-muted-foreground">
500
500
  {question.detail}
@@ -512,7 +512,7 @@ function TextQuestionStep({
512
512
  }
513
513
  }}
514
514
  placeholder={question.placeholder || '输入你的回答...'}
515
- className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-sm text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
515
+ className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
516
516
  />
517
517
  </div>
518
518
 
@@ -577,10 +577,10 @@ export function AskUserQuestionMenu({
577
577
  if (!current) return null;
578
578
 
579
579
  return (
580
- <div className="overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas text-sm text-agent-foreground shadow-xl">
581
- <div className="flex items-start justify-between gap-3 border-b border-agent-border px-4 py-3">
580
+ <div className="overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas text-xs text-agent-foreground shadow-xl">
581
+ <div className="flex items-start justify-between gap-2 border-b border-agent-border px-3 py-2">
582
582
  <div className="min-w-0 flex-1">
583
- <p className="text-sm font-medium text-agent-foreground">
583
+ <p className="text-xs font-medium text-agent-foreground">
584
584
  {intro || '需要你的输入'}
585
585
  </p>
586
586
  <p className="mt-0.5 text-[11px] text-agent-muted-foreground">
@@ -13,6 +13,12 @@ import type { TurnBlock } from './turn-timeline';
13
13
  import { TurnProcessGroup } from './TurnProcessGroup';
14
14
  import { TurnFilesCard } from './TurnFilesCard';
15
15
  import type { TurnFile } from './turn-files';
16
+ import { stripNextStepsTags } from './next-steps-tags';
17
+ import {
18
+ formatTokenSpeed,
19
+ llmRequestElapsedMs,
20
+ type LlmSpeedSnapshot,
21
+ } from './process-status';
16
22
 
17
23
  /**
18
24
  * AssistantMessage — Tier-1 port of `deeppath`'s assistant-bubble.
@@ -98,6 +104,12 @@ interface AssistantMessageProps {
98
104
  startedAtMs?: number;
99
105
  /** Frozen wall-clock of a finished turn. */
100
106
  durationMs?: number;
107
+ /**
108
+ * Model-request generation speed (reasoning + reply + any other LLM
109
+ * output). Elapsed excludes tool waits. Live snapshots keep ticking
110
+ * while `live` is true.
111
+ */
112
+ llmSpeed?: LlmSpeedSnapshot;
101
113
  /**
102
114
  * 本回合产生/修改的文件列表(local-backend 回合收尾时收集,经
103
115
  * `turn_files` SSE + messageMetadata.turnFiles 持久化)。渲染在回答
@@ -152,11 +164,7 @@ const META_ACTION =
152
164
  '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
165
 
154
166
  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
- }`;
167
+ return isPlanMode ? 'rounded-agent-lg bg-amber-50/10 dark:bg-amber-950/5' : '';
160
168
  }
161
169
 
162
170
  // 框架 useChatStream 收到 SSE error 事件时把最后一条助手消息的内容打成
@@ -194,7 +202,7 @@ function readTurnFailure(
194
202
 
195
203
  function TurnErrorBubble({ reason }: { reason: string }) {
196
204
  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">
205
+ <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
206
  请求失败:{reason}
199
207
  </div>
200
208
  );
@@ -206,6 +214,17 @@ function StreamingCursor() {
206
214
  );
207
215
  }
208
216
 
217
+ function useLiveNow(enabled: boolean): number {
218
+ const [now, setNow] = useState(() => Date.now());
219
+ useEffect(() => {
220
+ if (!enabled) return;
221
+ setNow(Date.now());
222
+ const id = window.setInterval(() => setNow(Date.now()), 1000);
223
+ return () => window.clearInterval(id);
224
+ }, [enabled]);
225
+ return now;
226
+ }
227
+
209
228
  export function AssistantMessage({
210
229
  message,
211
230
  isStreaming,
@@ -221,6 +240,7 @@ export function AssistantMessage({
221
240
  onRegenerate,
222
241
  startedAtMs,
223
242
  durationMs,
243
+ llmSpeed,
224
244
  turnFiles,
225
245
  onShare,
226
246
  }: AssistantMessageProps) {
@@ -229,7 +249,7 @@ export function AssistantMessage({
229
249
  // live 错误文本("[stream error] ...")不当正文渲染;落库的失败回合可能
230
250
  // 留有部分正文,此时正文与错误气泡都展示。
231
251
  const displayContent = failure?.contentIsError ? '' : content;
232
- const { copied, copy } = useCopy(content);
252
+ const { copied, copy } = useCopy(stripNextStepsTags(content));
233
253
  const [regenerating, setRegenerating] = useState(false);
234
254
  const [regenerateError, setRegenerateError] = useState<string | null>(null);
235
255
  const persistedAgent = message.agentId
@@ -240,10 +260,15 @@ export function AssistantMessage({
240
260
  const displayAgent = persistedAgent ?? (isStreaming ? currentAgent : null);
241
261
  const useTimeline = timeline !== undefined;
242
262
  const blocks = timeline ?? [];
263
+ const now = useLiveNow(Boolean(isStreaming && llmSpeed?.live));
264
+ const tokenSpeed = formatTokenSpeed(
265
+ llmSpeed?.tokens ?? 0,
266
+ llmRequestElapsedMs(llmSpeed, now),
267
+ );
243
268
 
244
269
  return (
245
270
  <motion.div
246
- className="group/message mb-2"
271
+ className="group/message mb-1"
247
272
  data-message-role="assistant"
248
273
  data-message-id={message.id}
249
274
  initial={{ opacity: 0, y: 8 }}
@@ -252,7 +277,7 @@ export function AssistantMessage({
252
277
  >
253
278
  <div className="mx-auto w-full max-w-[var(--chat-input-box-width)] px-1">
254
279
  {(displayAgent || isPlanMode) && (
255
- <div className="flex items-center justify-between mb-1.5 min-h-[18px]">
280
+ <div className="mb-1 flex min-h-4 items-center justify-between">
256
281
  {displayAgent ? (
257
282
  <AgentBadge agent={displayAgent} isStreaming={isStreaming} />
258
283
  ) : (
@@ -291,7 +316,7 @@ export function AssistantMessage({
291
316
  />
292
317
  </div>
293
318
  ) : (
294
- <div className={`${bubbleClass(isPlanMode)} text-sm italic text-agent-muted-foreground`}>
319
+ <div className={`${bubbleClass(isPlanMode)} text-xs italic text-agent-muted-foreground`}>
295
320
  (空消息)
296
321
  </div>
297
322
  )
@@ -305,7 +330,7 @@ export function AssistantMessage({
305
330
  }
306
331
  renderAnswer={(block, isLast) => (
307
332
  <div className={bubbleClass(isPlanMode)}>
308
- <div className="markdown-content text-sm leading-relaxed text-agent-foreground">
333
+ <div className="markdown-content text-xs leading-relaxed text-agent-foreground">
309
334
  <Markdown agents={agents} chats={chats} chatId={chatId}>{block.content}</Markdown>
310
335
  </div>
311
336
  {isStreaming && isLast && <StreamingCursor />}
@@ -327,7 +352,7 @@ export function AssistantMessage({
327
352
  )}
328
353
  {displayContent ? (
329
354
  <div className={bubbleClass(isPlanMode)}>
330
- <div className="markdown-content text-sm leading-relaxed text-agent-foreground">
355
+ <div className="markdown-content text-xs leading-relaxed text-agent-foreground">
331
356
  <Markdown agents={agents} chats={chats} chatId={chatId}>{displayContent}</Markdown>
332
357
  </div>
333
358
  {isStreaming && <StreamingCursor />}
@@ -343,7 +368,7 @@ export function AssistantMessage({
343
368
  />
344
369
  </div>
345
370
  ) : (
346
- <div className={`${bubbleClass(isPlanMode)} text-sm italic text-agent-muted-foreground`}>
371
+ <div className={`${bubbleClass(isPlanMode)} text-xs italic text-agent-muted-foreground`}>
347
372
  (空消息)
348
373
  </div>
349
374
  )}
@@ -367,6 +392,9 @@ export function AssistantMessage({
367
392
  ? getFriendlyDate(new Date(message.createdAt))
368
393
  : ''}
369
394
  </span>
395
+ {tokenSpeed ? (
396
+ <span data-testid="turn-token-speed">{tokenSpeed}</span>
397
+ ) : null}
370
398
  {content && !isStreaming && (
371
399
  <button
372
400
  type="button"