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,120 @@
1
+ import { create } from 'zustand'
2
+ import { settingsApi } from '../api/settings'
3
+ import { modelsApi } from '../api/models'
4
+ import type { PermissionMode, EffortLevel, ModelInfo, ThemeMode } from '../types/settings'
5
+ import type { Locale } from '../i18n'
6
+ import { useUIStore } from './uiStore'
7
+
8
+ const LOCALE_STORAGE_KEY = 'cc-haha-locale'
9
+
10
+ function getStoredLocale(): Locale {
11
+ try {
12
+ const stored = localStorage.getItem(LOCALE_STORAGE_KEY)
13
+ if (stored === 'en' || stored === 'zh') return stored
14
+ } catch { /* localStorage unavailable */ }
15
+ return 'en'
16
+ }
17
+
18
+ type SettingsStore = {
19
+ permissionMode: PermissionMode
20
+ currentModel: ModelInfo | null
21
+ effortLevel: EffortLevel
22
+ availableModels: ModelInfo[]
23
+ activeProviderName: string | null
24
+ locale: Locale
25
+ theme: ThemeMode
26
+ isLoading: boolean
27
+ error: string | null
28
+
29
+ fetchAll: () => Promise<void>
30
+ setPermissionMode: (mode: PermissionMode) => Promise<void>
31
+ setModel: (modelId: string) => Promise<void>
32
+ setEffort: (level: EffortLevel) => Promise<void>
33
+ setLocale: (locale: Locale) => void
34
+ setTheme: (theme: ThemeMode) => Promise<void>
35
+ }
36
+
37
+ export const useSettingsStore = create<SettingsStore>((set, get) => ({
38
+ permissionMode: 'default',
39
+ currentModel: null,
40
+ effortLevel: 'high',
41
+ availableModels: [],
42
+ activeProviderName: null,
43
+ locale: getStoredLocale(),
44
+ theme: useUIStore.getState().theme,
45
+ isLoading: false,
46
+ error: null,
47
+
48
+ fetchAll: async () => {
49
+ set({ isLoading: true, error: null })
50
+ try {
51
+ const [{ mode }, modelsRes, { model }, { level }, userSettings] = await Promise.all([
52
+ settingsApi.getPermissionMode(),
53
+ modelsApi.list(),
54
+ modelsApi.getCurrent(),
55
+ modelsApi.getEffort(),
56
+ settingsApi.getUser(),
57
+ ])
58
+ const theme = userSettings.theme === 'dark' ? 'dark' : 'light'
59
+ useUIStore.getState().setTheme(theme)
60
+ set({
61
+ permissionMode: mode,
62
+ availableModels: modelsRes.models,
63
+ activeProviderName: modelsRes.provider?.name ?? null,
64
+ currentModel: model,
65
+ effortLevel: level,
66
+ theme,
67
+ isLoading: false,
68
+ error: null,
69
+ })
70
+ } catch (error) {
71
+ const message =
72
+ error instanceof Error ? error.message : 'Failed to load desktop settings'
73
+ set({ isLoading: false, error: message })
74
+ throw error
75
+ }
76
+ },
77
+
78
+ setPermissionMode: async (mode) => {
79
+ const prev = get().permissionMode
80
+ set({ permissionMode: mode })
81
+ try {
82
+ await settingsApi.setPermissionMode(mode)
83
+ } catch {
84
+ set({ permissionMode: prev })
85
+ }
86
+ },
87
+
88
+ setModel: async (modelId) => {
89
+ await modelsApi.setCurrent(modelId)
90
+ const { model } = await modelsApi.getCurrent()
91
+ set({ currentModel: model })
92
+ },
93
+
94
+ setEffort: async (level) => {
95
+ const prev = get().effortLevel
96
+ set({ effortLevel: level })
97
+ try {
98
+ await modelsApi.setEffort(level)
99
+ } catch {
100
+ set({ effortLevel: prev })
101
+ }
102
+ },
103
+
104
+ setLocale: (locale) => {
105
+ set({ locale })
106
+ try { localStorage.setItem(LOCALE_STORAGE_KEY, locale) } catch { /* noop */ }
107
+ },
108
+
109
+ setTheme: async (theme) => {
110
+ const prev = get().theme
111
+ set({ theme })
112
+ useUIStore.getState().setTheme(theme)
113
+ try {
114
+ await settingsApi.updateUser({ theme })
115
+ } catch {
116
+ set({ theme: prev })
117
+ useUIStore.getState().setTheme(prev)
118
+ }
119
+ },
120
+ }))
@@ -0,0 +1,51 @@
1
+ import { create } from 'zustand'
2
+ import { skillsApi } from '../api/skills'
3
+ import type { SkillMeta, SkillDetail } from '../types/skill'
4
+
5
+ type SkillStore = {
6
+ skills: SkillMeta[]
7
+ selectedSkill: SkillDetail | null
8
+ isLoading: boolean
9
+ isDetailLoading: boolean
10
+ error: string | null
11
+
12
+ fetchSkills: (cwd?: string) => Promise<void>
13
+ fetchSkillDetail: (source: string, name: string, cwd?: string) => Promise<void>
14
+ clearSelection: () => void
15
+ }
16
+
17
+ export const useSkillStore = create<SkillStore>((set) => ({
18
+ skills: [],
19
+ selectedSkill: null,
20
+ isLoading: false,
21
+ isDetailLoading: false,
22
+ error: null,
23
+
24
+ fetchSkills: async (cwd) => {
25
+ set({ isLoading: true, error: null })
26
+ try {
27
+ const { skills } = await skillsApi.list(cwd)
28
+ set({ skills, isLoading: false })
29
+ } catch (err) {
30
+ set({
31
+ error: err instanceof Error ? err.message : String(err),
32
+ isLoading: false,
33
+ })
34
+ }
35
+ },
36
+
37
+ fetchSkillDetail: async (source, name, cwd) => {
38
+ set({ isDetailLoading: true, error: null })
39
+ try {
40
+ const { detail } = await skillsApi.detail(source, name, cwd)
41
+ set({ selectedSkill: detail, isDetailLoading: false })
42
+ } catch (err) {
43
+ set({
44
+ error: err instanceof Error ? err.message : String(err),
45
+ isDetailLoading: false,
46
+ })
47
+ }
48
+ },
49
+
50
+ clearSelection: () => set({ selectedSkill: null }),
51
+ }))
@@ -0,0 +1,169 @@
1
+ import { create } from 'zustand'
2
+ import { sessionsApi } from '../api/sessions'
3
+
4
+ const TAB_STORAGE_KEY = 'cc-haha-open-tabs'
5
+
6
+ export const SETTINGS_TAB_ID = '__settings__'
7
+ export const SCHEDULED_TAB_ID = '__scheduled__'
8
+
9
+ export type TabType = 'session' | 'settings' | 'scheduled'
10
+
11
+ export type Tab = {
12
+ sessionId: string
13
+ title: string
14
+ type: TabType
15
+ status: 'idle' | 'running' | 'error'
16
+ }
17
+
18
+ type TabPersistence = {
19
+ openTabs: Array<{ sessionId: string; title: string; type?: TabType }>
20
+ activeTabId: string | null
21
+ }
22
+
23
+ type TabStore = {
24
+ tabs: Tab[]
25
+ activeTabId: string | null
26
+
27
+ openTab: (sessionId: string, title: string, type?: TabType) => void
28
+ closeTab: (sessionId: string) => void
29
+ setActiveTab: (sessionId: string) => void
30
+ updateTabTitle: (sessionId: string, title: string) => void
31
+ updateTabStatus: (sessionId: string, status: Tab['status']) => void
32
+ replaceTabSession: (oldSessionId: string, newSessionId: string) => void
33
+ moveTab: (fromIndex: number, toIndex: number) => void
34
+
35
+ saveTabs: () => void
36
+ restoreTabs: () => Promise<void>
37
+ }
38
+
39
+ export const useTabStore = create<TabStore>((set, get) => ({
40
+ tabs: [],
41
+ activeTabId: null,
42
+
43
+ openTab: (sessionId, title, type = 'session') => {
44
+ const { tabs } = get()
45
+ const existing = tabs.find((t) => t.sessionId === sessionId)
46
+ if (existing) {
47
+ set({ activeTabId: sessionId })
48
+ } else {
49
+ set({
50
+ tabs: [...tabs, { sessionId, title, type, status: 'idle' }],
51
+ activeTabId: sessionId,
52
+ })
53
+ }
54
+ get().saveTabs()
55
+ },
56
+
57
+ closeTab: (sessionId) => {
58
+ const { tabs, activeTabId } = get()
59
+ const index = tabs.findIndex((t) => t.sessionId === sessionId)
60
+ if (index < 0) return
61
+
62
+ const newTabs = tabs.filter((t) => t.sessionId !== sessionId)
63
+ let newActiveId = activeTabId
64
+
65
+ if (activeTabId === sessionId) {
66
+ if (newTabs.length === 0) {
67
+ newActiveId = null
68
+ } else if (index >= newTabs.length) {
69
+ newActiveId = newTabs[newTabs.length - 1]!.sessionId
70
+ } else {
71
+ newActiveId = newTabs[index]!.sessionId
72
+ }
73
+ }
74
+
75
+ set({ tabs: newTabs, activeTabId: newActiveId })
76
+ get().saveTabs()
77
+ },
78
+
79
+ setActiveTab: (sessionId) => {
80
+ set({ activeTabId: sessionId })
81
+ get().saveTabs()
82
+ },
83
+
84
+ updateTabTitle: (sessionId, title) => {
85
+ set((s) => ({
86
+ tabs: s.tabs.map((t) => (t.sessionId === sessionId ? { ...t, title } : t)),
87
+ }))
88
+ get().saveTabs()
89
+ },
90
+
91
+ updateTabStatus: (sessionId, status) => {
92
+ set((s) => ({
93
+ tabs: s.tabs.map((t) => (t.sessionId === sessionId ? { ...t, status } : t)),
94
+ }))
95
+ },
96
+
97
+ replaceTabSession: (oldSessionId, newSessionId) => {
98
+ const { activeTabId } = get()
99
+ set((s) => ({
100
+ tabs: s.tabs.map((t) =>
101
+ t.sessionId === oldSessionId ? { ...t, sessionId: newSessionId } : t,
102
+ ),
103
+ activeTabId: activeTabId === oldSessionId ? newSessionId : activeTabId,
104
+ }))
105
+ get().saveTabs()
106
+ },
107
+
108
+ moveTab: (fromIndex, toIndex) => {
109
+ if (fromIndex === toIndex) return
110
+ const { tabs } = get()
111
+ if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length) return
112
+ const newTabs = [...tabs]
113
+ const [moved] = newTabs.splice(fromIndex, 1)
114
+ newTabs.splice(toIndex, 0, moved!)
115
+ set({ tabs: newTabs })
116
+ get().saveTabs()
117
+ },
118
+
119
+ saveTabs: () => {
120
+ const { tabs, activeTabId } = get()
121
+ const data: TabPersistence = {
122
+ openTabs: tabs.map((t) => ({ sessionId: t.sessionId, title: t.title, type: t.type })),
123
+ activeTabId,
124
+ }
125
+ try {
126
+ localStorage.setItem(TAB_STORAGE_KEY, JSON.stringify(data))
127
+ } catch { /* noop */ }
128
+ },
129
+
130
+ restoreTabs: async () => {
131
+ try {
132
+ const raw = localStorage.getItem(TAB_STORAGE_KEY)
133
+ if (!raw) return
134
+
135
+ const data = JSON.parse(raw) as TabPersistence
136
+ if (!data.openTabs || data.openTabs.length === 0) return
137
+
138
+ const { sessions } = await sessionsApi.list({ limit: 200 })
139
+ const existingIds = new Set(sessions.map((s) => s.id))
140
+
141
+ const validTabs: Tab[] = data.openTabs
142
+ .filter((t) => {
143
+ // Special tabs are always valid
144
+ if (t.type === 'settings' || t.type === 'scheduled') return true
145
+ // Session tabs must exist on server
146
+ return existingIds.has(t.sessionId)
147
+ })
148
+ .map((t) => {
149
+ if (t.type === 'settings' || t.type === 'scheduled') {
150
+ return { sessionId: t.sessionId, title: t.title, type: t.type, status: 'idle' as const }
151
+ }
152
+ return {
153
+ sessionId: t.sessionId,
154
+ title: sessions.find((s) => s.id === t.sessionId)?.title || t.title,
155
+ type: 'session' as const,
156
+ status: 'idle' as const,
157
+ }
158
+ })
159
+
160
+ if (validTabs.length === 0) return
161
+
162
+ const activeId = data.activeTabId && validTabs.some((t) => t.sessionId === data.activeTabId)
163
+ ? data.activeTabId
164
+ : validTabs[0]!.sessionId
165
+
166
+ set({ tabs: validTabs, activeTabId: activeId })
167
+ } catch { /* noop */ }
168
+ },
169
+ }))
@@ -0,0 +1,68 @@
1
+ import { create } from 'zustand'
2
+ import { tasksApi } from '../api/tasks'
3
+ import type { CronTask, CreateTaskInput, TaskRun } from '../types/task'
4
+
5
+ type TaskStore = {
6
+ tasks: CronTask[]
7
+ recentRuns: TaskRun[]
8
+ isLoading: boolean
9
+ error: string | null
10
+
11
+ fetchTasks: () => Promise<void>
12
+ createTask: (input: CreateTaskInput) => Promise<void>
13
+ updateTask: (id: string, updates: Partial<CronTask>) => Promise<void>
14
+ deleteTask: (id: string) => Promise<void>
15
+ runTask: (taskId: string) => Promise<void>
16
+ fetchRecentRuns: () => Promise<void>
17
+ fetchTaskRuns: (taskId: string) => Promise<TaskRun[]>
18
+ }
19
+
20
+ export const useTaskStore = create<TaskStore>((set) => ({
21
+ tasks: [],
22
+ recentRuns: [],
23
+ isLoading: false,
24
+ error: null,
25
+
26
+ fetchTasks: async () => {
27
+ set({ isLoading: true, error: null })
28
+ try {
29
+ const { tasks } = await tasksApi.list()
30
+ set({ tasks, isLoading: false })
31
+ } catch (err) {
32
+ set({ error: (err as Error).message, isLoading: false })
33
+ }
34
+ },
35
+
36
+ createTask: async (input) => {
37
+ const { task } = await tasksApi.create(input)
38
+ set((s) => ({ tasks: [...s.tasks, task] }))
39
+ },
40
+
41
+ updateTask: async (id, updates) => {
42
+ const { task } = await tasksApi.update(id, updates)
43
+ set((s) => ({ tasks: s.tasks.map((t) => (t.id === id ? task : t)) }))
44
+ },
45
+
46
+ deleteTask: async (id) => {
47
+ await tasksApi.delete(id)
48
+ set((s) => ({ tasks: s.tasks.filter((t) => t.id !== id) }))
49
+ },
50
+
51
+ runTask: async (taskId) => {
52
+ await tasksApi.runTask(taskId)
53
+ },
54
+
55
+ fetchRecentRuns: async () => {
56
+ try {
57
+ const { runs } = await tasksApi.getRecentRuns()
58
+ set({ recentRuns: runs })
59
+ } catch {
60
+ // ignore
61
+ }
62
+ },
63
+
64
+ fetchTaskRuns: async (taskId) => {
65
+ const { runs } = await tasksApi.getTaskRuns(taskId)
66
+ return runs
67
+ },
68
+ }))