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
@@ -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,14 +1,40 @@
1
1
  import { useStore } from '../store/store.ts';
2
2
  import TurnContent from './TurnContent.tsx';
3
- import type { Item } from '../types/index.ts';
4
- import { memo } from 'react';
3
+ import { collectFileChanges } from '../utils/changes.ts';
4
+ import { memo, useEffect, useRef } from 'react';
5
5
 
6
6
  function RenderAreaBase() {
7
- const { activeSessionId, sessionDetails, activeTurnIndex, activeRightTab, changesScope, submittingPrompt } = 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 ?? [];
11
- const turn = turns[activeTurnIndex];
21
+ const renderScrollRef = useRef<HTMLDivElement>(null);
22
+
23
+ // F4: Scroll to selected change path when it changes
24
+ useEffect(() => {
25
+ if (!selectedChangePath || activeRightTab !== 'changes') return;
26
+ const timer = setTimeout(() => {
27
+ const sanitized = selectedChangePath.replace(/[^a-zA-Z0-9]/g, '-');
28
+ const el = document.getElementById(`diff-${sanitized}`);
29
+ if (el) {
30
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
31
+ el.classList.add('diff-highlight');
32
+ setTimeout(() => el.classList.remove('diff-highlight'), 1500);
33
+ }
34
+ setSelectedChangePath(null);
35
+ }, 50);
36
+ return () => clearTimeout(timer);
37
+ }, [selectedChangePath, activeRightTab, setSelectedChangePath]);
12
38
 
13
39
  // When Changes tab is active, render file changes from items
14
40
  if (activeRightTab === 'changes') {
@@ -20,30 +46,7 @@ function RenderAreaBase() {
20
46
  );
21
47
  }
22
48
 
23
- // Collect all file_change items across turns
24
- const allChanges: Array<{ path: string; kind: string; turnIdx: number }> = [];
25
- turns.forEach((t, tIdx) => {
26
- t.items.forEach((item: Item) => {
27
- if (item.type === 'file_change' && item.data?.changes) {
28
- const changes = item.data.changes as Array<{ path: string; kind: string }>;
29
- changes.forEach((c) => allChanges.push({ path: c.path, kind: c.kind, turnIdx: tIdx }));
30
- }
31
- });
32
- });
33
-
34
- let entries = allChanges;
35
- if (changesScope === 'turngroup') {
36
- let start = activeTurnIndex;
37
- while (start > 0 && turns[start] && false) start--; // no user/agent distinction in real turns
38
- entries = allChanges.filter((c) => c.turnIdx === activeTurnIndex);
39
- }
40
-
41
- // Deduplicate by path (keep last)
42
- const seen = new Map<string, { path: string; kind: string }>();
43
- for (const c of entries) {
44
- seen.set(c.path, { path: c.path, kind: c.kind });
45
- }
46
- const deduped = Array.from(seen.values());
49
+ const deduped = collectFileChanges(turns, changesScope, activeTurnIndex);
47
50
 
48
51
  if (deduped.length === 0) {
49
52
  return (
@@ -56,7 +59,7 @@ function RenderAreaBase() {
56
59
  }
57
60
 
58
61
  return (
59
- <div className="render-scroll">
62
+ <div className="render-scroll" ref={renderScrollRef}>
60
63
  <div className="render-content">
61
64
  <div style={{ marginBottom: '20px' }}>
62
65
  <span style={{ fontSize: '16px', fontWeight: 600, color: 'var(--text)' }}>
@@ -72,10 +75,11 @@ function RenderAreaBase() {
72
75
  const statusLabel = c.kind === 'add' ? 'Added' : c.kind === 'delete' ? 'Deleted' : 'Modified';
73
76
  const statusClass = c.kind === 'add' ? 'fc-add' : c.kind === 'delete' ? 'fc-del' : 'fc-mod';
74
77
  const statusChar = c.kind === 'add' ? 'A' : c.kind === 'delete' ? 'D' : 'M';
78
+ const sanitizedId = c.path.replace(/[^a-zA-Z0-9]/g, '-');
75
79
 
76
80
  return (
77
81
  <div key={c.path}>
78
- <div className="diff-file-header">
82
+ <div className="diff-file-header" id={`diff-${sanitizedId}`}>
79
83
  <span className={`diff-file-status ${statusClass}`}>{statusChar}</span>
80
84
  <span className="diff-file-name">
81
85
  {dir && <span style={{ color: 'var(--text-muted)' }}>{dir}/</span>}
@@ -96,16 +100,41 @@ function RenderAreaBase() {
96
100
  );
97
101
  }
98
102
 
99
- // Default: render turn content
100
- if (!turn) {
101
- // Show "Sending…" if a prompt was just submitted but turn hasn't been created yet
102
- if (submittingPrompt) {
103
- return (
104
- <div className="render-scroll">
103
+ // F2: Skeleton loading when fetching session detail for the first time
104
+ if (loading && !detail) {
105
+ return (
106
+ <div className="render-scroll">
107
+ <div className="skeleton-bars">
108
+ <div className="skeleton-bar" />
109
+ <div className="skeleton-bar" />
110
+ <div className="skeleton-bar" />
111
+ <div className="skeleton-bar" />
112
+ <div className="skeleton-bar" />
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ if (!detail) {
119
+ return (
120
+ <div className="render-scroll">
121
+ <div className="empty-state">No session selected</div>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ // Render ALL turns, scrollable
127
+ return (
128
+ <div className="render-scroll" ref={renderScrollRef}>
129
+ {/* Submitting prompt — shown at top while waiting for API response */}
130
+ {submittingPrompt && (
131
+ <div className="turn-block">
105
132
  <div className="turn-group">
106
133
  <div className="render-content">
107
134
  <div className="turn-header">
108
- <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>
109
138
  <span className="turn-header-title">You</span>
110
139
  </div>
111
140
  <p className="user-message">{submittingPrompt}</p>
@@ -115,43 +144,20 @@ function RenderAreaBase() {
115
144
  </div>
116
145
  </div>
117
146
  </div>
118
- );
119
- }
147
+ )}
120
148
 
121
- return (
122
- <div className="render-scroll">
123
- <div className="empty-state">
124
- {turns.length === 0 ? 'No turns yet. Send a message below to start.' : 'Select a turn to view content'}
125
- </div>
126
- </div>
127
- );
128
- }
149
+ {turns.length === 0 && !submittingPrompt && (
150
+ <div className="empty-state">No turns yet. Send a message below to start.</div>
151
+ )}
129
152
 
130
- // Error / aborted banner
131
- let banner: React.ReactNode = null;
132
- if (turn.status === 'failed' && turn.error) {
133
- banner = (
134
- <div className="error-banner">
135
- <span className="banner-icon">!</span>
136
- <span className="banner-text">{turn.error}</span>
137
- </div>
138
- );
139
- } else if (turn.status === 'aborted') {
140
- banner = (
141
- <div className="aborted-banner">
142
- <span className="banner-icon">■</span>
143
- <span className="banner-text">Turn was cancelled by user.</span>
144
- </div>
145
- );
146
- }
147
-
148
- return (
149
- <div className="render-scroll">
150
- {banner}
151
- <TurnContent turn={turn} />
153
+ {turns.map((turn, idx) => (
154
+ <div key={turn.id} id={`turn-${idx}`} className="turn-block">
155
+ <TurnContent turn={turn} />
156
+ </div>
157
+ ))}
152
158
  </div>
153
159
  );
154
160
  }
155
161
 
156
162
  const RenderArea = memo(RenderAreaBase);
157
- 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,26 +1,29 @@
1
1
  import { useStore } from '../store/store.ts';
2
- import { statusToDot } from '../types/index.ts';
3
2
  import StatusDot from './StatusDot.tsx';
3
+ import RuntimeIcon from './RuntimeIcon.tsx';
4
+ import { deriveSessionStatus } from '../hooks/useSessionStatus.ts';
4
5
  import { X, Plus } from 'lucide-react';
5
6
 
6
7
  export default function SessionTabs() {
7
- 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);
8
14
 
9
15
  return (
10
16
  <div className="ide-tabs">
11
17
  {openTabs.map((sid) => {
12
18
  const s = sessions.find((x) => x.id === sid);
13
19
  if (!s) return null;
14
- const dot = statusToDot(s.status, s.is_running === 1);
20
+ const status = deriveSessionStatus(s);
15
21
  const isActive = activeSessionId === sid;
16
22
  return (
17
- <div
18
- key={sid}
19
- className={`ide-tab ${isActive ? 'active' : ''}`}
20
- onClick={() => selectSession(sid)}
21
- >
23
+ <div key={sid} className={`ide-tab ${isActive ? 'active' : ''}`} onClick={() => selectSession(sid)}>
24
+ <RuntimeIcon iconClass={status.iconClass} iconText={status.iconText} size={14} />
22
25
  {s.title || sid.slice(0, 8)}
23
- {s.is_running === 1 && <StatusDot status={dot} />}
26
+ {status.dot && <StatusDot status={status.dot} />}
24
27
  <span
25
28
  className="ide-tab-close"
26
29
  onClick={(e) => {
@@ -38,4 +41,4 @@ export default function SessionTabs() {
38
41
  </div>
39
42
  </div>
40
43
  );
41
- }
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
+ });