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,129 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
3
+ import ModelSelector from './ModelSelector.tsx';
4
+ import { resetStore } from '../test/component-helpers.ts';
5
+ import { makeSession } from '../test/fixtures.ts';
6
+
7
+ describe('ModelSelector', () => {
8
+ beforeEach(() => {
9
+ resetStore();
10
+ });
11
+ afterEach(() => {
12
+ cleanup();
13
+ });
14
+
15
+ it('returns null when no active session', () => {
16
+ const { container } = render(<ModelSelector />);
17
+ expect(container.firstChild).toBeNull();
18
+ });
19
+
20
+ it('renders button with provider and model label', () => {
21
+ const session = makeSession('s1', { model: 'gpt-5.6-sol', model_reasoning_effort: 'low' });
22
+ resetStore({ activeSessionId: 's1', sessions: [session] });
23
+ render(<ModelSelector />);
24
+ const btn = document.querySelector('.model-selector-btn') as HTMLElement;
25
+ expect(btn.textContent).toContain('Codex');
26
+ expect(btn.textContent).toContain('GPT-5.6-Sol');
27
+ });
28
+
29
+ it('opens popover with 3 columns on button click', () => {
30
+ const session = makeSession('s1', { model: 'gpt-5.6-sol', model_reasoning_effort: 'low' });
31
+ resetStore({ activeSessionId: 's1', sessions: [session] });
32
+ render(<ModelSelector />);
33
+ const btn = document.querySelector('.model-selector-btn') as HTMLElement;
34
+ fireEvent.click(btn);
35
+ expect(document.querySelector('.model-selector-popover')).toBeInTheDocument();
36
+ const headers = document.querySelectorAll('.model-selector-col-header');
37
+ expect(headers).toHaveLength(3);
38
+ expect(headers[0].textContent).toBe('Provider');
39
+ expect(headers[1].textContent).toBe('Model');
40
+ expect(headers[2].textContent).toBe('Thinking');
41
+ });
42
+
43
+ it('selecting a model calls updateModelConfig and closes popover', async () => {
44
+ const session = makeSession('s1', { model: 'gpt-5.6-sol', model_reasoning_effort: 'low' });
45
+ const updateModelConfig = vi.fn().mockResolvedValue(undefined);
46
+ resetStore({ activeSessionId: 's1', sessions: [session], updateModelConfig });
47
+ render(<ModelSelector />);
48
+ fireEvent.click(document.querySelector('.model-selector-btn')!);
49
+ // Click on "GPT-5.6-Terra" model
50
+ const terraItem = Array.from(document.querySelectorAll('.model-selector-item')).find((el) =>
51
+ el.textContent?.includes('GPT-5.6-Terra'),
52
+ )!;
53
+ fireEvent.click(terraItem);
54
+ expect(updateModelConfig).toHaveBeenCalledWith('gpt-5.6-terra', expect.any(String));
55
+ });
56
+
57
+ it('selecting reasoning calls updateModelConfig with current model', async () => {
58
+ const session = makeSession('s1', { model: 'gpt-5.6-sol', model_reasoning_effort: 'low' });
59
+ const updateModelConfig = vi.fn().mockResolvedValue(undefined);
60
+ resetStore({ activeSessionId: 's1', sessions: [session], updateModelConfig });
61
+ render(<ModelSelector />);
62
+ fireEvent.click(document.querySelector('.model-selector-btn')!);
63
+ // Click on "High" reasoning level
64
+ const highItem = Array.from(document.querySelectorAll('.model-selector-item')).find((el) =>
65
+ el.textContent?.trim() === 'High',
66
+ )!;
67
+ fireEvent.click(highItem);
68
+ expect(updateModelConfig).toHaveBeenCalledWith('gpt-5.6-sol', 'high');
69
+ });
70
+
71
+ it('reasoning cascade: switching to model without current reasoning level switches to default', async () => {
72
+ // gpt-5.5 has levels [low, medium, high, xhigh] — no "max" or "ultra"
73
+ // gpt-5.6-sol has levels [low, medium, high, xhigh, max, ultra]
74
+ // Switch from gpt-5.6-sol (reasoning=ultra) to gpt-5.5 → ultra not supported → default for gpt-5.5 is "medium"
75
+ const session = makeSession('s1', { model: 'gpt-5.6-sol', model_reasoning_effort: 'ultra' });
76
+ const updateModelConfig = vi.fn().mockResolvedValue(undefined);
77
+ resetStore({ activeSessionId: 's1', sessions: [session], updateModelConfig });
78
+ render(<ModelSelector />);
79
+ fireEvent.click(document.querySelector('.model-selector-btn')!);
80
+ // Click on "GPT-5.5" model
81
+ const gpt55Item = Array.from(document.querySelectorAll('.model-selector-item')).find((el) =>
82
+ el.textContent?.includes('GPT-5.5'),
83
+ )!;
84
+ fireEvent.click(gpt55Item);
85
+ // gpt-5.5 defaultReasoning is "medium"
86
+ expect(updateModelConfig).toHaveBeenCalledWith('gpt-5.5', 'medium');
87
+ });
88
+
89
+ it('clicking a different provider calls updateModelConfig with runtimeType and keeps popover open', async () => {
90
+ const session = makeSession('s1', { runtime_type: 'codex', model: 'gpt-5.6-sol', model_reasoning_effort: 'low' });
91
+ const updateModelConfig = vi.fn().mockResolvedValue(undefined);
92
+ resetStore({ activeSessionId: 's1', sessions: [session], updateModelConfig });
93
+ render(<ModelSelector />);
94
+ fireEvent.click(document.querySelector('.model-selector-btn')!);
95
+ // Click on "Claude" provider
96
+ const claudeItem = Array.from(document.querySelectorAll('.model-selector-item')).find((el) =>
97
+ el.textContent?.includes('Claude'),
98
+ )!;
99
+ fireEvent.click(claudeItem);
100
+ expect(updateModelConfig).toHaveBeenCalledWith('', '', 'claude');
101
+ // Popover should stay open
102
+ expect(document.querySelector('.model-selector-popover')).toBeInTheDocument();
103
+ });
104
+
105
+ it('outside click closes popover', () => {
106
+ const session = makeSession('s1', { model: 'gpt-5.6-sol', model_reasoning_effort: 'low' });
107
+ resetStore({ activeSessionId: 's1', sessions: [session] });
108
+ render(
109
+ <div>
110
+ <div data-testid="outside">Outside</div>
111
+ <ModelSelector />
112
+ </div>,
113
+ );
114
+ fireEvent.click(document.querySelector('.model-selector-btn')!);
115
+ expect(document.querySelector('.model-selector-popover')).toBeInTheDocument();
116
+ fireEvent.mouseDown(screen.getByTestId('outside'));
117
+ expect(document.querySelector('.model-selector-popover')).toBeNull();
118
+ });
119
+
120
+ it('ESC closes popover', () => {
121
+ const session = makeSession('s1', { model: 'gpt-5.6-sol', model_reasoning_effort: 'low' });
122
+ resetStore({ activeSessionId: 's1', sessions: [session] });
123
+ render(<ModelSelector />);
124
+ fireEvent.click(document.querySelector('.model-selector-btn')!);
125
+ expect(document.querySelector('.model-selector-popover')).toBeInTheDocument();
126
+ fireEvent.keyDown(document, { key: 'Escape' });
127
+ expect(document.querySelector('.model-selector-popover')).toBeNull();
128
+ });
129
+ });
@@ -0,0 +1,191 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { useStore } from '../store/store.ts';
3
+ import { PROVIDERS, getModels, getReasoningLevels, getDefaultReasoning } from '../constants/models.ts';
4
+ import { useOutsideClick } from '../hooks/useOutsideClick.ts';
5
+ import { useEscapeKey } from '../hooks/useEscapeKey.ts';
6
+ import { ChevronDown, Check, Lock } from 'lucide-react';
7
+ import type { ModelInfo } from '../types/index.ts';
8
+
9
+ export default function ModelSelector() {
10
+ const { activeSessionId, sessions, updateModelConfig } = useStore();
11
+ const showModelSelector = useStore((s) => s.showModelSelector);
12
+ const setShowModelSelector = useStore((s) => s.setShowModelSelector);
13
+ const session = sessions.find((s) => s.id === activeSessionId);
14
+ const storeModels = useStore((s) => s.models);
15
+ const modelsLoading = useStore((s) => s.modelsLoading);
16
+ const loadModels = useStore((s) => s.loadModels);
17
+
18
+ const [open, setOpen] = useState(false);
19
+ const ref = useRef<HTMLDivElement>(null);
20
+
21
+ useOutsideClick(ref, () => setOpen(false), open);
22
+ useEscapeKey(() => setOpen(false), open);
23
+
24
+ // Open when triggered by /model command
25
+ useEffect(() => {
26
+ if (showModelSelector) {
27
+ setOpen(true);
28
+ setShowModelSelector(false);
29
+ }
30
+ }, [showModelSelector, setShowModelSelector]);
31
+
32
+ // Load dynamic models on mount
33
+ useEffect(() => {
34
+ if (session?.runtime_type) {
35
+ loadModels(session.runtime_type);
36
+ }
37
+ }, [session?.runtime_type, loadModels]);
38
+
39
+ if (!session) return null;
40
+
41
+ const providerId = session.runtime_type;
42
+ const currentModel = session.model ?? '';
43
+ const currentReasoning = session.model_reasoning_effort ?? '';
44
+
45
+ // Use dynamic models from store, fallback to hardcoded
46
+ const dynamicModels: ModelInfo[] = storeModels.length > 0 ? storeModels : [];
47
+ const fallbackModels = getModels(providerId);
48
+ const baseModels = dynamicModels.length > 0 ? dynamicModels : fallbackModels;
49
+
50
+ // Prepend current model if not in the list (e.g. custom or Ollama models)
51
+ const models: ModelInfo[] = [...baseModels];
52
+ if (currentModel && !models.some((m) => m.slug === currentModel)) {
53
+ const ref = fallbackModels[0];
54
+ models.unshift({
55
+ slug: currentModel,
56
+ displayName: currentModel,
57
+ description: 'Custom model',
58
+ defaultReasoning: ref?.defaultReasoning ?? 'medium',
59
+ reasoningLevels: ref?.reasoningLevels ?? [],
60
+ });
61
+ }
62
+
63
+ // Find a model by slug
64
+ function findModel(slug: string): ModelInfo | undefined {
65
+ return models.find((m) => m.slug === slug);
66
+ }
67
+
68
+ // Get reasoning levels for a model
69
+ function levelsFor(slug: string): { effort: string; label: string }[] {
70
+ if (dynamicModels.length > 0) {
71
+ return findModel(slug)?.reasoningLevels ?? [];
72
+ }
73
+ return getReasoningLevels(providerId, slug);
74
+ }
75
+
76
+ // Get default reasoning for a model
77
+ function defaultFor(slug: string): string | undefined {
78
+ if (dynamicModels.length > 0) {
79
+ return findModel(slug)?.defaultReasoning;
80
+ }
81
+ return getDefaultReasoning(providerId, slug);
82
+ }
83
+
84
+ const provider = PROVIDERS.find((p) => p.id === providerId);
85
+ const modelDef = findModel(currentModel);
86
+ const reasoningLevels = currentModel ? levelsFor(currentModel) : [];
87
+
88
+ const providerLabel = provider?.label ?? providerId;
89
+ const modelLabel = modelDef?.displayName ?? (currentModel || 'Default');
90
+ const reasoningDef = reasoningLevels.find((r) => r.effort === currentReasoning);
91
+ const reasoningLabel = reasoningDef?.label ?? (currentReasoning || 'Default');
92
+
93
+ function handleSelectModel(modelSlug: string) {
94
+ const levels = levelsFor(modelSlug);
95
+ const stillSupported = levels.some((r) => r.effort === currentReasoning);
96
+ const newReasoning = stillSupported
97
+ ? currentReasoning
98
+ : (defaultFor(modelSlug) ?? levels[0]?.effort ?? '');
99
+ updateModelConfig(modelSlug, newReasoning);
100
+ setOpen(false);
101
+ }
102
+
103
+ function handleSelectReasoning(effort: string) {
104
+ updateModelConfig(currentModel, effort);
105
+ setOpen(false);
106
+ }
107
+
108
+ return (
109
+ <div className="model-selector" ref={ref}>
110
+ <button className="model-selector-btn" onClick={() => setOpen(!open)} title="Change model and thinking level">
111
+ <span className="model-selector-provider">{providerLabel}</span>
112
+ <span className="model-selector-sep">›</span>
113
+ <span className="model-selector-model">{modelLabel}</span>
114
+ {reasoningLabel !== 'Default' && (
115
+ <>
116
+ <span className="model-selector-sep">›</span>
117
+ <span className="model-selector-reasoning">{reasoningLabel}</span>
118
+ </>
119
+ )}
120
+ <ChevronDown size={12} className="model-selector-chevron" />
121
+ </button>
122
+
123
+ {open && (
124
+ <div className="model-selector-popover">
125
+ {/* Column 1: Provider */}
126
+ <div className="model-selector-column">
127
+ <div className="model-selector-col-header">Provider</div>
128
+ {PROVIDERS.map((p) => (
129
+ <div
130
+ key={p.id}
131
+ className={`model-selector-item ${p.id === providerId ? 'selected' : ''} ${!p.available ? 'disabled' : ''}`}
132
+ onClick={() => {
133
+ if (!p.available) return;
134
+ if (p.id === providerId) { setOpen(false); return; }
135
+ // Switch provider — clear model and reasoning, keep popover open
136
+ updateModelConfig('', '', p.id);
137
+ }}
138
+ >
139
+ <span>{p.label}</span>
140
+ {!p.available && (
141
+ <span className="model-selector-coming-soon">
142
+ <Lock size={10} /> Soon
143
+ </span>
144
+ )}
145
+ {p.id === providerId && <Check size={12} />}
146
+ </div>
147
+ ))}
148
+ </div>
149
+
150
+ {/* Column 2: Model */}
151
+ <div className="model-selector-column">
152
+ <div className="model-selector-col-header">Model</div>
153
+ {modelsLoading && <div className="model-selector-empty">Loading models…</div>}
154
+ {!modelsLoading && models.length === 0 && (
155
+ <div className="model-selector-empty">No models available</div>
156
+ )}
157
+ {models.map((m) => (
158
+ <div
159
+ key={m.slug}
160
+ className={`model-selector-item ${m.slug === currentModel ? 'selected' : ''}`}
161
+ onClick={() => handleSelectModel(m.slug)}
162
+ >
163
+ <div className="model-selector-item-main">
164
+ <span className="model-selector-item-name">{m.displayName}</span>
165
+ <span className="model-selector-item-desc">{m.description}</span>
166
+ </div>
167
+ {m.slug === currentModel && <Check size={12} />}
168
+ </div>
169
+ ))}
170
+ </div>
171
+
172
+ {/* Column 3: Thinking Level */}
173
+ <div className="model-selector-column">
174
+ <div className="model-selector-col-header">Thinking</div>
175
+ {reasoningLevels.length === 0 && <div className="model-selector-empty">Select a model first</div>}
176
+ {reasoningLevels.map((r) => (
177
+ <div
178
+ key={r.effort}
179
+ className={`model-selector-item ${r.effort === currentReasoning ? 'selected' : ''}`}
180
+ onClick={() => handleSelectReasoning(r.effort)}
181
+ >
182
+ <span>{r.label}</span>
183
+ {r.effort === currentReasoning && <Check size={12} />}
184
+ </div>
185
+ ))}
186
+ </div>
187
+ </div>
188
+ )}
189
+ </div>
190
+ );
191
+ }
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
3
+ import NewSessionDialog from './NewSessionDialog.tsx';
4
+ import { resetStore } from '../test/component-helpers.ts';
5
+
6
+ describe('NewSessionDialog', () => {
7
+ beforeEach(() => {
8
+ resetStore();
9
+ });
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+
14
+ it('returns null when showNewSessionDialog is false', () => {
15
+ const { container } = render(<NewSessionDialog />);
16
+ expect(container.firstChild).toBeNull();
17
+ });
18
+
19
+ it('renders form with title, model, and cwd inputs when visible', () => {
20
+ resetStore({ showNewSessionDialog: true });
21
+ render(<NewSessionDialog />);
22
+ expect(screen.getByPlaceholderText('e.g. refactor auth module')).toBeInTheDocument();
23
+ expect(screen.getByPlaceholderText('e.g. /Users/me/projects/my-app')).toBeInTheDocument();
24
+ expect(screen.getByText('Create')).toBeInTheDocument();
25
+ });
26
+
27
+ it('model select shows codex models', () => {
28
+ resetStore({ showNewSessionDialog: true });
29
+ render(<NewSessionDialog />);
30
+ const modelSelect = document.querySelector('select') as HTMLSelectElement;
31
+ // Default option + 4 codex models
32
+ expect(modelSelect.options.length).toBe(5);
33
+ expect(Array.from(modelSelect.options).map((o) => o.textContent)).toEqual(
34
+ expect.arrayContaining(['Default (Codex picks)', 'GPT-5.6-Sol — Flagship — complex, open-ended work']),
35
+ );
36
+ });
37
+
38
+ it('selecting a model shows reasoning levels', () => {
39
+ resetStore({ showNewSessionDialog: true });
40
+ render(<NewSessionDialog />);
41
+ const modelSelect = document.querySelector('select') as HTMLSelectElement;
42
+ fireEvent.change(modelSelect, { target: { value: 'gpt-5.6-sol' } });
43
+ // Reasoning level select should appear (second select)
44
+ const reasoningSelect = document.querySelectorAll('select')[1];
45
+ expect(reasoningSelect).toBeTruthy();
46
+ });
47
+
48
+ it('changing model to one without current reasoning level switches to default', () => {
49
+ resetStore({
50
+ showNewSessionDialog: true,
51
+ lastModelConfig: { provider: 'codex', model: 'gpt-5.6-sol', reasoning: 'ultra' },
52
+ });
53
+ render(<NewSessionDialog />);
54
+ // gpt-5.6-sol has reasoning 'ultra', but gpt-5.5 only goes up to 'xhigh'
55
+ // First select gpt-5.6-sol to get reasoning levels visible
56
+ const selects = document.querySelectorAll('select');
57
+ const modelSelect = selects[0];
58
+ fireEvent.change(modelSelect, { target: { value: 'gpt-5.6-sol' } });
59
+ // Now select gpt-5.5
60
+ fireEvent.change(modelSelect, { target: { value: 'gpt-5.5' } });
61
+ // gpt-5.5 defaultReasoning is 'medium'
62
+ const reasoningSelect = document.querySelectorAll('select')[1] as HTMLSelectElement;
63
+ expect(reasoningSelect.value).toBe('medium');
64
+ });
65
+
66
+ it('Create button calls createSession with form values', async () => {
67
+ const createSession = vi.fn().mockResolvedValue(undefined);
68
+ resetStore({ showNewSessionDialog: true, createSession });
69
+ render(<NewSessionDialog />);
70
+ const titleInput = screen.getByPlaceholderText('e.g. refactor auth module');
71
+ fireEvent.change(titleInput, { target: { value: 'My Session' } });
72
+ fireEvent.click(screen.getByText('Create'));
73
+ expect(createSession).toHaveBeenCalledWith(expect.objectContaining({
74
+ title: 'My Session',
75
+ runtime_type: 'codex',
76
+ }));
77
+ });
78
+
79
+ it('ESC closes dialog', () => {
80
+ const setShowNewSessionDialog = vi.fn();
81
+ resetStore({ showNewSessionDialog: true, setShowNewSessionDialog });
82
+ render(<NewSessionDialog />);
83
+ fireEvent.keyDown(document.querySelector('.dialog')!, { key: 'Escape' });
84
+ expect(setShowNewSessionDialog).toHaveBeenCalledWith(false);
85
+ });
86
+
87
+ it('overlay click closes dialog', () => {
88
+ const setShowNewSessionDialog = vi.fn();
89
+ resetStore({ showNewSessionDialog: true, setShowNewSessionDialog });
90
+ render(<NewSessionDialog />);
91
+ const overlay = document.querySelector('.dialog-overlay')!;
92
+ fireEvent.click(overlay);
93
+ expect(setShowNewSessionDialog).toHaveBeenCalledWith(false);
94
+ });
95
+ });
@@ -1,24 +1,34 @@
1
1
  import { useState } from 'react';
