repoburg 1.3.186 → 1.3.195

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 (262) hide show
  1. package/agent-pane/.prettierignore +5 -0
  2. package/agent-pane/.prettierrc +8 -0
  3. package/agent-pane/backend/coverage/base.css +224 -0
  4. package/agent-pane/backend/coverage/block-navigation.js +87 -0
  5. package/agent-pane/backend/coverage/clover.xml +887 -0
  6. package/agent-pane/backend/coverage/coverage-final.json +10 -0
  7. package/agent-pane/backend/coverage/favicon.png +0 -0
  8. package/agent-pane/backend/coverage/index.html +176 -0
  9. package/agent-pane/backend/coverage/prettify.css +1 -0
  10. package/agent-pane/backend/coverage/prettify.js +2 -0
  11. package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
  12. package/agent-pane/backend/coverage/sorter.js +210 -0
  13. package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +1402 -0
  14. package/agent-pane/backend/coverage/src/codex/index.html +131 -0
  15. package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +331 -0
  16. package/agent-pane/backend/coverage/src/db/index.html +131 -0
  17. package/agent-pane/backend/coverage/src/db/index.ts.html +436 -0
  18. package/agent-pane/backend/coverage/src/db/schema.ts.html +175 -0
  19. package/agent-pane/backend/coverage/src/index.html +116 -0
  20. package/agent-pane/backend/coverage/src/index.ts.html +262 -0
  21. package/agent-pane/backend/coverage/src/routes/index.html +146 -0
  22. package/agent-pane/backend/coverage/src/routes/sessions.ts.html +991 -0
  23. package/agent-pane/backend/coverage/src/routes/turns.ts.html +256 -0
  24. package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +406 -0
  25. package/agent-pane/backend/coverage/src/ws/index.html +116 -0
  26. package/agent-pane/backend/coverage/src/ws/index.ts.html +238 -0
  27. package/agent-pane/backend/data.db +0 -0
  28. package/agent-pane/backend/data.db-shm +0 -0
  29. package/agent-pane/backend/data.db-wal +0 -0
  30. package/agent-pane/backend/dist/index.js +70869 -0
  31. package/agent-pane/backend/package-lock.json +4529 -1013
  32. package/agent-pane/backend/package.json +10 -6
  33. package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +163 -0
  34. package/agent-pane/backend/src/claude/claude-message-mapper.ts +154 -0
  35. package/agent-pane/backend/src/claude/claude-provider.test.ts +65 -0
  36. package/agent-pane/backend/src/claude/claude-provider.ts +482 -0
  37. package/agent-pane/backend/src/claude/claude-types.ts +33 -0
  38. package/agent-pane/backend/src/codex/app-server-client.test.ts +183 -0
  39. package/agent-pane/backend/src/codex/app-server-client.ts +225 -0
  40. package/agent-pane/backend/src/codex/app-server-types.ts +259 -0
  41. package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +56 -0
  42. package/agent-pane/backend/src/codex/codex-provider.test.ts +60 -0
  43. package/agent-pane/backend/src/codex/codex-provider.ts +556 -197
  44. package/agent-pane/backend/src/codex/item-mapper.test.ts +221 -0
  45. package/agent-pane/backend/src/codex/item-mapper.ts +35 -8
  46. package/agent-pane/backend/src/codex/shared-app-server.ts +25 -0
  47. package/agent-pane/backend/src/db/index.test.ts +125 -0
  48. package/agent-pane/backend/src/db/index.ts +61 -6
  49. package/agent-pane/backend/src/db/schema.ts +28 -63
  50. package/agent-pane/backend/src/index.ts +31 -2
  51. package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +29 -0
  52. package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +40 -0
  53. package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +16 -0
  54. package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +60 -0
  55. package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +34 -0
  56. package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +85 -0
  57. package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +48 -0
  58. package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +35 -0
  59. package/agent-pane/backend/src/integration/delta-streaming.test.ts +130 -0
  60. package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +272 -0
  61. package/agent-pane/backend/src/integration/helpers/test-app.ts +108 -0
  62. package/agent-pane/backend/src/integration/helpers/ws-capture.ts +64 -0
  63. package/agent-pane/backend/src/integration/item-lifecycle.test.ts +133 -0
  64. package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +102 -0
  65. package/agent-pane/backend/src/integration/missing-item-started.test.ts +91 -0
  66. package/agent-pane/backend/src/integration/multi-turn.test.ts +121 -0
  67. package/agent-pane/backend/src/integration/session-operations.test.ts +168 -0
  68. package/agent-pane/backend/src/integration/title-generation.test.ts +109 -0
  69. package/agent-pane/backend/src/integration/turn-cancel.test.ts +111 -0
  70. package/agent-pane/backend/src/integration/turn-failure.test.ts +97 -0
  71. package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +141 -0
  72. package/agent-pane/backend/src/providers/index.ts +14 -0
  73. package/agent-pane/backend/src/providers/registry.test.ts +92 -0
  74. package/agent-pane/backend/src/providers/registry.ts +43 -0
  75. package/agent-pane/backend/src/providers/runtime-provider.ts +25 -0
  76. package/agent-pane/backend/src/routes/files.ts +31 -0
  77. package/agent-pane/backend/src/routes/mcp.ts +33 -0
  78. package/agent-pane/backend/src/routes/models.test.ts +92 -0
  79. package/agent-pane/backend/src/routes/models.ts +16 -0
  80. package/agent-pane/backend/src/routes/plugins.ts +40 -0
  81. package/agent-pane/backend/src/routes/providers.test.ts +69 -0
  82. package/agent-pane/backend/src/routes/providers.ts +57 -0
  83. package/agent-pane/backend/src/routes/sessions.test.ts +424 -0
  84. package/agent-pane/backend/src/routes/sessions.ts +311 -21
  85. package/agent-pane/backend/src/routes/skills.test.ts +114 -0
  86. package/agent-pane/backend/src/routes/skills.ts +70 -0
  87. package/agent-pane/backend/src/routes/turns.test.ts +152 -0
  88. package/agent-pane/backend/src/routes/turns.ts +36 -16
  89. package/agent-pane/backend/src/routes/workspaces.test.ts +197 -0
  90. package/agent-pane/backend/src/routes/workspaces.ts +128 -0
  91. package/agent-pane/backend/src/ws/index.test.ts +107 -0
  92. package/agent-pane/backend/src/ws/index.ts +19 -11
  93. package/agent-pane/backend/tsconfig.json +4 -2
  94. package/agent-pane/backend/vitest.config.ts +20 -0
  95. package/agent-pane/eslint.config.js +33 -0
  96. package/agent-pane/frontend/coverage/base.css +224 -0
  97. package/agent-pane/frontend/coverage/block-navigation.js +87 -0
  98. package/agent-pane/frontend/coverage/clover.xml +2962 -0
  99. package/agent-pane/frontend/coverage/coverage-final.json +37 -0
  100. package/agent-pane/frontend/coverage/favicon.png +0 -0
  101. package/agent-pane/frontend/coverage/index.html +251 -0
  102. package/agent-pane/frontend/coverage/prettify.css +1 -0
  103. package/agent-pane/frontend/coverage/prettify.js +2 -0
  104. package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
  105. package/agent-pane/frontend/coverage/sorter.js +210 -0
  106. package/agent-pane/frontend/coverage/src/App.tsx.html +217 -0
  107. package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +223 -0
  108. package/agent-pane/frontend/coverage/src/api/client.ts.html +187 -0
  109. package/agent-pane/frontend/coverage/src/api/index.html +161 -0
  110. package/agent-pane/frontend/coverage/src/api/sessions.ts.html +316 -0
  111. package/agent-pane/frontend/coverage/src/api/websocket.ts.html +544 -0
  112. package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +199 -0
  113. package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +226 -0
  114. package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +382 -0
  115. package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +715 -0
  116. package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +160 -0
  117. package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +478 -0
  118. package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +472 -0
  119. package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +574 -0
  120. package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +205 -0
  121. package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +133 -0
  122. package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +217 -0
  123. package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +271 -0
  124. package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +109 -0
  125. package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +313 -0
  126. package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +1216 -0
  127. package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +1138 -0
  128. package/agent-pane/frontend/coverage/src/components/index.html +326 -0
  129. package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +247 -0
  130. package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +160 -0
  131. package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +427 -0
  132. package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +397 -0
  133. package/agent-pane/frontend/coverage/src/components/panels/index.html +161 -0
  134. package/agent-pane/frontend/coverage/src/constants/index.html +116 -0
  135. package/agent-pane/frontend/coverage/src/constants/models.ts.html +406 -0
  136. package/agent-pane/frontend/coverage/src/hooks/index.html +146 -0
  137. package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +124 -0
  138. package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +142 -0
  139. package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +187 -0
  140. package/agent-pane/frontend/coverage/src/index.html +146 -0
  141. package/agent-pane/frontend/coverage/src/main.tsx.html +115 -0
  142. package/agent-pane/frontend/coverage/src/store/index.html +116 -0
  143. package/agent-pane/frontend/coverage/src/store/store.ts.html +2404 -0
  144. package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +190 -0
  145. package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +367 -0
  146. package/agent-pane/frontend/coverage/src/test/index.html +131 -0
  147. package/agent-pane/frontend/coverage/src/types/index.html +116 -0
  148. package/agent-pane/frontend/coverage/src/types/index.ts.html +466 -0
  149. package/agent-pane/frontend/coverage/src/utils/changes.ts.html +202 -0
  150. package/agent-pane/frontend/coverage/src/utils/duration.ts.html +127 -0
  151. package/agent-pane/frontend/coverage/src/utils/index.html +131 -0
  152. package/agent-pane/frontend/dist/assets/index-Bg0PF2aV.css +1 -0
  153. package/agent-pane/frontend/dist/assets/index-BlybeX5e.js +281 -0
  154. package/agent-pane/frontend/dist/index.html +19 -0
  155. package/agent-pane/frontend/package-lock.json +2434 -213
  156. package/agent-pane/frontend/package.json +10 -3
  157. package/agent-pane/frontend/src/App.test.tsx +74 -0
  158. package/agent-pane/frontend/src/App.tsx +1 -1
  159. package/agent-pane/frontend/src/AppInitializer.test.tsx +142 -0
  160. package/agent-pane/frontend/src/AppInitializer.tsx +1 -1
  161. package/agent-pane/frontend/src/api/client.test.ts +126 -0
  162. package/agent-pane/frontend/src/api/client.ts +24 -19
  163. package/agent-pane/frontend/src/api/models.ts +7 -0
  164. package/agent-pane/frontend/src/api/providers.ts +12 -0
  165. package/agent-pane/frontend/src/api/sessions.test.ts +241 -0
  166. package/agent-pane/frontend/src/api/sessions.ts +60 -2
  167. package/agent-pane/frontend/src/api/skills.ts +12 -0
  168. package/agent-pane/frontend/src/api/websocket.test.ts +288 -0
  169. package/agent-pane/frontend/src/api/websocket.ts +37 -2
  170. package/agent-pane/frontend/src/api/workspaces.test.ts +112 -0
  171. package/agent-pane/frontend/src/api/workspaces.ts +38 -0
  172. package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +49 -0
  173. package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +47 -0
  174. package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +133 -0
  175. package/agent-pane/frontend/src/components/CommandInputBar.tsx +296 -17
  176. package/agent-pane/frontend/src/components/CommandPalette.test.tsx +230 -0
  177. package/agent-pane/frontend/src/components/CommandPalette.tsx +36 -22
  178. package/agent-pane/frontend/src/components/IdeWorkspace.tsx +1 -1
  179. package/agent-pane/frontend/src/components/ModelSelector.test.tsx +129 -0
  180. package/agent-pane/frontend/src/components/ModelSelector.tsx +191 -0
  181. package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +95 -0
  182. package/agent-pane/frontend/src/components/NewSessionDialog.tsx +58 -13
  183. package/agent-pane/frontend/src/components/RenderArea.test.tsx +160 -0
  184. package/agent-pane/frontend/src/components/RenderArea.tsx +76 -70
  185. package/agent-pane/frontend/src/components/RightPanel.test.tsx +43 -0
  186. package/agent-pane/frontend/src/components/RightPanel.tsx +1 -1
  187. package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +32 -0
  188. package/agent-pane/frontend/src/components/RuntimeIcon.tsx +1 -1
  189. package/agent-pane/frontend/src/components/SessionTabs.test.tsx +72 -0
  190. package/agent-pane/frontend/src/components/SessionTabs.tsx +13 -10
  191. package/agent-pane/frontend/src/components/StatusBar.test.tsx +56 -0
  192. package/agent-pane/frontend/src/components/StatusBar.tsx +26 -24
  193. package/agent-pane/frontend/src/components/StatusDot.test.tsx +25 -0
  194. package/agent-pane/frontend/src/components/StatusDot.tsx +2 -7
  195. package/agent-pane/frontend/src/components/Toast.test.tsx +79 -0
  196. package/agent-pane/frontend/src/components/Toast.tsx +57 -15
  197. package/agent-pane/frontend/src/components/TurnContent.test.tsx +287 -0
  198. package/agent-pane/frontend/src/components/TurnContent.tsx +201 -26
  199. package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +297 -0
  200. package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +327 -36
  201. package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +32 -0
  202. package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +63 -0
  203. package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +78 -0
  204. package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +96 -0
  205. package/agent-pane/frontend/src/components/input/SlashPopup.tsx +63 -0
  206. package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +61 -0
  207. package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +12 -35
  208. package/agent-pane/frontend/src/components/panels/LogsPanel.test.tsx +58 -0
  209. package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +1 -1
  210. package/agent-pane/frontend/src/components/panels/MetricsPanel.test.tsx +71 -0
  211. package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +64 -19
  212. package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +83 -0
  213. package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +79 -59
  214. package/agent-pane/frontend/src/constants/models.test.ts +97 -0
  215. package/agent-pane/frontend/src/constants/models.ts +129 -0
  216. package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +42 -0
  217. package/agent-pane/frontend/src/hooks/useEscapeKey.ts +13 -0
  218. package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +92 -0
  219. package/agent-pane/frontend/src/hooks/useOutsideClick.ts +19 -0
  220. package/agent-pane/frontend/src/hooks/useProvider.ts +44 -0
  221. package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +114 -0
  222. package/agent-pane/frontend/src/hooks/useSessionStatus.ts +34 -0
  223. package/agent-pane/frontend/src/main.test.tsx +37 -0
  224. package/agent-pane/frontend/src/main.tsx +1 -1
  225. package/agent-pane/frontend/src/store/sessionSlice.ts +417 -0
  226. package/agent-pane/frontend/src/store/store.test.ts +1284 -0
  227. package/agent-pane/frontend/src/store/store.ts +18 -476
  228. package/agent-pane/frontend/src/store/uiSlice.ts +83 -0
  229. package/agent-pane/frontend/src/store/workspaceSlice.ts +77 -0
  230. package/agent-pane/frontend/src/store/wsSlice.ts +292 -0
  231. package/agent-pane/frontend/src/styles/app.css +191 -8
  232. package/agent-pane/frontend/src/test/component-helpers.ts +36 -0
  233. package/agent-pane/frontend/src/test/fixtures.ts +96 -0
  234. package/agent-pane/frontend/src/test-setup.ts +5 -0
  235. package/agent-pane/frontend/src/types/index.test.ts +128 -0
  236. package/agent-pane/frontend/src/types/index.ts +100 -86
  237. package/agent-pane/frontend/src/utils/changes.test.ts +81 -0
  238. package/agent-pane/frontend/src/utils/changes.ts +39 -0
  239. package/agent-pane/frontend/src/utils/duration.test.ts +71 -0
  240. package/agent-pane/frontend/src/utils/duration.ts +14 -0
  241. package/agent-pane/frontend/tsconfig.json +4 -1
  242. package/agent-pane/frontend/vite.config.ts +6 -0
  243. package/agent-pane/frontend/vitest.config.ts +29 -0
  244. package/agent-pane/package-lock.json +1527 -0
  245. package/agent-pane/package.json +19 -0
  246. package/agent-pane/shared/provider-types.ts +53 -0
  247. package/agent-pane/shared/types.ts +107 -0
  248. package/agentpane-branch-session.png +0 -0
  249. package/agentpane-expanded-all.png +0 -0
  250. package/agentpane-fixed-rendering.png +0 -0
  251. package/agentpane-focus-check.png +0 -0
  252. package/agentpane-initial.png +0 -0
  253. package/agentpane-mcp-detail.png +0 -0
  254. package/agentpane-no-focus-ring.png +0 -0
  255. package/agentpane-soft-focus.png +0 -0
  256. package/data.db +0 -0
  257. package/empty-state.png +0 -0
  258. package/frontend-home.png +0 -0
  259. package/model-selector-open.png +0 -0
  260. package/package.json +2 -2
  261. package/slash-popup-working.png +0 -0
  262. package/agent-pane/frontend/src/components/DiffViewer.tsx +0 -27
