bingocode 1.0.2 → 1.0.3

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 (186) hide show
  1. package/desktop/README.md +30 -0
  2. package/desktop/bunfig.toml +1 -0
  3. package/desktop/index.html +17 -0
  4. package/desktop/package.json +55 -0
  5. package/desktop/pnpm-lock.yaml +3832 -0
  6. package/desktop/public/app-icon.jpg +0 -0
  7. package/desktop/public/fonts/inter-latin-ext.woff2 +0 -0
  8. package/desktop/public/fonts/inter-latin.woff2 +0 -0
  9. package/desktop/public/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  10. package/desktop/public/fonts/jetbrains-mono-latin.woff2 +0 -0
  11. package/desktop/public/fonts/manrope-latin-ext.woff2 +0 -0
  12. package/desktop/public/fonts/manrope-latin.woff2 +0 -0
  13. package/desktop/public/fonts/material-symbols-outlined.woff2 +0 -0
  14. package/desktop/public/icons/bilibili.svg +1 -0
  15. package/desktop/public/icons/douyin.svg +1 -0
  16. package/desktop/public/icons/github.svg +3 -0
  17. package/desktop/public/icons/xiaohongshu.svg +1 -0
  18. package/desktop/scripts/build-macos-arm64.sh +270 -0
  19. package/desktop/scripts/build-sidecars.ts +183 -0
  20. package/desktop/scripts/build-windows-x64.ps1 +295 -0
  21. package/desktop/scripts/scan-missing-imports.ts +235 -0
  22. package/desktop/sidecars/claude-sidecar.ts +156 -0
  23. package/desktop/src/App.tsx +5 -0
  24. package/desktop/src/__tests__/agentsSettings.test.tsx +349 -0
  25. package/desktop/src/__tests__/pages.test.tsx +290 -0
  26. package/desktop/src/__tests__/skillsSettings.test.tsx +205 -0
  27. package/desktop/src/api/adapters.ts +12 -0
  28. package/desktop/src/api/agents.ts +36 -0
  29. package/desktop/src/api/cliTasks.ts +28 -0
  30. package/desktop/src/api/client.ts +63 -0
  31. package/desktop/src/api/computerUse.ts +76 -0
  32. package/desktop/src/api/filesystem.ts +30 -0
  33. package/desktop/src/api/hahaOAuth.ts +38 -0
  34. package/desktop/src/api/models.ts +28 -0
  35. package/desktop/src/api/providers.ts +63 -0
  36. package/desktop/src/api/search.ts +29 -0
  37. package/desktop/src/api/sessions.ts +56 -0
  38. package/desktop/src/api/settings.ts +20 -0
  39. package/desktop/src/api/skills.ts +19 -0
  40. package/desktop/src/api/tasks.ts +36 -0
  41. package/desktop/src/api/teams.ts +44 -0
  42. package/desktop/src/api/websocket.ts +164 -0
  43. package/desktop/src/components/chat/AskUserQuestion.tsx +268 -0
  44. package/desktop/src/components/chat/AssistantMessage.tsx +29 -0
  45. package/desktop/src/components/chat/AttachmentGallery.tsx +113 -0
  46. package/desktop/src/components/chat/ChatInput.tsx +622 -0
  47. package/desktop/src/components/chat/CodeViewer.tsx +161 -0
  48. package/desktop/src/components/chat/ComputerUsePermissionModal.test.tsx +174 -0
  49. package/desktop/src/components/chat/ComputerUsePermissionModal.tsx +311 -0
  50. package/desktop/src/components/chat/DiffViewer.tsx +157 -0
  51. package/desktop/src/components/chat/FileSearchMenu.tsx +198 -0
  52. package/desktop/src/components/chat/ImageGalleryModal.tsx +91 -0
  53. package/desktop/src/components/chat/InlineImageGallery.tsx +106 -0
  54. package/desktop/src/components/chat/InlineTaskSummary.tsx +60 -0
  55. package/desktop/src/components/chat/MermaidRenderer.test.tsx +98 -0
  56. package/desktop/src/components/chat/MermaidRenderer.tsx +361 -0
  57. package/desktop/src/components/chat/MessageActionBar.tsx +27 -0
  58. package/desktop/src/components/chat/MessageList.test.tsx +313 -0
  59. package/desktop/src/components/chat/MessageList.tsx +249 -0
  60. package/desktop/src/components/chat/PermissionDialog.tsx +262 -0
  61. package/desktop/src/components/chat/SessionTaskBar.test.tsx +99 -0
  62. package/desktop/src/components/chat/SessionTaskBar.tsx +159 -0
  63. package/desktop/src/components/chat/StreamingIndicator.tsx +41 -0
  64. package/desktop/src/components/chat/TerminalChrome.tsx +35 -0
  65. package/desktop/src/components/chat/ThinkingBlock.tsx +87 -0
  66. package/desktop/src/components/chat/ToolCallBlock.tsx +247 -0
  67. package/desktop/src/components/chat/ToolCallGroup.tsx +617 -0
  68. package/desktop/src/components/chat/ToolResultBlock.tsx +107 -0
  69. package/desktop/src/components/chat/UserMessage.tsx +38 -0
  70. package/desktop/src/components/chat/chatBlocks.test.tsx +136 -0
  71. package/desktop/src/components/chat/clipboard.ts +25 -0
  72. package/desktop/src/components/chat/composerUtils.test.ts +55 -0
  73. package/desktop/src/components/chat/composerUtils.ts +149 -0
  74. package/desktop/src/components/controls/ModelSelector.tsx +156 -0
  75. package/desktop/src/components/controls/PermissionModeSelector.tsx +229 -0
  76. package/desktop/src/components/layout/AppShell.tsx +107 -0
  77. package/desktop/src/components/layout/ContentRouter.tsx +27 -0
  78. package/desktop/src/components/layout/ProjectFilter.tsx +126 -0
  79. package/desktop/src/components/layout/Sidebar.test.tsx +158 -0
  80. package/desktop/src/components/layout/Sidebar.tsx +384 -0
  81. package/desktop/src/components/layout/StatusBar.tsx +31 -0
  82. package/desktop/src/components/layout/TabBar.test.tsx +136 -0
  83. package/desktop/src/components/layout/TabBar.tsx +318 -0
  84. package/desktop/src/components/layout/TitleBar.tsx +96 -0
  85. package/desktop/src/components/layout/WindowControls.test.tsx +69 -0
  86. package/desktop/src/components/layout/WindowControls.tsx +89 -0
  87. package/desktop/src/components/markdown/MarkdownRenderer.test.tsx +100 -0
  88. package/desktop/src/components/markdown/MarkdownRenderer.tsx +229 -0
  89. package/desktop/src/components/settings/ClaudeOfficialLogin.tsx +107 -0
  90. package/desktop/src/components/shared/Button.tsx +63 -0
  91. package/desktop/src/components/shared/CopyButton.tsx +58 -0
  92. package/desktop/src/components/shared/DirectoryPicker.tsx +316 -0
  93. package/desktop/src/components/shared/Dropdown.tsx +91 -0
  94. package/desktop/src/components/shared/Input.tsx +38 -0
  95. package/desktop/src/components/shared/Modal.tsx +65 -0
  96. package/desktop/src/components/shared/ProjectContextChip.tsx +30 -0
  97. package/desktop/src/components/shared/Spinner.tsx +30 -0
  98. package/desktop/src/components/shared/Textarea.tsx +38 -0
  99. package/desktop/src/components/shared/Toast.tsx +47 -0
  100. package/desktop/src/components/shared/UpdateChecker.tsx +90 -0
  101. package/desktop/src/components/skills/SkillDetail.test.tsx +89 -0
  102. package/desktop/src/components/skills/SkillDetail.tsx +403 -0
  103. package/desktop/src/components/skills/SkillList.tsx +254 -0
  104. package/desktop/src/components/tasks/DayOfWeekPicker.tsx +57 -0
  105. package/desktop/src/components/tasks/NewTaskModal.tsx +407 -0
  106. package/desktop/src/components/tasks/PromptEditor.tsx +74 -0
  107. package/desktop/src/components/tasks/TaskEmptyState.tsx +30 -0
  108. package/desktop/src/components/tasks/TaskList.tsx +46 -0
  109. package/desktop/src/components/tasks/TaskRow.tsx +253 -0
  110. package/desktop/src/components/tasks/TaskRunsPanel.tsx +195 -0
  111. package/desktop/src/components/teams/TeamStatusBar.tsx +147 -0
  112. package/desktop/src/config/providerPresets.ts +78 -0
  113. package/desktop/src/config/spinnerVerbs.ts +193 -0
  114. package/desktop/src/hooks/useKeyboardShortcuts.ts +60 -0
  115. package/desktop/src/i18n/index.ts +54 -0
  116. package/desktop/src/i18n/locales/en.ts +670 -0
  117. package/desktop/src/i18n/locales/zh.ts +670 -0
  118. package/desktop/src/lib/__tests__/cronDescribe.test.ts +93 -0
  119. package/desktop/src/lib/cronDescribe.ts +188 -0
  120. package/desktop/src/lib/desktopRuntime.ts +54 -0
  121. package/desktop/src/lib/parseRunOutput.ts +79 -0
  122. package/desktop/src/main.tsx +13 -0
  123. package/desktop/src/mocks/data.ts +202 -0
  124. package/desktop/src/pages/ActiveSession.test.tsx +181 -0
  125. package/desktop/src/pages/ActiveSession.tsx +219 -0
  126. package/desktop/src/pages/AdapterSettings.tsx +375 -0
  127. package/desktop/src/pages/AgentTeams.tsx +200 -0
  128. package/desktop/src/pages/ComputerUseSettings.tsx +420 -0
  129. package/desktop/src/pages/EmptySession.tsx +518 -0
  130. package/desktop/src/pages/NewTaskModal.tsx +346 -0
  131. package/desktop/src/pages/ScheduledTasks.tsx +66 -0
  132. package/desktop/src/pages/ScheduledTasksEmpty.tsx +152 -0
  133. package/desktop/src/pages/ScheduledTasksList.tsx +416 -0
  134. package/desktop/src/pages/SessionControls.tsx +460 -0
  135. package/desktop/src/pages/Settings.tsx +1448 -0
  136. package/desktop/src/pages/ToolInspection.tsx +235 -0
  137. package/desktop/src/stores/adapterStore.ts +106 -0
  138. package/desktop/src/stores/agentStore.ts +34 -0
  139. package/desktop/src/stores/chatStore.test.ts +505 -0
  140. package/desktop/src/stores/chatStore.ts +850 -0
  141. package/desktop/src/stores/cliTaskStore.ts +152 -0
  142. package/desktop/src/stores/hahaOAuthStore.test.ts +77 -0
  143. package/desktop/src/stores/hahaOAuthStore.ts +97 -0
  144. package/desktop/src/stores/providerStore.ts +101 -0
  145. package/desktop/src/stores/sessionStore.test.ts +63 -0
  146. package/desktop/src/stores/sessionStore.ts +102 -0
  147. package/desktop/src/stores/settingsStore.ts +120 -0
  148. package/desktop/src/stores/skillStore.ts +51 -0
  149. package/desktop/src/stores/tabStore.ts +169 -0
  150. package/desktop/src/stores/taskStore.ts +68 -0
  151. package/desktop/src/stores/teamStore.ts +344 -0
  152. package/desktop/src/stores/uiStore.ts +100 -0
  153. package/desktop/src/stores/updateStore.test.ts +71 -0
  154. package/desktop/src/stores/updateStore.ts +221 -0
  155. package/desktop/src/theme/globals.css +465 -0
  156. package/desktop/src/types/adapter.ts +33 -0
  157. package/desktop/src/types/chat.ts +152 -0
  158. package/desktop/src/types/cliTask.ts +24 -0
  159. package/desktop/src/types/provider.ts +62 -0
  160. package/desktop/src/types/session.ts +27 -0
  161. package/desktop/src/types/settings.ts +22 -0
  162. package/desktop/src/types/skill.ts +38 -0
  163. package/desktop/src/types/task.ts +56 -0
  164. package/desktop/src/types/team.ts +38 -0
  165. package/desktop/src-tauri/Cargo.lock +5549 -0
  166. package/desktop/src-tauri/Cargo.toml +20 -0
  167. package/desktop/src-tauri/app-icon.svg +13 -0
  168. package/desktop/src-tauri/build.rs +3 -0
  169. package/desktop/src-tauri/capabilities/default.json +106 -0
  170. package/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  171. package/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +4 -0
  172. package/desktop/src-tauri/icons/icon.icns +0 -0
  173. package/desktop/src-tauri/icons/icon.ico +0 -0
  174. package/desktop/src-tauri/src/lib.rs +408 -0
  175. package/desktop/src-tauri/src/main.rs +6 -0
  176. package/desktop/src-tauri/tauri.conf.json +78 -0
  177. package/desktop/src-tauri/tauri.macos.conf.json +18 -0
  178. package/desktop/src-tauri/tauri.release-ci.json +5 -0
  179. package/desktop/src-tauri/tauri.windows.conf.json +16 -0
  180. package/desktop/src-tauri/windows-installer-hooks.nsh +17 -0
  181. package/desktop/tsconfig.json +25 -0
  182. package/desktop/vite.config.ts +26 -0
  183. package/desktop/vitest.config.ts +18 -0
  184. package/package.json +1 -1
  185. package/src/commands/desktop/desktop.tsx +9 -0
  186. package/src/commands/desktop/index.ts +26 -0