2
2
  import { useStore } from '../store/store.ts';
3
3
  import { X } from 'lucide-react';
4
+ import { getModels, getReasoningLevels, getDefaultReasoning } from '../constants/models.ts';
4
5
 
5
6
  export default function NewSessionDialog() {
6
- const { showNewSessionDialog, setShowNewSessionDialog, createSession } = useStore();
7
+ const { showNewSessionDialog, setShowNewSessionDialog, createSession, lastModelConfig } = useStore();
8
+
9
+ const initialProvider = lastModelConfig?.provider ?? 'codex';
10
+ const initialModel = lastModelConfig?.model ?? '';
11
+ const initialReasoning = lastModelConfig?.reasoning ?? '';
12
+
7
13
  const [title, setTitle] = useState('');
8
- const [model, setModel] = useState('');
14
+ const [model, setModel] = useState(initialModel);
15
+ const [reasoning, setReasoning] = useState(initialReasoning);
9
16
  const [cwd, setCwd] = useState('');
10
17
 
11
18
  if (!showNewSessionDialog) return null;
12
19
 
20
+ const models = getModels(initialProvider);
21
+ const reasoningLevels = model ? getReasoningLevels(initialProvider, model) : [];
22
+
13
23
  function handleSubmit() {
14
24
  createSession({
15
25
  title: title.trim() || undefined,
16
- model: model.trim() || undefined,
26
+ model: model || undefined,
27
+ model_reasoning_effort: reasoning || undefined,
17
28
  cwd: cwd.trim() || undefined,
18
- runtime_type: 'codex',
29
+ runtime_type: initialProvider,
19
30
  });
20
31
  setTitle('');
21
- setModel('');
22
32
  setCwd('');
23
33
  }
24
34
 
@@ -28,6 +38,16 @@ export default function NewSessionDialog() {
28
38
  }
29
39
  }
