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,160 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { render, screen, cleanup } from '@testing-library/react';
3
+ import RenderArea from './RenderArea.tsx';
4
+ import { resetStore } from '../test/component-helpers.ts';
5
+ import { makeDetail, makeTurn, makeItem } from '../test/fixtures.ts';
6
+
7
+
8
+ describe('RenderArea', () => {
9
+ beforeEach(() => {
10
+ resetStore();
11
+ });
12
+ afterEach(() => {
13
+ cleanup();
14
+ });
15
+
16
+ it('shows "No session selected" when no active session and not loading', () => {
17
+ render(<RenderArea />);
18
+ expect(screen.getByText('No session selected')).toBeInTheDocument();
19
+ });
20
+
21
+ it('shows skeleton bars when loading and no detail', () => {
22
+ resetStore({ loading: true });
23
+ render(<RenderArea />);
24
+ expect(document.querySelector('.skeleton-bars')).toBeInTheDocument();
25
+ });
26
+
27
+ it('renders turns when session has detail', () => {
28
+ const turn = makeTurn('t1', 's1', { user_prompt: 'Hello agent' });
29
+ const detail = makeDetail('s1', [turn]);
30
+ resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
31
+ render(<RenderArea />);
32
+ expect(screen.getByText('Hello agent')).toBeInTheDocument();
33
+ });
34
+
35
+ it('shows "No turns yet" when session has no turns', () => {
36
+ const detail = makeDetail('s1', []);
37
+ resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
38
+ render(<RenderArea />);
39
+ expect(screen.getByText('No turns yet. Send a message below to start.')).toBeInTheDocument();
40
+ });
41
+
42
+ it('shows submitting prompt at top while waiting', () => {
43
+ const detail = makeDetail('s1', []);
44
+ resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, submittingPrompt: 'pending msg' });
45
+ render(<RenderArea />);
46
+ expect(screen.getByText('pending msg')).toBeInTheDocument();
47
+ expect(screen.getByText('Sending…')).toBeInTheDocument();
48
+ });
49
+
50
+ it('changes tab shows file changes when present', () => {
51
+ const item = makeItem('i1', 't1', 'file_change');
52
+ item.data = { changes: [{ path: 'src/app.ts', kind: 'modify' }] };
53
+ const turn = makeTurn('t1', 's1', { items: [item] });
54
+ const detail = makeDetail('s1', [turn]);
55
+ resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, activeRightTab: 'changes' });
56
+ render(<RenderArea />);
57
+ expect(screen.getByText(/app\.ts/)).toBeInTheDocument();
58
+ });
59
+
60
+ it('changes tab shows "No file changes" when empty', () => {
61
+ const detail = makeDetail('s1', [makeTurn('t1', 's1')]);
62
+ resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, activeRightTab: 'changes' });
63
+ render(<RenderArea />);
64
+ expect(screen.getByText('No file changes in this scope.')).toBeInTheDocument();
65
+ });
66
+
67
+ it('changes tab shows "No session selected" when no session', () => {
68
+ resetStore({ activeRightTab: 'changes' });
69
+ render(<RenderArea />);
70
+ expect(screen.getByText('No session selected')).toBeInTheDocument();
71
+ });
72
+
73
+ // --- selectedChangePath scroll effect ---
74
+
75
+ it('selectedChangePath scrolls to diff element and clears path', () => {
76
+ // Stub scrollIntoView on Element prototype (jsdom doesn't implement it)
77
+ const proto = Element.prototype as unknown as { scrollIntoView: unknown };
78
+ const orig = proto.scrollIntoView;
79
+ proto.scrollIntoView = vi.fn();
80
+
81
+ vi.useFakeTimers();
82
+ const item = makeItem('i1', 't1', 'file_change');
83
+ item.data = { changes: [{ path: 'src/app.ts', kind: 'modify' }] };
84
+ const turn = makeTurn('t1', 's1', { items: [item] });
85
+ const detail = makeDetail('s1', [turn]);
86
+ const setSelectedChangePath = vi.fn();
87
+ resetStore({
88
+ activeSessionId: 's1',
89
+ sessionDetails: { s1: detail },
90
+ activeRightTab: 'changes',
91
+ selectedChangePath: 'src/app.ts',
92
+ setSelectedChangePath,
93
+ });
94
+
95
+ render(<RenderArea />);
96
+ vi.advanceTimersByTime(100);
97
+
98
+ expect(setSelectedChangePath).toHaveBeenCalledWith(null);
99
+ vi.useRealTimers();
100
+ proto.scrollIntoView = orig;
101
+ });
102
+
103
+ // --- changes tab edge cases ---
104
+
105
+ it('changes tab shows "files" plural when multiple changes', () => {
106
+ const item = makeItem('i1', 't1', 'file_change');
107
+ item.data = {
108
+ changes: [
109
+ { path: 'src/a.ts', kind: 'add' },
110
+ { path: 'src/b.ts', kind: 'delete' },
111
+ ],
112
+ };
113
+ const turn = makeTurn('t1', 's1', { items: [item] });
114
+ const detail = makeDetail('s1', [turn]);
115
+ resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, activeRightTab: 'changes' });
116
+ render(<RenderArea />);
117
+ expect(screen.getByText('2 files')).toBeInTheDocument();
118
+ });
119
+
120
+ it('changes tab shows add and delete status badges', () => {
121
+ const item = makeItem('i1', 't1', 'file_change');
122
+ item.data = {
123
+ changes: [
124
+ { path: 'src/new.ts', kind: 'add' },
125
+ { path: 'src/old.ts', kind: 'delete' },
126
+ ],
127
+ };
128
+ const turn = makeTurn('t1', 's1', { items: [item] });
129
+ const detail = makeDetail('s1', [turn]);
130
+ resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, activeRightTab: 'changes' });
131
+ render(<RenderArea />);
132
+ expect(screen.getByText('Added')).toBeInTheDocument();
133
+ expect(screen.getByText('Deleted')).toBeInTheDocument();
134
+ });
135
+
136
+ it('changes tab shows directory path split', () => {
137
+ const item = makeItem('i1', 't1', 'file_change');
138
+ item.data = { changes: [{ path: 'src/components/Foo.tsx', kind: 'modify' }] };
139
+ const turn = makeTurn('t1', 's1', { items: [item] });
140
+ const detail = makeDetail('s1', [turn]);
141
+ resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail }, activeRightTab: 'changes' });
142
+ render(<RenderArea />);
143
+ expect(screen.getByText('Foo.tsx')).toBeInTheDocument();
144
+ });
145
+
146
+ // --- loading + detail edge case ---
147
+
148
+ it('loading with existing detail renders turns, not skeleton', () => {
149
+ const turn = makeTurn('t1', 's1', { user_prompt: 'Hello' });
150
+ const detail = makeDetail('s1', [turn]);
151
+ resetStore({
152
+ activeSessionId: 's1',
153
+ sessionDetails: { s1: detail },
154
+ loading: true,
155
+ });
156
+ render(<RenderArea />);
157
+ expect(screen.getByText('Hello')).toBeInTheDocument();
158
+ expect(document.querySelector('.skeleton-bars')).not.toBeInTheDocument();
159
+ });
160
+ });
@@ -1,10 +1,20 @@
1
1
  import { useStore } from '../store/store.ts';