@@ -1,47 +1,47 @@
1
1
  import { useStore } from '../store/store.ts';
2
- import { statusToDot, statusToLabel } from '../types/index.ts';
3
2
  import StatusDot from './StatusDot.tsx';
3
+ import ModelSelector from './ModelSelector.tsx';
4
+ import { deriveSessionStatus } from '../hooks/useSessionStatus.ts';
5
+ import { calcSessionDuration } from '../utils/duration.ts';
4
6
  import { Zap, Command } from 'lucide-react';
5
7
 
6
8
  export default function StatusBar() {
7
- const { activeSessionId, sessions, sessionDetails, wsConnected } = useStore();
8
- const session = sessions.find((s) => s.id === activeSessionId);
9
- const detail = activeSessionId ? sessionDetails[activeSessionId] : null;
9
+ const activeSessionId = useStore((s) => s.activeSessionId);
10
+ const session = useStore((s) =>
11
+ activeSessionId ? (s.sessions.find((x) => x.id === activeSessionId) ?? null) : null,
12
+ );
13
+ const detail = useStore((s) => (activeSessionId ? (s.sessionDetails[activeSessionId] ?? null) : null));
14
+ const wsConnected = useStore((s) => s.wsConnected);
10
15
 
11
16
  if (!session) {
12
17
  return (
13
18
  <div className="ide-statusbar">
14
- <div className="sb-item"><Zap size={14} /> AgentPane</div>
19
+ <div className="sb-item">
20
+ <Zap size={14} /> AgentPane
21
+ </div>
15
22
  <div className="sb-divider">|</div>
16
23
  <div className="sb-item" title={wsConnected ? 'WebSocket connected' : 'WebSocket disconnected'}>
17
24
  <span className={`status-dot ${wsConnected ? 'done' : 'error'}`} />
18
25
  {wsConnected ? 'Live' : 'Offline'}
19
26
  </div>
20
27
  <div className="sb-spacer" />
21
- <div className="sb-item"><Command size={14} /> K</div>
28
+ <div className="sb-item">
29
+ <Command size={14} /> K
30
+ </div>
22
31
  </div>
23
32
  );
24
33
  }
25
34
 
26
- const isRunning = session.is_running === 1;
27
- const dot = statusToDot(session.status, isRunning);
28
- const label = statusToLabel(session.status, isRunning);
29
- const turnCount = detail?.turns.length ?? 0;
35
+ const status = deriveSessionStatus(session);
36
+ const duration = calcSessionDuration(detail, status.isRunning);
30
37
 
31
38
  return (
32
39
  <div className="ide-statusbar">
33
- <div className="sb-item"><Zap size={14} /> AgentPane</div>
34
- <div className="sb-divider">|</div>
35
40
  <div className="sb-item">
36
- <span className={`runtime-icon ic-codex`} style={{ width: 16, height: 16, fontSize: 8 }}>CX</span>
37
- {session.runtime_type}
41
+ <Zap size={14} /> AgentPane
38
42
  </div>
39
- {session.model && (
40
- <>
41
- <div className="sb-divider">|</div>
42
- <div className="sb-item">{session.model}</div>
43
- </>
44
- )}
43
+ <div className="sb-divider">|</div>
44
+ <ModelSelector />
45
45
  <div className="sb-spacer" />
46
46
  <div className="sb-item" title={wsConnected ? 'WebSocket connected' : 'WebSocket disconnected'}>
47
47
  <span className={`status-dot ${wsConnected ? 'done' : 'error'}`} />
@@ -49,12 +49,14 @@ export default function StatusBar() {
49
49
  </div>
50
50
  <div className="sb-divider">|</div>
51
51
  <div className="sb-item" style={session.status === 'failed' ? { color: 'var(--red)' } : undefined}>
52
- <StatusDot status={dot} /> {label}
52
+ <StatusDot status={status.dot} /> {status.label}
53
53
  </div>
54
54
  <div className="sb-divider">|</div>
55
- <div className="sb-item">{turnCount} turn{turnCount !== 1 ? 's' : ''}</div>
55
+ <div className="sb-item">{duration}</div>
56
56
  <div className="sb-divider">|</div>
57
- <div className="sb-item"><Command size={14} /> K</div>
57
+ <div className="sb-item">
58
+ <Command size={14} /> K
59
+ </div>
58
60
  </div>
59
61
  );
60
- }
62
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import StatusDot from './StatusDot.tsx';
4
+
5
+ describe('StatusDot', () => {
6
+ it('renders span with status-dot and status class', () => {
7
+ const { container } = render(<StatusDot status="running" />);
8
+ const span = container.firstChild as HTMLElement;
9
+ expect(span.tagName).toBe('SPAN');
10
+ expect(span.className).toBe('status-dot running');
11
+ });
12
+
13
+ it('does not set inline style when size is default (6)', () => {
14
+ const { container } = render(<StatusDot status="error" />);
15
+ const span = container.firstChild as HTMLElement;
16
+ expect(span.style.cssText).toBe('');
17
+ });
18
+
19
+ it('sets width and height inline style when size is custom', () => {
20
+ const { container } = render(<StatusDot status="queued" size={10} />);
21
+ const span = container.firstChild as HTMLElement;
22
+ expect(span.style.width).toBe('10px');
23
+ expect(span.style.height).toBe('10px');
24
+ });
25
+ });
@@ -4,10 +4,5 @@ interface Props {
4
4
  }
5
5
 
6
6
  export default function StatusDot({ status, size = 6 }: Props) {
7
- return (
8
- <span
9
- className={`status-dot ${status}`}
10
- style={size !== 6 ? { width: size, height: size } : undefined}
11
- />
12
- );
13
- }
7
+ return <span className={`status-dot ${status}`} style={size !== 6 ? { width: size, height: size } : undefined} />;
8
+ }
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
3
+ import Toast from './Toast.tsx';
4
+ import { useStore } from '../store/store.ts';
5
+ import { resetStore } from '../test/component-helpers.ts';
6
+
7
+ describe('Toast', () => {
8
+ beforeEach(() => {
9
+ vi.useFakeTimers();
10
+ resetStore();
11
+ });
12
+ afterEach(() => {
13
+ cleanup();
14
+ vi.useRealTimers();
15
+ });
16
+
17
+ it('returns null when no toasts', () => {
18
+ const { container } = render(<Toast />);
19
+ expect(container.firstChild).toBeNull();
20
+ });
21
+
22
+ it('renders toast container with toast items', () => {
23
+ useStore.setState({ toasts: [{ id: 't1', type: 'info', message: 'Hello' }] });
24
+ render(<Toast />);
25
+ expect(screen.getByText('Hello')).toBeInTheDocument();
26
+ expect(screen.getByText('Hello').closest('.toast')).toHaveClass('toast-info');
27
+ });
28
+
29
+ it('auto-dismisses after 4000ms', () => {
30
+ useStore.setState({ toasts: [{ id: 't1', type: 'success', message: 'Auto dismiss' }] });
31
+ render(<Toast />);
32
+ expect(useStore.getState().toasts).toHaveLength(1);
33
+ vi.advanceTimersByTime(4000);
34
+ expect(useStore.getState().toasts).toHaveLength(0);
35
+ });
36
+
37
+ it('hover pauses auto-dismiss timer', () => {
38
+ useStore.setState({ toasts: [{ id: 't1', type: 'info', message: 'Hover test' }] });
39
+ render(<Toast />);
40
+ const toastEl = screen.getByText('Hover test').closest('.toast')!;
41
+ fireEvent.mouseEnter(toastEl);
42
+ vi.advanceTimersByTime(4000);
43
+ // Still present — timer was cleared
44
+ expect(useStore.getState().toasts).toHaveLength(1);
45
+ });
46
+
47
+ it('mouse leave restarts timer with 2000ms delay', () => {
48
+ useStore.setState({ toasts: [{ id: 't1', type: 'info', message: 'Mouse leave' }] });
49
+ render(<Toast />);
50
+ const toastEl = screen.getByText('Mouse leave').closest('.toast')!;
51
+ fireEvent.mouseEnter(toastEl);
52
+ fireEvent.mouseLeave(toastEl);
53
+ vi.advanceTimersByTime(1999);
54
+ expect(useStore.getState().toasts).toHaveLength(1);
55
+ vi.advanceTimersByTime(1);
56
+ expect(useStore.getState().toasts).toHaveLength(0);
57
+ });
58
+
59
+ it('undo button calls undoAction then removes toast', () => {
60
+ const undoFn = vi.fn();
61
+ useStore.setState({
62
+ toasts: [{ id: 't1', type: 'error', message: 'Deleted', undoAction: undoFn, undoLabel: 'Undo' }],
63
+ });
64
+ render(<Toast />);
65
+ const undoBtn = document.querySelector('.toast-undo') as HTMLElement;
66
+ fireEvent.click(undoBtn);
67
+ expect(undoFn).toHaveBeenCalled();
68
+ expect(useStore.getState().toasts).toHaveLength(0);
69
+ });
70
+
71
+ it('close button removes toast', () => {
72
+ useStore.setState({ toasts: [{ id: 't1', type: 'warn', message: 'Warning' }] });
73
+ render(<Toast />);
74
+ // The close button contains an X icon — find by the .toast-close class
75
+ const closeBtn = document.querySelector('.toast-close') as HTMLElement;
76
+ fireEvent.click(closeBtn);
77
+ expect(useStore.getState().toasts).toHaveLength(0);
78
+ });
79
+ });
@@ -1,5 +1,6 @@
1
+ import { useEffect, useRef } from 'react';
1
2
  import { useStore } from '../store/store.ts';
