repoburg 1.3.188 β†’ 1.3.196

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 (265) 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 +521 -265
  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 +39 -8
  49. package/agent-pane/backend/src/db/schema.ts +28 -74
  50. package/agent-pane/backend/src/index.ts +29 -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 +295 -42
  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 +73 -55
  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 +22 -29
  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 +43 -1
  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 +27 -2
  170. package/agent-pane/frontend/src/api/workspaces.test.ts +112 -0
  171. package/agent-pane/frontend/src/api/workspaces.ts +17 -3
  172. package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +49 -0
  173. package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +2 -5
  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 +26 -23
  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 +18 -25
  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 +12 -12
  191. package/agent-pane/frontend/src/components/StatusBar.test.tsx +56 -0
  192. package/agent-pane/frontend/src/components/StatusBar.tsx +25 -31
  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 +2 -6
  197. package/agent-pane/frontend/src/components/TurnContent.test.tsx +287 -0
  198. package/agent-pane/frontend/src/components/TurnContent.tsx +107 -24
  199. package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +297 -0
  200. package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +33 -32
  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 -39
  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 +63 -31
  212. package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +83 -0
  213. package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +66 -7
  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 +16 -617
  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 +113 -7
  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 +80 -104
  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/backend/dist/src/llm-provider/ollama-llm.provider.js +1 -0
  257. package/backend/dist/src/llm-provider/ollama-llm.provider.js.map +1 -1
  258. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  259. package/data.db +0 -0
  260. package/empty-state.png +0 -0
  261. package/frontend-home.png +0 -0
  262. package/model-selector-open.png +0 -0
  263. package/package.json +1 -1
  264. package/slash-popup-working.png +0 -0
  265. package/agent-pane/frontend/src/components/DiffViewer.tsx +0 -27
@@ -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
+ });
@@ -40,11 +40,7 @@ function ToastItem({ toast }: { toast: ToastType }) {
40
40
  }
41
41
 
42
42
  return (
43
- <div
44
- className={`toast toast-${toast.type}`}
45
- onMouseEnter={handleMouseEnter}
46
- onMouseLeave={handleMouseLeave}
47
- >
43
+ <div className={`toast toast-${toast.type}`} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
48
44
  <Icon size={16} />
49
45
  <span className="toast-message">{toast.message}</span>
50
46
  {toast.undoAction && (
@@ -77,4 +73,4 @@ export default function Toast() {
77
73
  ))}
78
74
  </div>
79
75
  );
80
- }
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
+ });
@@ -18,9 +18,19 @@ function itemText(item: Item): string {
18
18
  return '';
19
19
  }
20
20
 
21
+ /** Strip shell wrapper from command for display (e.g. /bin/zsh -lc "actual cmd"). */
22
+ function cleanCommand(cmd: string): string {
23
+ const match = cmd.match(/^\/bin\/\w+\s+-\w+\s+"(.*)"$/s);
24
+ return match ? match[1] : cmd;
25
+ }
26
+
27
+ /** Maximum output length before truncation (10KB). */
28
+ const MAX_OUTPUT = 10_000;
29
+
21
30
  /** Render a single item as a turn card. */