2
2
  import TurnContent from './TurnContent.tsx';
3
- import type { Item } from '../types/index.ts';
3
+ import { collectFileChanges } from '../utils/changes.ts';
4
4
  import { memo, useEffect, useRef } from 'react';
5
5
 
6
6
  function RenderAreaBase() {
7
- const { activeSessionId, sessionDetails, activeTurnIndex, activeRightTab, changesScope, submittingPrompt, loading, selectedChangePath, setSelectedChangePath } = useStore();
7
+ const {
8
+ activeSessionId,
9
+ sessionDetails,
10
+ activeTurnIndex,
11
+ activeRightTab,
12
+ changesScope,
13
+ submittingPrompt,
14
+ loading,
15
+ selectedChangePath,
16
+ setSelectedChangePath,
17
+ } = useStore();
8
18
 
9
19
  const detail = activeSessionId ? sessionDetails[activeSessionId] : null;
10
20
  const turns = detail?.turns ?? [];
@@ -36,24 +46,7 @@ function RenderAreaBase() {
36
46
  );
37
47
  }
38
48
 
39
- const allChanges: Array<{ path: string; kind: string; turnIdx: number }> = [];
40
- turns.forEach((t, tIdx) => {
41
- t.items.forEach((item: Item) => {
42
- if (item.type === 'file_change' && item.data?.changes) {
43
- const changes = item.data.changes as Array<{ path: string; kind: string }>;
44
- changes.forEach((c) => allChanges.push({ path: c.path, kind: c.kind, turnIdx: tIdx }));
45
- }
46
- });
47
- });
48
-
49
- let entries = allChanges;
50
- if (changesScope === 'turngroup') {
51
- entries = allChanges.filter((c) => c.turnIdx === activeTurnIndex);
52
- }
53
-
54
- const seen = new Map<string, { path: string; kind: string }>();
55
- for (const c of entries) seen.set(c.path, { path: c.path, kind: c.kind });
56
- const deduped = Array.from(seen.values());
49
+ const deduped = collectFileChanges(turns, changesScope, activeTurnIndex);
57
50
 