30
40
 
41
+ function handleModelChange(slug: string) {
42
+ setModel(slug);
43
+ // If current reasoning is not supported by new model, switch to default
44
+ const levels = getReasoningLevels(initialProvider, slug);
45
+ const stillSupported = levels.some((r) => r.effort === reasoning);
46
+ if (!stillSupported) {
47
+ setReasoning(getDefaultReasoning(initialProvider, slug) ?? levels[0]?.effort ?? '');
48
+ }
49
+ }
50
+
31
51
  return (
32
52
  <div className="dialog-overlay" onClick={() => setShowNewSessionDialog(false)}>
33
53
  <div className="dialog" onClick={(e) => e.stopPropagation()} onKeyDown={handleKeyDown}>
@@ -49,16 +69,41 @@ export default function NewSessionDialog() {
49
69
  autoFocus
50
70
  />
51
71
  </label>
72
+
52
73
  <label className="dialog-label">
53
- Model (optional)
54
- <input
55
- className="dialog-input"
56
- type="text"
74
+ Model
75
+ <select
76
+ className="dialog-input dialog-select"
57
77
  value={model}
58
- onChange={(e) => setModel(e.target.value)}
59
- placeholder="e.g. gpt-5 (default: Codex picks)"
60
- />
78
+ onChange={(e) => handleModelChange(e.target.value)}
79
+ >
80
+ <option value="">Default (Codex picks)</option>
81
+ {models.map((m) => (
82
+ <option key={m.slug} value={m.slug}>
83
+ {m.displayName} — {m.description}
84
+ </option>
85
+ ))}
86
+ </select>
61
87
  </label>
88
+
89
+ {reasoningLevels.length > 0 && (
90
+ <label className="dialog-label">
91
+ Thinking Level
92
+ <select
93
+ className="dialog-input dialog-select"
94
+ value={reasoning}
95
+ onChange={(e) => setReasoning(e.target.value)}
96
+ >
97
+ <option value="">Default ({getDefaultReasoning(initialProvider, model)})</option>
98
+ {reasoningLevels.map((r) => (
99
+ <option key={r.effort} value={r.effort}>
100
+ {r.label}
101
+ </option>
102
+ ))}
103
+ </select>
104
+ </label>
105
+ )}
106
+
62
107
  <label className="dialog-label">
63
108
  Working Directory (optional)
64
109
  <input
@@ -81,4 +126,4 @@ export default function NewSessionDialog() {
81
126
  </div>
82
127
  </div>
83
128
  );
84
- }
129
+ }