22
31
  function ItemCard({ item }: { item: Item }) {
23
32
  const isRunning = item.status === 'in_progress';
33
+ const [showFullOutput, setShowFullOutput] = useState(false);
24
34
 
25
35
  switch (item.type) {
26
36
  case 'agent_message': {
@@ -40,7 +50,9 @@ function ItemCard({ item }: { item: Item }) {
40
50
  return (
41
51
  <div className="render-content">
42
52
  <div className="turn-header">
43
- <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>πŸ’­</span>
53
+ <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
54
+ πŸ’­
55
+ </span>
44
56
  <span className="turn-header-title">Reasoning</span>
45
57
  </div>
46
58
  <div dangerouslySetInnerHTML={{ __html: renderMarkdown(itemText(item)) }} />
@@ -50,19 +62,37 @@ function ItemCard({ item }: { item: Item }) {
50
62
 
51
63
  case 'command_execution': {
52
64
  const data = item.data || {};
53
- const command = (data.command as string) || '';
65
+ const command = cleanCommand((data.command as string) || '');
54
66
  const output = (data.aggregated_output as string) || '';
55
67
  const exitCode = data.exit_code as number | null;
56
- const summary = isRunning ? 'running…' : (exitCode !== null ? `exit ${exitCode}` : '');
68
+ const summary = isRunning ? 'running…' : exitCode !== null ? `exit ${exitCode}` : '';
69
+ const isTruncated = output.length > MAX_OUTPUT;
70
+ const displayOutput =
71
+ isTruncated && !showFullOutput ? output.slice(0, MAX_OUTPUT) + '\n… (truncated, click to expand)' : output;
57
72
  return (
58
73
  <div className="render-content">
59
74
  <div className="turn-header">
60
- <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>πŸ”§</span>
75
+ <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
76
+ πŸ”§
77
+ </span>
61
78
  <span className="turn-header-title">Command</span>
62
79
  {summary && <span className="turn-header-meta">{summary}</span>}
63
80
  </div>
64
- <pre className="cmd-block"><code>{command}</code></pre>
65
- {output && <pre className="cmd-output"><code>{output}</code></pre>}
81
+ <pre className="cmd-block">
82
+ <code>{command}</code>
83
+ </pre>
84
+ {output && (
85
+ <>
86
+ <pre className="cmd-output">
87
+ <code>{displayOutput}</code>
88
+ </pre>
89
+ {isTruncated && (
90
+ <button className="cmd-output-toggle" onClick={() => setShowFullOutput((v) => !v)}>
91
+ {showFullOutput ? 'Show less' : `Show full output (${(output.length / 1024).toFixed(0)}KB)`}
92
+ </button>
93
+ )}
94
+ </>
95
+ )}
66
96
  </div>
67
97
  );
68
98
  }
@@ -73,7 +103,9 @@ function ItemCard({ item }: { item: Item }) {
73
103
  return (
74
104
  <div className="render-content">
75
105
  <div className="turn-header">
76
- <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>πŸ“</span>
106
+ <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
107
+ πŸ“
108
+ </span>
77
109
  <span className="turn-header-title">File Changes</span>
78
110
  <span className="turn-header-meta">{summary}</span>
79
111
  </div>
@@ -97,13 +129,58 @@ function ItemCard({ item }: { item: Item }) {
97
129
  const data = item.data || {};
98
130
  const tool = (data.tool as string) || 'mcp';
99
131
  const server = (data.server as string) || '';
132
+ const args = data.arguments as Record<string, unknown> | null;
133
+ const result = data.result as {
134
+ content?: Array<{ type: string; text: string }>;
135
+ structured_content?: Record<string, unknown>;
136
+ } | null;
137
+ const error = data.error as string | null;
138
+ const isFailed = item.status === 'failed' || !!error;
139
+ const summary = isRunning ? 'running…' : isFailed ? 'failed' : 'done';
140
+ const resultTexts = result?.content?.filter((c) => c.type === 'text').map((c) => c.text) ?? [];
100
141
  return (
101
142
  <div className="render-content">
102
143
  <div className="turn-header">
103
- <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>πŸ”Œ</span>
144
+ <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
145
+ πŸ”Œ
146
+ </span>
104
147
  <span className="turn-header-title">MCP: {tool}</span>
105
148
  {server && <span className="turn-header-meta">{server}</span>}
149
+ {summary && (
150
+ <span className="turn-header-meta" style={isFailed ? { color: 'var(--red)' } : undefined}>
151
+ {summary}
152
+ </span>
153
+ )}
106
154
  </div>
155
+ {args && Object.keys(args).length > 0 && (
156
+ <div className="mcp-args">
157
+ {Object.entries(args).map(([key, value]) => (
158
+ <div key={key} className="mcp-arg">
159
+ <span className="mcp-arg-key">{key}</span>
160
+ {key === 'code' ? (
161
+ <pre className="cmd-block">
162
+ <code>{String(value)}</code>
163
+ </pre>
164
+ ) : (
165
+ <span className="mcp-arg-value">
166
+ {typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
167
+ </span>
168
+ )}
169
+ </div>
170
+ ))}
171
+ </div>
172
+ )}
173
+ {resultTexts.length > 0 &&
174
+ resultTexts.map((text, i) => (
175
+ <pre key={i} className="cmd-output">
176
+ <code>{text}</code>
177
+ </pre>
178
+ ))}
179
+ {error && (
180
+ <pre className="cmd-output" style={{ color: 'var(--red)' }}>
181
+ <code>{error}</code>
182
+ </pre>
183
+ )}
107
184
  </div>
108
185
  );
109
186
  }
@@ -113,7 +190,9 @@ function ItemCard({ item }: { item: Item }) {
113
190
  return (
114
191
  <div className="render-content">
115
192
  <div className="turn-header">
116
- <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>πŸ”</span>
193
+ <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
194
+ πŸ”
195
+ </span>
117
196
  <span className="turn-header-title">Web Search</span>
118
197
  </div>
119
198
  <p className="user-message">{query}</p>
@@ -126,7 +205,9 @@ function ItemCard({ item }: { item: Item }) {
126
205
  return (
127
206
  <div className="render-content">
128
207
  <div className="turn-header">
129
- <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>πŸ“‹</span>
208
+ <span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
209
+ πŸ“‹
210
+ </span>
130
211
  <span className="turn-header-title">Todo List</span>
131
212
  </div>
132
213
  <ul className="todo-list">
@@ -145,10 +226,14 @@ function ItemCard({ item }: { item: Item }) {
145
226
  return (
146
227
  <div className="render-content">
147
228
  <div className="turn-header">
148
- <span className="turn-header-icon" style={{ background: 'var(--red)', color: '#fff' }}>!</span>
229
+ <span className="turn-header-icon" style={{ background: 'var(--red)', color: '#fff' }}>
230
+ !
231
+ </span>
149
232
  <span className="turn-header-title">Error</span>
150
233
  </div>
151
- <pre className="cmd-output" style={{ color: 'var(--red)' }}><code>{msg}</code></pre>
234
+ <pre className="cmd-output" style={{ color: 'var(--red)' }}>
235
+ <code>{msg}</code>
236
+ </pre>
152
237
  </div>
153
238
  );
154
239
  }
@@ -188,9 +273,7 @@ function TurnContentBase({ turn }: Props) {
188
273
 
189
274
  const hasFinalResponse = lastAgentIdx >= 0;
190
275
  // Intermediate = everything before final agent_message (or ALL items if running)
191
- const intermediateItems = hasFinalResponse
192
- ? turn.items.slice(0, lastAgentIdx)
193
- : turn.items;
276
+ const intermediateItems = hasFinalResponse ? turn.items.slice(0, lastAgentIdx) : turn.items;
194
277
 
195
278
  // Group intermediate items into segments by agent_message boundaries
196
279
  // Each segment: one agent_message + following tool/non-tool items until next agent_message
@@ -220,7 +303,9 @@ function TurnContentBase({ turn }: Props) {
220
303
  {/* User prompt β€” always visible at top */}
221
304
  <div className="render-content">
222
305
  <div className="turn-header">
223
- <span className="turn-header-icon" style={{ background: 'var(--amber)', color: '#fff' }}>U</span>
306
+ <span className="turn-header-icon" style={{ background: 'var(--amber)', color: '#fff' }}>
307
+ U
308
+ </span>
224
309
  <span className="turn-header-title">You</span>
225
310
  </div>
226
311
  <p className="user-message">{turn.user_prompt}</p>
@@ -262,14 +347,10 @@ function TurnContentBase({ turn }: Props) {
262
347
  )}
263
348
 
264
349
  {/* Final agent message β€” always visible */}
265
- {hasFinalResponse && (
266
- <MemoItemCard key={turn.items[lastAgentIdx].id} item={turn.items[lastAgentIdx]} />
267
- )}
350
+ {hasFinalResponse && <MemoItemCard key={turn.items[lastAgentIdx].id} item={turn.items[lastAgentIdx]} />}
268
351
 
269
352
  {/* Items after final agent message (rare β€” errors, etc.) */}
270
- {hasFinalResponse && turn.items.slice(lastAgentIdx + 1).map((item) => (
271
- <MemoItemCard key={item.id} item={item} />
272
- ))}
353
+ {hasFinalResponse && turn.items.slice(lastAgentIdx + 1).map((item) => <MemoItemCard key={item.id} item={item} />)}
273
354
 
274
355
  {/* Pending state (no items at all) */}
275
356
  {turn.items.length === 0 && !turn.error && (turn.status === 'pending' || turn.status === 'running') && (
@@ -283,7 +364,9 @@ function TurnContentBase({ turn }: Props) {
283
364
  {/* Turn error (if any) */}
284
365
  {turn.error && (
285
366
  <div className="render-content">
286
- <pre className="cmd-output" style={{ color: 'var(--red)' }}><code>{turn.error}</code></pre>
367
+ <pre className="cmd-output" style={{ color: 'var(--red)' }}>
368
+ <code>{turn.error}</code>
369
+ </pre>
287
370
  </div>
288
371
  )}
289
372
  </div>
@@ -291,4 +374,4 @@ function TurnContentBase({ turn }: Props) {
291
374
  }
292
375
 
293
376
  const TurnContent = memo(TurnContentBase);
294
- export default TurnContent;
377
+ export default TurnContent;