58
51
  if (deduped.length === 0) {
59
52
  return (
@@ -139,7 +132,9 @@ function RenderAreaBase() {
139
132
  <div className="turn-group">
140
133
  <div className="render-content">
141
134
  <div className="turn-header">
142
- <span className="turn-header-icon" style={{ background: 'var(--amber)', color: '#fff' }}>U</span>
135
+ <span className="turn-header-icon" style={{ background: 'var(--amber)', color: '#fff' }}>
136
+ U
137
+ </span>
143
138
  <span className="turn-header-title">You</span>
144
139
  </div>
145
140
  <p className="user-message">{submittingPrompt}</p>
@@ -152,9 +147,7 @@ function RenderAreaBase() {
152
147
  )}
153
148
 
154
149
  {turns.length === 0 && !submittingPrompt && (
155
- <div className="empty-state">
156
- No turns yet. Send a message below to start.
157
- </div>
150
+ <div className="empty-state">No turns yet. Send a message below to start.</div>
158
151
  )}
159
152
 
160
153
  {turns.map((turn, idx) => (
@@ -167,4 +160,4 @@ function RenderAreaBase() {
167
160
  }
168
161
 
169
162
  const RenderArea = memo(RenderAreaBase);
170
- export default RenderArea;
163
+ export default RenderArea;
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
3
+ import RightPanel from './RightPanel.tsx';
4
+ import { resetStore } from '../test/component-helpers.ts';
5
+
6
+ describe('RightPanel', () => {
7
+ beforeEach(() => {
8
+ resetStore();
9
+ });
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+
14
+ it('renders 4 tab buttons', () => {
15
+ render(<RightPanel />);
16
+ expect(screen.getByText('Turns')).toBeInTheDocument();
17
+ expect(screen.getByText('Metrics')).toBeInTheDocument();
18
+ expect(screen.getByText('Changes')).toBeInTheDocument();
19
+ expect(screen.getByText('Logs')).toBeInTheDocument();
20
+ });
21
+
22
+ it('active tab has active class', () => {
23
+ resetStore({ activeRightTab: 'metrics' });
24
+ render(<RightPanel />);
25
+ const metricsTab = screen.getByText('Metrics').closest('.right-tab');
26
+ expect(metricsTab).toHaveClass('active');
27
+ });
28
+
29
+ it('click on tab calls switchRightTab', () => {
30
+ const switchRightTab = vi.fn();
31
+ resetStore({ switchRightTab });
32
+ render(<RightPanel />);
33
+ fireEvent.click(screen.getByText('Changes'));
34
+ expect(switchRightTab).toHaveBeenCalledWith('changes');
35
+ });
36
+
37
+ it('renders TurnsPanel when activeRightTab is turns', () => {
38
+ resetStore({ activeRightTab: 'turns' });
39
+ render(<RightPanel />);
40
+ // TurnsPanel renders "No turns yet" when no active session
41
+ expect(screen.getByText('No turns yet')).toBeInTheDocument();
42
+ });
43
+ });
@@ -37,4 +37,4 @@ export default function RightPanel() {
37
37
  </div>
38
38
  </div>
39
39
  );
40
- }
40
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import RuntimeIcon from './RuntimeIcon.tsx';
4
+
5
+ describe('RuntimeIcon', () => {
6
+ it('renders span with runtime-icon and iconClass', () => {
7
+ const { container } = render(<RuntimeIcon iconClass="ic-codex" iconText="CX" />);
8
+ const span = container.firstChild as HTMLElement;
9
+ expect(span.tagName).toBe('SPAN');
10
+ expect(span.className).toBe('runtime-icon ic-codex');
11
+ });
12
+
13
+ it('renders iconText as child content', () => {
14
+ const { container } = render(<RuntimeIcon iconClass="ic-claude" iconText="CL" />);
15
+ const span = container.firstChild as HTMLElement;
16
+ expect(span.textContent).toBe('CL');
17
+ });
18
+
19
+ it('does not set inline style when size is default (20)', () => {
20
+ const { container } = render(<RuntimeIcon iconClass="ic-codex" iconText="CX" />);
21
+ const span = container.firstChild as HTMLElement;
22
+ expect(span.style.cssText).toBe('');
23
+ });
24
+
25
+ it('sets width, height, and fontSize inline style when size is custom', () => {
26
+ const { container } = render(<RuntimeIcon iconClass="ic-codex" iconText="CX" size={14} />);
27
+ const span = container.firstChild as HTMLElement;
28
+ expect(span.style.width).toBe('14px');
29
+ expect(span.style.height).toBe('14px');
30
+ expect(span.style.fontSize).toBe('6.3px'); // 14 * 0.45
31
+ });
32
+ });
@@ -13,4 +13,4 @@ export default function RuntimeIcon({ iconClass, iconText, size = 20 }: Props) {
13
13
  {iconText}
14
14
  </span>
15
15
  );
16
- }
16
+ }
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
3
+ import SessionTabs from './SessionTabs.tsx';
4
+ import { resetStore } from '../test/component-helpers.ts';
5
+ import { makeSession } from '../test/fixtures.ts';
6
+
7
+ describe('SessionTabs', () => {
8
+ beforeEach(() => {
9
+ resetStore();
10
+ });
11
+ afterEach(() => {
12
+ cleanup();
13
+ });
14
+
15
+ it('renders one tab per openTabs entry', () => {
16
+ const s1 = makeSession('s1', { title: 'Session 1' });
17
+ const s2 = makeSession('s2', { title: 'Session 2' });
18
+ resetStore({
19
+ openTabs: ['s1', 's2'],
20
+ sessions: [s1, s2],
21
+ activeSessionId: 's1',
22
+ });
23
+ render(<SessionTabs />);
24
+ expect(screen.getByText('Session 1')).toBeInTheDocument();
25
+ expect(screen.getByText('Session 2')).toBeInTheDocument();
26
+ });
27
+
28
+ it('active tab has active class', () => {
29
+ const s1 = makeSession('s1', { title: 'Tab One' });
30
+ resetStore({ openTabs: ['s1'], sessions: [s1], activeSessionId: 's1' });
31
+ render(<SessionTabs />);
32
+ const tab = screen.getByText('Tab One').closest('.ide-tab');
33
+ expect(tab).toHaveClass('active');
34
+ });
35
+
36
+ it('click on tab calls selectSession', () => {
37
+ const selectSession = vi.fn().mockResolvedValue(undefined);
38
+ const s1 = makeSession('s1', { title: 'Tab One' });
39
+ const s2 = makeSession('s2', { title: 'Tab Two' });
40
+ resetStore({ openTabs: ['s1', 's2'], sessions: [s1, s2], activeSessionId: 's1', selectSession });
41
+ render(<SessionTabs />);
42
+ fireEvent.click(screen.getByText('Tab Two'));
43
+ expect(selectSession).toHaveBeenCalledWith('s2');
44
+ });
45
+
46
+ it('close button calls closeTab with correct id', () => {
47
+ const closeTab = vi.fn();
48
+ const s1 = makeSession('s1', { title: 'Close Me' });
49
+ resetStore({ openTabs: ['s1'], sessions: [s1], activeSessionId: 's1', closeTab });
50
+ render(<SessionTabs />);
51
+ const closeBtn = screen.getByText('Close Me').closest('.ide-tab')!.querySelector('.ide-tab-close')!;
52
+ fireEvent.click(closeBtn);
53
+ expect(closeTab).toHaveBeenCalledWith('s1');
54
+ });
55
+
56
+ it('plus button calls setShowNewSessionDialog(true)', () => {
57
+ const setShowNewSessionDialog = vi.fn();
58
+ resetStore({ openTabs: [], sessions: [], setShowNewSessionDialog });
59
+ render(<SessionTabs />);
60
+ const addBtn = document.querySelector('.ide-tab-add')!;
61
+ fireEvent.click(addBtn);
62
+ expect(setShowNewSessionDialog).toHaveBeenCalledWith(true);
63
+ });
64
+
65
+ it('missing session in openTabs returns null for that tab', () => {
66
+ resetStore({ openTabs: ['s1'], sessions: [], activeSessionId: null });
67
+ render(<SessionTabs />);
68
+ // No tab content rendered — only the + button
69
+ expect(document.querySelectorAll('.ide-tab')).toHaveLength(0);
70
+ expect(document.querySelector('.ide-tab-add')).toBeInTheDocument();
71
+ });
72
+ });
@@ -1,29 +1,29 @@
1
1
  import { useStore } from '../store/store.ts';
2
- import { statusToDot, runtimeIcon } from '../types/index.ts';
3
2
  import StatusDot from './StatusDot.tsx';
4
3
  import RuntimeIcon from './RuntimeIcon.tsx';
4
+ import { deriveSessionStatus } from '../hooks/useSessionStatus.ts';
5
5
  import { X, Plus } from 'lucide-react';
6
6
 
7
7
  export default function SessionTabs() {
8
- const { openTabs, activeSessionId, sessions, selectSession, closeTab, setShowNewSessionDialog } = useStore();
8
+ const openTabs = useStore((s) => s.openTabs);
9
+ const activeSessionId = useStore((s) => s.activeSessionId);
10
+ const sessions = useStore((s) => s.sessions);
11
+ const selectSession = useStore((s) => s.selectSession);
12
+ const closeTab = useStore((s) => s.closeTab);
13
+ const setShowNewSessionDialog = useStore((s) => s.setShowNewSessionDialog);
9
14
 
10
15
  return (
11
16
  <div className="ide-tabs">
12
17
  {openTabs.map((sid) => {
13
18
  const s = sessions.find((x) => x.id === sid);
14
19
  if (!s) return null;
15
- const dot = statusToDot(s.status, s.is_running === 1, s.has_unreviewed === 1, s.lifecycle);
20
+ const status = deriveSessionStatus(s);
16
21
  const isActive = activeSessionId === sid;
17
- const ri = runtimeIcon(s.runtime_type);
18
22
  return (
19
- <div
20
- key={sid}
21
- className={`ide-tab ${isActive ? 'active' : ''}`}
22
- onClick={() => selectSession(sid)}
23
- >
24
- <RuntimeIcon iconClass={ri.iconClass} iconText={ri.iconText} size={14} />
23
+ <div key={sid} className={`ide-tab ${isActive ? 'active' : ''}`} onClick={() => selectSession(sid)}>
24
+ <RuntimeIcon iconClass={status.iconClass} iconText={status.iconText} size={14} />
25
25
  {s.title || sid.slice(0, 8)}
26
- {dot && <StatusDot status={dot} />}
26
+ {status.dot && <StatusDot status={status.dot} />}
27
27
  <span
28
28
  className="ide-tab-close"
29
29
  onClick={(e) => {
@@ -41,4 +41,4 @@ export default function SessionTabs() {
41
41
  </div>
42
42
  </div>
43
43
  );
44
- }
44
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { render, screen, cleanup } from '@testing-library/react';
3
+ import StatusBar from './StatusBar.tsx';
4
+ import { resetStore } from '../test/component-helpers.ts';
5
+ import { makeSession, makeDetail, makeTurn } from '../test/fixtures.ts';
6
+
7
+ describe('StatusBar', () => {
8
+ beforeEach(() => {
9
+ resetStore();
10
+ });
11
+ afterEach(() => {
12
+ cleanup();
13
+ });
14
+
15
+ it('shows AgentPane and Offline when no session', () => {
16
+ render(<StatusBar />);
17
+ expect(screen.getByText('AgentPane')).toBeInTheDocument();
18
+ expect(screen.getByText('Offline')).toBeInTheDocument();
19
+ });
20
+
21
+ it('shows Live when wsConnected is true', () => {
22
+ resetStore({ wsConnected: true });
23
+ render(<StatusBar />);
24
+ expect(screen.getByText('Live')).toBeInTheDocument();
25
+ });
26
+
27
+ it('shows Offline when wsConnected is false', () => {
28
+ resetStore({ wsConnected: false });
29
+ render(<StatusBar />);
30
+ expect(screen.getByText('Offline')).toBeInTheDocument();
31
+ });
32
+
33
+ it('shows status label for active session', () => {
34
+ const session = makeSession('s1', { status: 'active', is_running: 1 });
35
+ resetStore({ activeSessionId: 's1', sessions: [session], sessionDetails: { s1: makeDetail('s1') } });
36
+ render(<StatusBar />);
37
+ expect(screen.getByText('Running')).toBeInTheDocument();
38
+ });
39
+
40
+ it('shows duration for active session', () => {
41
+ const session = makeSession('s1', { status: 'active', is_running: 0 });
42
+ const t1 = makeTurn('t1', 's1', {
43
+ created_at: '2025-01-01T00:00:00Z',
44
+ updated_at: '2025-01-01T00:00:05Z',
45
+ });
46
+ const detail = makeDetail('s1', [t1]);
47
+ resetStore({
48
+ activeSessionId: 's1',
49
+ sessions: [session],
50
+ sessionDetails: { s1: detail },
51
+ });
52
+ render(<StatusBar />);
53
+ // Duration = 5 seconds between first turn created_at and last turn updated_at
54
+ expect(screen.getByText('5s')).toBeInTheDocument();
55
+ });
56
+ });
@@ -1,55 +1,47 @@
1
1
  import { useStore } from '../store/store.ts';
2
- import { statusToDot, statusToLabel, formatDuration } 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, session.has_unreviewed === 1, session.lifecycle);
28
- const label = statusToLabel(session.status, isRunning, session.has_unreviewed === 1, session.lifecycle);
29
-
30
- // Calculate duration
31
- let duration = '—';
32
- if (detail && detail.turns.length > 0) {
33
- const firstTurn = detail.turns[0];
34
- const lastTurn = detail.turns[detail.turns.length - 1];
35
- const endISO = isRunning ? null : lastTurn.updated_at;
36
- duration = formatDuration(firstTurn.created_at, endISO);
37
- }
35
+ const status = deriveSessionStatus(session);
36
+ const duration = calcSessionDuration(detail, status.isRunning);
38
37
 
39
38
  return (
40
39
  <div className="ide-statusbar">
41
- <div className="sb-item"><Zap size={14} /> AgentPane</div>
42
- <div className="sb-divider">|</div>
43
40
  <div className="sb-item">
44
- <span className={`runtime-icon ic-codex`} style={{ width: 16, height: 16, fontSize: 8 }}>CX</span>
45
- {session.runtime_type}
41
+ <Zap size={14} /> AgentPane
46
42
  </div>
47
- {session.model && (
48
- <>
49
- <div className="sb-divider">|</div>
50
- <div className="sb-item">{session.model}</div>
51
- </>
52
- )}
43
+ <div className="sb-divider">|</div>
44
+ <ModelSelector />
53
45
  <div className="sb-spacer" />
54
46
  <div className="sb-item" title={wsConnected ? 'WebSocket connected' : 'WebSocket disconnected'}>
55
47
  <span className={`status-dot ${wsConnected ? 'done' : 'error'}`} />
@@ -57,12 +49,14 @@ export default function StatusBar() {
57
49
  </div>
58
50
  <div className="sb-divider">|</div>
59
51
  <div className="sb-item" style={session.status === 'failed' ? { color: 'var(--red)' } : undefined}>
60
- <StatusDot status={dot} /> {label}
52
+ <StatusDot status={status.dot} /> {status.label}
61
53
  </div>
62
54
  <div className="sb-divider">|</div>
63
55
  <div className="sb-item">{duration}</div>
64
56
  <div className="sb-divider">|</div>
65
- <div className="sb-item"><Command size={14} /> K</div>
57
+ <div className="sb-item">
58
+ <Command size={14} /> K
59
+ </div>
66
60
  </div>
67
61
  );
68
- }
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
+ }