@@ -0,0 +1,349 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import '@testing-library/jest-dom'
4
+
5
+ import { Settings } from '../pages/Settings'
6
+ import { useAgentStore } from '../stores/agentStore'
7
+ import { useSkillStore } from '../stores/skillStore'
8
+ import { useSettingsStore } from '../stores/settingsStore'
9
+ import { useSessionStore } from '../stores/sessionStore'
10
+ import { SETTINGS_TAB_ID, useTabStore } from '../stores/tabStore'
11
+
12
+ vi.mock('../api/agents', () => ({
13
+ agentsApi: {
14
+ list: vi.fn().mockResolvedValue({ activeAgents: [], allAgents: [] }),
15
+ },
16
+ }))
17
+
18
+ const noopFetch = vi.fn()
19
+
20
+ vi.mock('../stores/providerStore', () => ({
21
+ useProviderStore: () => ({
22
+ providers: [],
23
+ activeId: null,
24
+ isLoading: false,
25
+ fetchProviders: vi.fn(),
26
+ deleteProvider: vi.fn(),
27
+ activateProvider: vi.fn(),
28
+ activateOfficial: vi.fn(),
29
+ testProvider: vi.fn(),
30
+ createProvider: vi.fn(),
31
+ updateProvider: vi.fn(),
32
+ testConfig: vi.fn(),
33
+ }),
34
+ }))
35
+
36
+ vi.mock('../pages/AdapterSettings', () => ({
37
+ AdapterSettings: () => <div>Adapter Settings Mock</div>,
38
+ }))
39
+
40
+ vi.mock('../components/chat/CodeViewer', () => ({
41
+ CodeViewer: ({ code }: { code: string }) => <pre data-testid="code-viewer">{code}</pre>,
42
+ }))
43
+
44
+ const MOCK_AGENTS = [
45
+ {
46
+ agentType: 'code-reviewer',
47
+ description: 'Reviews code for quality and security',
48
+ model: 'claude-sonnet-4-6',
49
+ modelDisplay: 'claude-sonnet-4-6',
50
+ tools: ['Read', 'Grep', 'Glob'],
51
+ systemPrompt: '# Code Reviewer\n\nYou are an expert code reviewer.',
52
+ color: 'blue',
53
+ source: 'userSettings' as const,
54
+ baseDir: '~/.claude/agents',
55
+ isActive: true,
56
+ },
57
+ {
58
+ agentType: 'doc-writer',
59
+ description: 'Writes technical documentation',
60
+ model: 'claude-haiku-4-5',
61
+ modelDisplay: 'claude-haiku-4-5',
62
+ tools: ['Read'],
63
+ systemPrompt: 'You write clear and concise docs.',
64
+ color: 'green',
65
+ source: 'built-in' as const,
66
+ baseDir: 'built-in',
67
+ isActive: true,
68
+ },
69
+ {
70
+ agentType: 'plain-agent',
71
+ description: undefined,
72
+ model: undefined,
73
+ modelDisplay: 'inherit',
74
+ tools: undefined,
75
+ systemPrompt: undefined,
76
+ color: undefined,
77
+ source: 'projectSettings' as const,
78
+ baseDir: '/workspace/project/.claude/agents',
79
+ isActive: false,
80
+ overriddenBy: 'userSettings' as const,
81
+ },
82
+ ]
83
+
84
+ const MOCK_SKILL_DETAIL = {
85
+ meta: {
86
+ name: 'skill-docs',
87
+ displayName: 'Skill Docs',
88
+ description: 'A rich skill readme',
89
+ source: 'user' as const,
90
+ userInvocable: true,
91
+ contentLength: 200,
92
+ hasDirectory: true,
93
+ },
94
+ tree: [
95
+ { name: 'SKILL.md', path: 'SKILL.md', type: 'file' as const },
96
+ { name: 'helper.ts', path: 'helper.ts', type: 'file' as const },
97
+ ],
98
+ files: [
99
+ {
100
+ path: 'SKILL.md',
101
+ language: 'markdown',
102
+ content: '# Heading\n\nParagraph with `inline code`.\n\n## Section\n\n- First item\n- Second item\n\n> Helpful quote',
103
+ body: '# Heading\n\nParagraph with `inline code`.\n\n## Section\n\n- First item\n- Second item\n\n> Helpful quote',
104
+ isEntry: true,
105
+ frontmatter: {
106
+ description: 'A rich skill readme',
107
+ model: 'sonnet',
108
+ },
109
+ },
110
+ {
111
+ path: 'helper.ts',
112
+ language: 'typescript',
113
+ content: 'export const helper = true',
114
+ isEntry: false,
115
+ },
116
+ ],
117
+ skillRoot: '/tmp/skill-docs',
118
+ }
119
+
120
+ function switchToAgentsTab() {
121
+ fireEvent.click(screen.getByText('Agents'))
122
+ }
123
+
124
+ function switchToSkillsTab() {
125
+ fireEvent.click(screen.getByText('Skills'))
126
+ }
127
+
128
+ describe('Settings > Agents tab', () => {
129
+ beforeEach(() => {
130
+ useSettingsStore.setState({ locale: 'en' })
131
+ useTabStore.setState({
132
+ activeTabId: 'session-1',
133
+ tabs: [{ sessionId: 'session-1', title: 'Test', type: 'session', status: 'idle' }],
134
+ })
135
+ useSessionStore.setState({
136
+ sessions: [
137
+ {
138
+ id: 'session-1',
139
+ title: 'Test Session',
140
+ createdAt: '',
141
+ modifiedAt: '',
142
+ messageCount: 0,
143
+ projectPath: '/workspace/project',
144
+ workDir: '/workspace/project',
145
+ workDirExists: true,
146
+ },
147
+ ],
148
+ activeSessionId: 'session-1',
149
+ isLoading: false,
150
+ error: null,
151
+ selectedProjects: [],
152
+ availableProjects: [],
153
+ fetchSessions: noopFetch,
154
+ createSession: vi.fn(),
155
+ deleteSession: vi.fn(),
156
+ renameSession: vi.fn(),
157
+ updateSessionTitle: vi.fn(),
158
+ setActiveSession: vi.fn(),
159
+ setSelectedProjects: vi.fn(),
160
+ })
161
+ useAgentStore.setState({
162
+ activeAgents: [],
163
+ allAgents: [],
164
+ isLoading: false,
165
+ error: null,
166
+ selectedAgent: null,
167
+ fetchAgents: noopFetch,
168
+ selectAgent: (agent) => useAgentStore.setState({ selectedAgent: agent }),
169
+ })
170
+ useSkillStore.setState({
171
+ skills: [],
172
+ selectedSkill: null,
173
+ isLoading: false,
174
+ isDetailLoading: false,
175
+ error: null,
176
+ fetchSkills: noopFetch,
177
+ fetchSkillDetail: noopFetch,
178
+ clearSelection: () => useSkillStore.setState({ selectedSkill: null }),
179
+ })
180
+ })
181
+
182
+ it('renders the Agents tab button in sidebar', () => {
183
+ render(<Settings />)
184
+ expect(screen.getByText('Agents')).toBeInTheDocument()
185
+ })
186
+
187
+ it('shows loading spinner when fetching agents', () => {
188
+ useAgentStore.setState({ isLoading: true, allAgents: [], activeAgents: [], fetchAgents: noopFetch })
189
+ render(<Settings />)
190
+ switchToAgentsTab()
191
+
192
+ const spinner = document.querySelector('.animate-spin')
193
+ expect(spinner).toBeInTheDocument()
194
+ })
195
+
196
+ it('uses the active session workDir even when settings tab is focused', async () => {
197
+ const fetchAgents = vi.fn()
198
+ useAgentStore.setState({
199
+ allAgents: [],
200
+ activeAgents: [],
201
+ isLoading: false,
202
+ fetchAgents,
203
+ })
204
+ useTabStore.setState({
205
+ activeTabId: SETTINGS_TAB_ID,
206
+ tabs: [{ sessionId: SETTINGS_TAB_ID, title: 'Settings', type: 'settings', status: 'idle' }],
207
+ })
208
+
209
+ render(<Settings />)
210
+ switchToAgentsTab()
211
+
212
+ expect(fetchAgents).toHaveBeenCalledWith('/workspace/project')
213
+ })
214
+
215
+ it('shows error state with retry button when API fails', () => {
216
+ useAgentStore.setState({ allAgents: [], activeAgents: [], isLoading: false, error: 'Network error', fetchAgents: noopFetch })
217
+ render(<Settings />)
218
+ switchToAgentsTab()
219
+
220
+ expect(screen.getByText('Network error')).toBeInTheDocument()
221
+ expect(screen.getByText('Retry')).toBeInTheDocument()
222
+ })
223
+
224
+ it('renders grouped agent browser with source sections', () => {
225
+ useAgentStore.setState({
226
+ allAgents: MOCK_AGENTS,
227
+ activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive),
228
+ isLoading: false,
229
+ fetchAgents: noopFetch,
230
+ })
231
+ render(<Settings />)
232
+ switchToAgentsTab()
233
+
234
+ expect(screen.getByText('Browse installed agents')).toBeInTheDocument()
235
+ expect(screen.getByText('Agent Browser')).toBeInTheDocument()
236
+ expect(screen.getAllByText('User').length).toBeGreaterThan(0)
237
+ expect(screen.getAllByText('Built-in').length).toBeGreaterThan(0)
238
+ expect(screen.getAllByText('Project').length).toBeGreaterThan(0)
239
+ expect(screen.getByText('code-reviewer')).toBeInTheDocument()
240
+ expect(screen.getByText('Writes technical documentation')).toBeInTheDocument()
241
+ expect(screen.getByText('Overridden by User')).toBeInTheDocument()
242
+ })
243
+
244
+ it('opens agent detail with metadata cards and document prompt', () => {
245
+ useAgentStore.setState({
246
+ allAgents: MOCK_AGENTS,
247
+ activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive),
248
+ isLoading: false,
249
+ fetchAgents: noopFetch,
250
+ })
251
+ render(<Settings />)
252
+ switchToAgentsTab()
253
+
254
+ fireEvent.click(screen.getByText('code-reviewer'))
255
+
256
+ expect(screen.getByText('Back to list')).toBeInTheDocument()
257
+ expect(screen.getByText('Agent Profile')).toBeInTheDocument()
258
+ expect(screen.getAllByText('claude-sonnet-4-6')[0]).toBeInTheDocument()
259
+ expect(screen.getByText('Read')).toBeInTheDocument()
260
+ expect(screen.getByRole('heading', { name: 'Code Reviewer' })).toBeInTheDocument()
261
+
262
+ const rendererRoot = screen.getByRole('heading', { name: 'Code Reviewer' }).closest('div[class*="prose"]')
263
+ expect(rendererRoot?.className).toContain('max-w-[72ch]')
264
+ })
265
+
266
+ it('shows no system prompt state when agent has no prompt', () => {
267
+ useAgentStore.setState({
268
+ allAgents: MOCK_AGENTS,
269
+ activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive),
270
+ isLoading: false,
271
+ fetchAgents: noopFetch,
272
+ })
273
+ render(<Settings />)
274
+ switchToAgentsTab()
275
+
276
+ fireEvent.click(screen.getByText('plain-agent'))
277
+
278
+ expect(screen.getByText('No system prompt defined.')).toBeInTheDocument()
279
+ expect(screen.getByText('shadowed by User')).toBeInTheDocument()
280
+ })
281
+
282
+ it('navigates back to list from detail view', () => {
283
+ useAgentStore.setState({
284
+ allAgents: MOCK_AGENTS,
285
+ activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive),
286
+ isLoading: false,
287
+ fetchAgents: noopFetch,
288
+ })
289
+ render(<Settings />)
290
+ switchToAgentsTab()
291
+
292
+ fireEvent.click(screen.getByText('code-reviewer'))
293
+ fireEvent.click(screen.getByText('Back to list'))
294
+
295
+ expect(screen.getByText('code-reviewer')).toBeInTheDocument()
296
+ expect(screen.getByText('doc-writer')).toBeInTheDocument()
297
+ expect(screen.getByText('plain-agent')).toBeInTheDocument()
298
+ })
299
+ })
300
+
301
+ describe('Settings > Skills tab', () => {
302
+ beforeEach(() => {
303
+ useSettingsStore.setState({ locale: 'en' })
304
+ useSkillStore.setState({
305
+ skills: [],
306
+ selectedSkill: null,
307
+ isLoading: false,
308
+ isDetailLoading: false,
309
+ error: null,
310
+ fetchSkills: noopFetch,
311
+ fetchSkillDetail: noopFetch,
312
+ clearSelection: () => useSkillStore.setState({ selectedSkill: null }),
313
+ })
314
+ })
315
+
316
+ it('renders markdown skills with document styling in detail view', () => {
317
+ useSkillStore.setState({
318
+ selectedSkill: MOCK_SKILL_DETAIL,
319
+ clearSelection: () => useSkillStore.setState({ selectedSkill: null }),
320
+ })
321
+
322
+ render(<Settings />)
323
+ switchToSkillsTab()
324
+
325
+ expect(screen.getByText('Skill metadata')).toBeInTheDocument()
326
+ expect(screen.getByRole('heading', { name: 'Heading' })).toBeInTheDocument()
327
+
328
+ const rendererRoot = screen.getByRole('heading', { name: 'Heading' }).closest('div[class*="prose"]')
329
+ expect(rendererRoot?.className).toContain('max-w-[72ch]')
330
+ expect(rendererRoot?.className).toContain('prose-h2:border-b')
331
+ expect(rendererRoot?.className).toContain('prose-p:text-[15px]')
332
+ expect(screen.getByText('Helpful quote')).toBeInTheDocument()
333
+ })
334
+
335
+ it('keeps code files rendered in CodeViewer instead of markdown prose', () => {
336
+ useSkillStore.setState({
337
+ selectedSkill: MOCK_SKILL_DETAIL,
338
+ clearSelection: () => useSkillStore.setState({ selectedSkill: null }),
339
+ })
340
+
341
+ render(<Settings />)
342
+ switchToSkillsTab()
343
+
344
+ fireEvent.click(screen.getAllByText('helper.ts')[0]!)
345
+
346
+ expect(screen.getByTestId('code-viewer')).toHaveTextContent('export const helper = true')
347
+ expect(screen.queryByRole('heading', { name: 'Heading' })).not.toBeInTheDocument()
348
+ })
349
+ })
@@ -0,0 +1,290 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { fireEvent, render, screen } from '@testing-library/react'
3
+ import '@testing-library/jest-dom'
4
+
5
+ import { skillsApi } from '../api/skills'
6
+
7
+ vi.mock('../api/skills', () => ({
8
+ skillsApi: {
9
+ list: vi.fn(async () => ({ skills: [] })),
10
+ },
11
+ }))
12
+
13
+ // Import all pages
14
+ import { EmptySession } from '../pages/EmptySession'
15
+ import { ActiveSession } from '../pages/ActiveSession'
16
+ import { AgentTeams } from '../pages/AgentTeams'
17
+ import { ScheduledTasks } from '../pages/ScheduledTasks'
18
+ import { ToolInspection } from '../pages/ToolInspection'
19
+
20
+ // Layout components (chrome is now here, not in pages)
21
+ import { Sidebar } from '../components/layout/Sidebar'
22
+ import { UserMessage } from '../components/chat/UserMessage'
23
+ import { useChatStore } from '../stores/chatStore'
24
+ import { useSessionStore } from '../stores/sessionStore'
25
+ import { useTabStore } from '../stores/tabStore'
26
+
27
+ /**
28
+ * Core rendering tests: content-only pages must render without crashing
29
+ * and contain key structural elements from the prototype.
30
+ */
31
+ describe('Content-only pages render without errors', () => {
32
+ it('EmptySession slash picker includes dynamic skills before the first session starts', async () => {
33
+ vi.mocked(skillsApi.list).mockResolvedValueOnce({
34
+ skills: [
35
+ {
36
+ name: 'lark-mail',
37
+ description: 'Draft, send, and search emails',
38
+ source: 'user',
39
+ userInvocable: true,
40
+ contentLength: 120,
41
+ hasDirectory: true,
42
+ },
43
+ {
44
+ name: 'internal-only',
45
+ description: 'Should stay hidden',
46
+ source: 'user',
47
+ userInvocable: false,
48
+ contentLength: 60,
49
+ hasDirectory: true,
50
+ },
51
+ ],
52
+ })
53
+
54
+ render(<EmptySession />)
55
+
56
+ fireEvent.change(screen.getByRole('textbox'), {
57
+ target: { value: '/', selectionStart: 1 },
58
+ })
59
+
60
+ expect(await screen.findByText('/lark-mail')).toBeInTheDocument()
61
+ expect(screen.queryByText('/internal-only')).not.toBeInTheDocument()
62
+ })
63
+
64
+ it('EmptySession renders mascot and composer', () => {
65
+ const { container } = render(<EmptySession />)
66
+ expect(container.querySelector('textarea')).toBeInTheDocument()
67
+ expect(container.innerHTML).toContain('New session')
68
+ expect(container.innerHTML).toContain('Ask anything')
69
+ })
70
+
71
+ it('EmptySession plus menu exposes uploads and slash commands before chat starts', () => {
72
+ render(<EmptySession />)
73
+ fireEvent.click(screen.getByRole('button', { name: 'Open composer tools' }))
74
+ expect(screen.getByText('Add files or photos')).toBeInTheDocument()
75
+ expect(screen.getByText('Slash commands')).toBeInTheDocument()
76
+ })
77
+
78
+ it('ActiveSession renders with chat components', () => {
79
+ const SESSION_ID = 'test-active-session'
80
+ useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID })
81
+ useChatStore.setState({
82
+ sessions: {
83
+ [SESSION_ID]: {
84
+ messages: [],
85
+ chatState: 'idle',
86
+ connectionState: 'connected',
87
+ streamingText: '',
88
+ streamingToolInput: '',
89
+ activeToolUseId: null,
90
+ activeToolName: null,
91
+ activeThinkingId: null,
92
+ pendingPermission: null,
93
+ pendingComputerUsePermission: null,
94
+ tokenUsage: { input_tokens: 0, output_tokens: 0 },
95
+ elapsedSeconds: 0,
96
+ statusVerb: '',
97
+ slashCommands: [],
98
+ agentTaskNotifications: {},
99
+ elapsedTimer: null,
100
+ },
101
+ },
102
+ })
103
+ const { container } = render(<ActiveSession />)
104
+ // With empty messages, the hero is shown
105
+ expect(container.innerHTML).toContain('New session')
106
+ // ChatInput has a textarea
107
+ const textarea = container.querySelector('textarea')
108
+ expect(textarea).toBeInTheDocument()
109
+ expect(textarea).toHaveAttribute('placeholder', 'Ask anything...')
110
+ expect(textarea).toHaveAttribute('rows', '2')
111
+ expect(container.innerHTML).not.toContain('Preview')
112
+ // Cleanup
113
+ useTabStore.setState({ tabs: [], activeTabId: null })
114
+ useChatStore.setState({ sessions: {} })
115
+ })
116
+
117
+ it('ActiveSession keeps the compact composer once messages exist', () => {
118
+ const SESSION_ID = 'test-active-session-with-messages'
119
+ useTabStore.setState({ tabs: [{ sessionId: SESSION_ID, title: 'Test', type: 'session' as const, status: 'idle' }], activeTabId: SESSION_ID })
120
+ useChatStore.setState({
121
+ sessions: {
122
+ [SESSION_ID]: {
123
+ messages: [{
124
+ id: 'msg-1',
125
+ type: 'user_text',
126
+ content: 'hello',
127
+ timestamp: Date.now(),
128
+ }],
129
+ chatState: 'idle',
130
+ connectionState: 'connected',
131
+ streamingText: '',
132
+ streamingToolInput: '',
133
+ activeToolUseId: null,
134
+ activeToolName: null,
135
+ activeThinkingId: null,
136
+ pendingPermission: null,
137
+ pendingComputerUsePermission: null,
138
+ tokenUsage: { input_tokens: 0, output_tokens: 0 },
139
+ elapsedSeconds: 0,
140
+ statusVerb: '',
141
+ slashCommands: [],
142
+ agentTaskNotifications: {},
143
+ elapsedTimer: null,
144
+ },
145
+ },
146
+ })
147
+ useSessionStore.setState({
148
+ sessions: [{
149
+ id: SESSION_ID,
150
+ title: 'Test',
151
+ createdAt: '2026-04-10T00:00:00.000Z',
152
+ modifiedAt: '2026-04-10T00:00:00.000Z',
153
+ messageCount: 1,
154
+ projectPath: '',
155
+ workDir: null,
156
+ workDirExists: true,
157
+ }],
158
+ activeSessionId: SESSION_ID,
159
+ isLoading: false,
160
+ error: null,
161
+ })
162
+
163
+ render(<ActiveSession />)
164
+
165
+ const textarea = screen.getByPlaceholderText('Ask Claude to edit, debug or explain...')
166
+ expect(textarea).toHaveAttribute('rows', '1')
167
+
168
+ useTabStore.setState({ tabs: [], activeTabId: null })
169
+ useSessionStore.setState({ sessions: [], activeSessionId: null, isLoading: false, error: null })
170
+ useChatStore.setState({ sessions: {} })
171
+ })
172
+
173
+ it('ActiveSession shows a single primary action button while a turn is active', () => {
174
+ useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', type: 'session' as const, status: 'idle' }] })
175
+ useChatStore.setState({
176
+ sessions: {
177
+ 'active-tab': {
178
+ messages: [],
179
+ chatState: 'thinking',
180
+ connectionState: 'connected',
181
+ streamingText: '',
182
+ streamingToolInput: '',
183
+ activeToolUseId: null,
184
+ activeToolName: null,
185
+ activeThinkingId: null,
186
+ pendingPermission: null,
187
+ pendingComputerUsePermission: null,
188
+ tokenUsage: { input_tokens: 0, output_tokens: 0 },
189
+ elapsedSeconds: 0,
190
+ statusVerb: '',
191
+ slashCommands: [],
192
+ agentTaskNotifications: {},
193
+ elapsedTimer: null,
194
+ },
195
+ },
196
+ })
197
+ render(<ActiveSession />)
198
+
199
+ expect(screen.getByRole('button', { name: /stop/i })).toBeInTheDocument()
200
+ expect(screen.queryByRole('button', { name: /^run$/i })).not.toBeInTheDocument()
201
+ useChatStore.setState({ sessions: {} })
202
+ })
203
+
204
+ it('AgentTeams renders team strip and members', () => {
205
+ const { container } = render(<AgentTeams />)
206
+ expect(container.innerHTML).toContain('Architect')
207
+ expect(container.innerHTML).toContain('session-dev')
208
+ expect(container.innerHTML).toContain('groups')
209
+ })
210
+
211
+ it('ScheduledTasks renders (store-connected)', async () => {
212
+ const { container } = render(<ScheduledTasks />)
213
+ await screen.findByText('Scheduled tasks')
214
+ expect(container.innerHTML).toContain('Scheduled tasks')
215
+ })
216
+
217
+ it('ToolInspection renders diff viewer', () => {
218
+ const { container } = render(<ToolInspection />)
219
+ expect(container.innerHTML).toContain('edit_file')
220
+ expect(container.innerHTML).toContain('Split')
221
+ expect(container.innerHTML).toContain('Unified')
222
+ })
223
+ })
224
+
225
+ describe('Chat attachments', () => {
226
+ it('UserMessage opens image gallery when an attachment is clicked', () => {
227
+ render(
228
+ <UserMessage
229
+ content=""
230
+ attachments={[
231
+ {
232
+ type: 'image',
233
+ name: 'diagram.png',
234
+ data: 'data:image/png;base64,abc123',
235
+ },
236
+ ]}
237
+ />,
238
+ )
239
+
240
+ fireEvent.click(screen.getByRole('button'))
241
+ expect(screen.getByText('diagram.png')).toBeInTheDocument()
242
+ })
243
+ })
244
+
245
+ describe('AppShell layout renders chrome', () => {
246
+ it('AppShell renders sidebar and session shell', () => {
247
+ const { container } = render(<Sidebar />)
248
+ expect(container.querySelector('aside')).toBeInTheDocument()
249
+ expect(container.innerHTML).toContain('New session')
250
+ expect(container.innerHTML).toContain('Scheduled')
251
+ expect(container.innerHTML).toContain('All projects')
252
+ })
253
+ })
254
+
255
+ describe('Design system compliance', () => {
256
+ it('Pages use Material Symbols Outlined icons', () => {
257
+ const pages = [EmptySession, AgentTeams, ToolInspection]
258
+ for (const Page of pages) {
259
+ const { container, unmount } = render(<Page />)
260
+ const icons = container.querySelectorAll('.material-symbols-outlined')
261
+ expect(icons.length).toBeGreaterThan(0)
262
+ unmount()
263
+ }
264
+ })
265
+
266
+ it('Current brand color is used in content pages', () => {
267
+ const pages = [EmptySession]
268
+ for (const Page of pages) {
269
+ const { container, unmount } = render(<Page />)
270
+ const html = container.innerHTML
271
+ expect(
272
+ html.includes('C47A5A') ||
273
+ html.includes('8F482F') ||
274
+ html.includes('var(--color-brand)') ||
275
+ html.includes('bg-[var(--color-brand)]'),
276
+ ).toBe(true)
277
+ unmount()
278
+ }
279
+ })
280
+ })
281
+
282
+ describe('Mock data integration', () => {
283
+ it('AgentTeams shows team members from mock data', () => {
284
+ const { container } = render(<AgentTeams />)
285
+ expect(container.innerHTML).toContain('Architect')
286
+ expect(container.innerHTML).toContain('Frontend Dev')
287
+ expect(container.innerHTML).toContain('Backend Dev')
288
+ expect(container.innerHTML).toContain('Tester')
289
+ })
290
+ })