2
- import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
3
+ import { X, CheckCircle, AlertCircle, Info, AlertTriangle, Undo2 } from 'lucide-react';
3
4
  import type { Toast as ToastType } from '../store/store.ts';
4
5
 
5
6
  const ICONS: Record<ToastType['type'], typeof CheckCircle> = {
@@ -9,26 +10,67 @@ const ICONS: Record<ToastType['type'], typeof CheckCircle> = {
9
10
  warn: AlertTriangle,
10
11
  };
11
12
 
13
+ const AUTO_DISMISS_MS = 4000;
14
+
15
+ function ToastItem({ toast }: { toast: ToastType }) {
16
+ const removeToast = useStore((s) => s.removeToast);
17
+ const Icon = ICONS[toast.type];
18
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
19
+
20
+ // Manage auto-dismiss with hover-to-pause
21
+ useEffect(() => {
22
+ function startTimer() {
23
+ timerRef.current = setTimeout(() => removeToast(toast.id), AUTO_DISMISS_MS);
24
+ }
25
+ startTimer();
26
+ return () => {
27
+ if (timerRef.current) clearTimeout(timerRef.current);
28
+ };
29
+ }, [toast.id, removeToast]);
30
+
31
+ function handleMouseEnter() {
32
+ if (timerRef.current) {
33
+ clearTimeout(timerRef.current);
34
+ timerRef.current = null;
35
+ }
36
+ }
37
+
38
+ function handleMouseLeave() {
39
+ timerRef.current = setTimeout(() => removeToast(toast.id), 2000);
40
+ }
41
+
42
+ return (
43
+ <div className={`toast toast-${toast.type}`} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
44
+ <Icon size={16} />
45
+ <span className="toast-message">{toast.message}</span>
46
+ {toast.undoAction && (
47
+ <button
48
+ className="toast-undo"
49
+ onClick={() => {
50
+ toast.undoAction?.();
51
+ removeToast(toast.id);
52
+ }}
53
+ >
54
+ <Undo2 size={12} /> {toast.undoLabel || 'Undo'}
55
+ </button>
56
+ )}
57
+ <button className="toast-close" onClick={() => removeToast(toast.id)}>
58
+ <X size={14} />
59
+ </button>
60
+ </div>
61
+ );
62
+ }
63
+
12
64
  export default function Toast() {
13
65
  const toasts = useStore((s) => s.toasts);
14
- const removeToast = useStore((s) => s.removeToast);
15
66
 
16
67
  if (toasts.length === 0) return null;
17
68
 
18
69
  return (
19
70
  <div className="toast-container">
20
- {toasts.map((toast) => {
21
- const Icon = ICONS[toast.type];
22
- return (
23
- <div key={toast.id} className={`toast toast-${toast.type}`}>
24
- <Icon size={16} />
25
- <span className="toast-message">{toast.message}</span>
26
- <button className="toast-close" onClick={() => removeToast(toast.id)}>
27
- <X size={14} />
28
- </button>
29
- </div>
30
- );
31
- })}
71
+ {toasts.map((toast) => (
72
+ <ToastItem key={toast.id} toast={toast} />
73
+ ))}
32
74
  </div>
33
75
  );
34
- }
76
+ }
@@ -0,0 +1,287 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
3
+ import TurnContent from './TurnContent.tsx';
4
+ import { makeTurn, makeItem } from '../test/fixtures.ts';
5
+ import type { Turn, Item } from '@agentpane/shared';
6
+
7
+ /** Helper: build a turn with items. */
8
+ function turnWithItems(items: Item[], overrides: Partial<Turn> = {}): Turn {
9
+ return makeTurn('t1', 's1', { items, ...overrides });
10
+ }
11
+
12
+ /** Non-tool items (reasoning, todo_list, error) are in otherItems — visible when intermediate expanded. */
13
+ function turnWithNonToolItem(item: Item): Turn {
14
+ return turnWithItems([item], { status: 'running' });
15
+ }
16
+
17
+ /** Tool items (command_execution, file_change, mcp_tool_call, web_search) go after agent_message — always visible. */
18
+ function turnWithToolItem(item: Item): Turn {
19
+ const agentMsg = makeItem('i-agent', 't1', 'agent_message');
20
+ agentMsg.data = { text: 'Done' };
21
+ return turnWithItems([agentMsg, item], { status: 'completed', agent_response: 'Done' });
22
+ }
23
+
24
+ describe('TurnContent', () => {
25
+ afterEach(() => {
26
+ cleanup();
27
+ });
28
+
29
+ it('renders user_prompt at top', () => {
30
+ const turn = turnWithItems([], { user_prompt: 'Fix the bug' });
31
+ render(<TurnContent turn={turn} />);
32
+ expect(screen.getByText('Fix the bug')).toBeInTheDocument();
33
+ });
34
+
35
+ it('renders agent_message item with markdown', () => {
36
+ const item = makeItem('i1', 't1', 'agent_message');
37
+ item.data = { text: 'Hello **world**' };
38
+ const turn = turnWithItems([item], { agent_response: 'Hello **world**' });
39
+ render(<TurnContent turn={turn} />);
40
+ expect(screen.getByText('world').tagName).toBe('STRONG');
41
+ });
42
+
43
+ it('renders reasoning item with "Reasoning" header', () => {
44
+ const item = makeItem('i1', 't1', 'reasoning');
45
+ item.data = { text: 'Thinking about it' };
46
+ render(<TurnContent turn={turnWithNonToolItem(item)} />);
47
+ expect(screen.getByText('Reasoning')).toBeInTheDocument();
48
+ });
49
+
50
+ it('renders command_execution item with command in pre', () => {
51
+ const item = makeItem('i1', 't1', 'command_execution');
52
+ item.data = { command: '/bin/zsh -lc "echo hello"', aggregated_output: 'hello', exit_code: 0 };
53
+ render(<TurnContent turn={turnWithToolItem(item)} />);
54
+ expect(screen.getByText('Command')).toBeInTheDocument();
55
+ expect(screen.getByText('echo hello')).toBeInTheDocument();
56
+ expect(screen.getByText('exit 0')).toBeInTheDocument();
57
+ });
58
+
59
+ it('shows truncation toggle when output exceeds 10KB', () => {
60
+ const item = makeItem('i1', 't1', 'command_execution');
61
+ const longOutput = 'x'.repeat(11_000);
62
+ item.data = { command: 'ls', aggregated_output: longOutput, exit_code: 0 };
63
+ render(<TurnContent turn={turnWithToolItem(item)} />);
64
+ expect(screen.getByText(/Show full output/)).toBeInTheDocument();
65
+ });
66
+
67
+ it('renders file_change item with file list and A/D/M status', () => {
68
+ const item = makeItem('i1', 't1', 'file_change');
69
+ item.data = { changes: [{ path: 'src/new.ts', kind: 'add' }, { path: 'src/old.ts', kind: 'delete' }] };
70
+ render(<TurnContent turn={turnWithToolItem(item)} />);
71
+ expect(screen.getByText('src/new.ts')).toBeInTheDocument();
72
+ expect(screen.getByText('src/old.ts')).toBeInTheDocument();
73
+ expect(screen.getByText('A')).toBeInTheDocument();
74
+ expect(screen.getByText('D')).toBeInTheDocument();
75
+ });
76
+
77
+ it('renders mcp_tool_call item with tool name and server', () => {
78
+ const item = makeItem('i1', 't1', 'mcp_tool_call');
79
+ item.data = { tool: 'search', server: 'my-server', arguments: { query: 'test' } };
80
+ render(<TurnContent turn={turnWithToolItem(item)} />);
81
+ expect(screen.getByText(/search/)).toBeInTheDocument();
82
+ expect(screen.getByText('my-server')).toBeInTheDocument();
83
+ });
84
+
85
+ it('renders web_search item with query', () => {
86
+ const item = makeItem('i1', 't1', 'web_search');
87
+ item.data = { query: 'how to test react' };
88
+ render(<TurnContent turn={turnWithToolItem(item)} />);
89
+ expect(screen.getByText('Web Search')).toBeInTheDocument();
90
+ expect(screen.getByText('how to test react')).toBeInTheDocument();
91
+ });
92
+
93
+ it('renders todo_list item with completed and pending items', () => {
94
+ const item = makeItem('i1', 't1', 'todo_list');
95
+ item.data = { items: [{ text: 'Task 1', completed: true }, { text: 'Task 2', completed: false }] };
96
+ render(<TurnContent turn={turnWithNonToolItem(item)} />);
97
+ expect(screen.getByText(/Task 1/)).toBeInTheDocument();
98
+ expect(screen.getByText(/Task 2/)).toBeInTheDocument();
99
+ });
100
+
101
+ it('renders error item with error message', () => {
102
+ const item = makeItem('i1', 't1', 'error');
103
+ item.data = { message: 'Something broke' };
104
+ render(<TurnContent turn={turnWithNonToolItem(item)} />);
105
+ expect(screen.getByText('Error')).toBeInTheDocument();
106
+ expect(screen.getByText('Something broke')).toBeInTheDocument();
107
+ });
108
+
109
+ it('auto-expands intermediate steps when turn status is running', () => {
110
+ const reasoning = makeItem('i1', 't1', 'reasoning');
111
+ reasoning.data = { text: 'Thinking' };
112
+ const agentMsg = makeItem('i2', 't1', 'agent_message');
113
+ agentMsg.data = { text: 'Done' };
114
+ // Reasoning is intermediate (before final agent_message)
115
+ const turn = turnWithItems([reasoning, agentMsg], { status: 'running', agent_response: 'Done' });
116
+ render(<TurnContent turn={turn} />);
117
+ // Reasoning is a non-tool item — visible when intermediate expanded
118
+ expect(screen.getByText('Reasoning')).toBeInTheDocument();
119
+ });
120
+
121
+ it('auto-collapses intermediate steps when turn status is completed', () => {
122
+ const reasoning = makeItem('i1', 't1', 'reasoning');
123
+ reasoning.data = { text: 'Thinking' };
124
+ const agentMsg = makeItem('i2', 't1', 'agent_message');
125
+ agentMsg.data = { text: 'Done' };
126
+ const turn = turnWithItems([reasoning, agentMsg], { status: 'completed', agent_response: 'Done' });
127
+ render(<TurnContent turn={turn} />);
128
+ // Collapsed — reasoning header not visible
129
+ expect(screen.queryByText('Reasoning')).not.toBeInTheDocument();
130
+ });
131
+
132
+ it('intermediate steps header click toggles expand', () => {
133
+ const reasoning = makeItem('i1', 't1', 'reasoning');
134
+ reasoning.data = { text: 'Thinking' };
135
+ const agentMsg = makeItem('i2', 't1', 'agent_message');
136
+ agentMsg.data = { text: 'Done' };
137
+ const turn = turnWithItems([reasoning, agentMsg], { status: 'completed', agent_response: 'Done' });
138
+ render(<TurnContent turn={turn} />);
139
+ // Initially collapsed
140
+ expect(screen.queryByText('Reasoning')).not.toBeInTheDocument();
141
+ // Click to expand
142
+ const header = document.querySelector('.tool-calls-header')!;
143
+ fireEvent.click(header);
144
+ expect(screen.getByText('Reasoning')).toBeInTheDocument();
145
+ });
146
+
147
+ it('shows "Waiting to start…" for pending turn with no items', () => {
148
+ const turn = turnWithItems([], { status: 'pending' });
149
+ render(<TurnContent turn={turn} />);
150
+ expect(screen.getByText('Waiting to start…')).toBeInTheDocument();
151
+ });
152
+
153
+ it('shows "Agent is thinking…" for running turn with no items', () => {
154
+ const turn = turnWithItems([], { status: 'running' });
155
+ render(<TurnContent turn={turn} />);
156
+ expect(screen.getByText('Agent is thinking…')).toBeInTheDocument();
157
+ });
158
+
159
+ // --- command_execution edge cases ---
160
+
161
+ it('command_execution in_progress shows "running…" summary', () => {
162
+ const item = makeItem('i1', 't1', 'command_execution');
163
+ item.status = 'in_progress';
164
+ item.data = { command: 'ls', aggregated_output: 'partial', exit_code: null };
165
+ render(<TurnContent turn={turnWithToolItem(item)} />);
166
+ expect(screen.getByText('running…')).toBeInTheDocument();
167
+ });
168
+
169
+ it('command_execution with no output renders no output pre', () => {
170
+ const item = makeItem('i1', 't1', 'command_execution');
171
+ item.data = { command: 'ls', exit_code: 0 };
172
+ render(<TurnContent turn={turnWithToolItem(item)} />);
173
+ expect(screen.getByText('ls')).toBeInTheDocument();
174
+ expect(document.querySelector('.cmd-output')).not.toBeInTheDocument();
175
+ });
176
+
177
+ it('command_execution truncate toggle click expands and collapses', () => {
178
+ const item = makeItem('i1', 't1', 'command_execution');
179
+ item.data = { command: 'ls', aggregated_output: 'x'.repeat(11_000), exit_code: 0 };
180
+ render(<TurnContent turn={turnWithToolItem(item)} />);
181
+ const toggle = screen.getByText(/Show full output/);
182
+ fireEvent.click(toggle);
183
+ expect(screen.getByText('Show less')).toBeInTheDocument();
184
+ fireEvent.click(screen.getByText('Show less'));
185
+ expect(screen.getByText(/Show full output/)).toBeInTheDocument();
186
+ });
187
+
188
+ // --- file_change edge cases ---
189
+
190
+ it('file_change with single file shows singular "file"', () => {
191
+ const item = makeItem('i1', 't1', 'file_change');
192
+ item.data = { changes: [{ path: 'src/one.ts', kind: 'modify' }] };
193
+ render(<TurnContent turn={turnWithToolItem(item)} />);
194
+ expect(screen.getByText('1 file')).toBeInTheDocument();
195
+ expect(screen.getByText('M')).toBeInTheDocument();
196
+ });
197
+
198
+ it('file_change with modify kind shows M and fc-mod class', () => {
199
+ const item = makeItem('i1', 't1', 'file_change');
200
+ item.data = { changes: [{ path: 'src/mod.ts', kind: 'modify' }] };
201
+ render(<TurnContent turn={turnWithToolItem(item)} />);
202
+ const statusSpan = document.querySelector('.fc-status')!;
203
+ expect(statusSpan).toHaveClass('fc-mod');
204
+ expect(statusSpan.textContent).toBe('M');
205
+ });
206
+
207
+ // --- mcp_tool_call edge cases ---
208
+
209
+ it('mcp_tool_call failed status shows "failed" summary', () => {
210
+ const item = makeItem('i1', 't1', 'mcp_tool_call');
211
+ item.status = 'failed';
212
+ item.data = { tool: 'broken', server: 'srv' };
213
+ render(<TurnContent turn={turnWithToolItem(item)} />);
214
+ expect(screen.getByText('failed')).toBeInTheDocument();
215
+ });
216
+
217
+ it('mcp_tool_call with code arg renders in pre block', () => {
218
+ const item = makeItem('i1', 't1', 'mcp_tool_call');
219
+ item.data = { tool: 'exec', arguments: { code: 'console.log(1)' } };
220
+ render(<TurnContent turn={turnWithToolItem(item)} />);
221
+ expect(screen.getByText('console.log(1)')).toBeInTheDocument();
222
+ });
223
+
224
+ it('mcp_tool_call with non-string arg renders JSON', () => {
225
+ const item = makeItem('i1', 't1', 'mcp_tool_call');
226
+ item.data = { tool: 'exec', arguments: { count: 42 } };
227
+ render(<TurnContent turn={turnWithToolItem(item)} />);
228
+ expect(screen.getByText('42')).toBeInTheDocument();
229
+ });
230
+
231
+ it('mcp_tool_call with result text renders pre', () => {
232
+ const item = makeItem('i1', 't1', 'mcp_tool_call');
233
+ item.data = {
234
+ tool: 'query',
235
+ result: { content: [{ type: 'text', text: 'result here' }] },
236
+ };
237
+ render(<TurnContent turn={turnWithToolItem(item)} />);
238
+ expect(screen.getByText('result here')).toBeInTheDocument();
239
+ });
240
+
241
+ it('mcp_tool_call with error renders error in red pre', () => {
242
+ const item = makeItem('i1', 't1', 'mcp_tool_call');
243
+ item.data = { tool: 'fail', error: 'MCP failed' };
244
+ render(<TurnContent turn={turnWithToolItem(item)} />);
245
+ expect(screen.getByText('MCP failed')).toBeInTheDocument();
246
+ });
247
+
248
+ it('mcp_tool_call with no args and no server renders minimal', () => {
249
+ const item = makeItem('i1', 't1', 'mcp_tool_call');
250
+ item.data = { tool: 'noop' };
251
+ render(<TurnContent turn={turnWithToolItem(item)} />);
252
+ expect(screen.getByText(/noop/)).toBeInTheDocument();
253
+ expect(document.querySelector('.mcp-args')).not.toBeInTheDocument();
254
+ });
255
+
256
+ // --- turn-level edge cases ---
257
+
258
+ it('renders turn.error in error block', () => {
259
+ const turn = turnWithItems([], { status: 'failed', error: 'Turn failed' });
260
+ render(<TurnContent turn={turn} />);
261
+ expect(screen.getByText('Turn failed')).toBeInTheDocument();
262
+ });
263
+
264
+ it('renders items after final agent_message', () => {
265
+ const agentMsg = makeItem('i1', 't1', 'agent_message');
266
+ agentMsg.data = { text: 'Final answer' };
267
+ const errorItem = makeItem('i2', 't1', 'error');
268
+ errorItem.data = { message: 'Post-error' };
269
+ const turn = turnWithItems([agentMsg, errorItem], { status: 'completed', agent_response: 'Final answer' });
270
+ render(<TurnContent turn={turn} />);
271
+ expect(screen.getByText('Final answer')).toBeInTheDocument();
272
+ expect(screen.getByText('Post-error')).toBeInTheDocument();
273
+ });
274
+
275
+ it('unknown item type renders null (default case)', () => {
276
+ const item = makeItem('i1', 't1', 'unknown_type' as never);
277
+ item.data = {};
278
+ const agentMsg = makeItem('i2', 't1', 'agent_message');
279
+ agentMsg.data = { text: 'Done' };
280
+ const turn = turnWithItems([item, agentMsg], { status: 'completed', agent_response: 'Done' });
281
+ render(<TurnContent turn={turn} />);
282
+ // Agent message renders, unknown item produces no content card
283
+ expect(screen.getByText('Done')).toBeInTheDocument();
284
+ // The unknown item is intermediate — no card for it
285
+ expect(document.querySelectorAll('.render-content').length).toBeLessThan(3);
286
+ });
287
+ });