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,253 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import type { CronTask } from '../../types/task'
3
+ import { useTaskStore } from '../../stores/taskStore'
4
+ import { useTranslation } from '../../i18n'
5
+ import { describeCron } from '../../lib/cronDescribe'
6
+ import { TaskRunsPanel } from './TaskRunsPanel'
7
+ import { NewTaskModal } from './NewTaskModal'
8
+
9
+ type Props = {
10
+ task: CronTask
11
+ showLogs: boolean
12
+ onToggleLogs: () => void
13
+ }
14
+
15
+ type ConfirmAction = 'run' | 'toggle' | 'delete' | null
16
+
17
+ export function TaskRow({ task, showLogs, onToggleLogs }: Props) {
18
+ const { deleteTask, updateTask, runTask } = useTaskStore()
19
+ const t = useTranslation()
20
+ const [showEdit, setShowEdit] = useState(false)
21
+ const [showMenu, setShowMenu] = useState(false)
22
+ const [isRunning, setIsRunning] = useState(false)
23
+ const [confirmAction, setConfirmAction] = useState<ConfirmAction>(null)
24
+ const [logsRefreshKey, setLogsRefreshKey] = useState(0)
25
+ const menuRef = useRef<HTMLDivElement>(null)
26
+ const confirmRef = useRef<HTMLDivElement>(null)
27
+
28
+ // Close menu / confirm on outside click
29
+ useEffect(() => {
30
+ if (!showMenu && !confirmAction) return
31
+ const handler = (e: MouseEvent) => {
32
+ const target = e.target as Node
33
+ if (showMenu && menuRef.current && !menuRef.current.contains(target)) {
34
+ setShowMenu(false)
35
+ }
36
+ if (confirmAction && confirmRef.current && !confirmRef.current.contains(target)) {
37
+ setConfirmAction(null)
38
+ }
39
+ }
40
+ document.addEventListener('mousedown', handler)
41
+ return () => document.removeEventListener('mousedown', handler)
42
+ }, [showMenu, confirmAction])
43
+
44
+ const handleRunNow = async () => {
45
+ setConfirmAction(null)
46
+ setIsRunning(true)
47
+ if (!showLogs) onToggleLogs() // open logs panel (accordion will close others)
48
+ try {
49
+ await runTask(task.id)
50
+ setLogsRefreshKey((k) => k + 1)
51
+ } catch (err) {
52
+ console.error('Failed to run task:', err)
53
+ } finally {
54
+ setIsRunning(false)
55
+ }
56
+ }
57
+
58
+ const handleToggle = () => {
59
+ setConfirmAction(null)
60
+ setShowMenu(false)
61
+ updateTask(task.id, { enabled: !task.enabled })
62
+ }
63
+
64
+ const handleDelete = () => {
65
+ setConfirmAction(null)
66
+ setShowMenu(false)
67
+ deleteTask(task.id)
68
+ }
69
+
70
+ const iconBtn = 'p-1.5 rounded-[var(--radius-sm)] transition-colors'
71
+ const menuItem = 'flex items-center gap-2.5 w-full px-3 py-2 text-xs text-left rounded-[var(--radius-sm)] transition-colors'
72
+
73
+ return (
74
+ <div className="border-b border-[var(--color-border-separator)]">
75
+ <div className="flex items-center justify-between px-4 py-3 hover:bg-[var(--color-surface-hover)] transition-colors group">
76
+ {/* Left: status + info */}
77
+ <div className="flex items-center gap-3 min-w-0 flex-1">
78
+ <span className={`w-2 h-2 rounded-full flex-shrink-0 ${task.enabled ? 'bg-[var(--color-success)]' : 'bg-[var(--color-text-tertiary)]'}`} />
79
+ <div className="min-w-0">
80
+ <div className="text-sm font-medium text-[var(--color-text-primary)] truncate">{task.name}</div>
81
+ {task.description && (
82
+ <div className="text-xs text-[var(--color-text-secondary)] truncate">{task.description}</div>
83
+ )}
84
+ <div className="flex items-center gap-3 text-[11px] text-[var(--color-text-tertiary)] mt-0.5">
85
+ <span>{t('tasks.createdAt')}{new Date(task.createdAt).toLocaleDateString()}</span>
86
+ {task.lastFiredAt && (
87
+ <span>{t('tasks.lastRunAt')}{new Date(task.lastFiredAt).toLocaleDateString()}</span>
88
+ )}
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ {/* Right: cron + actions */}
94
+ <div className="flex items-center gap-3 flex-shrink-0">
95
+ <span className="text-xs text-[var(--color-text-tertiary)]" title={task.cron}>
96
+ {describeCron(task.cron, t)}
97
+ </span>
98
+
99
+ <div className="flex items-center gap-0.5">
100
+ {/* Run Now */}
101
+ <div className="relative" ref={confirmAction === 'run' ? confirmRef : undefined}>
102
+ <button
103
+ onClick={() => isRunning || !task.enabled ? undefined : setConfirmAction(confirmAction === 'run' ? null : 'run')}
104
+ disabled={isRunning || !task.enabled}
105
+ className={`${iconBtn} ${task.enabled ? 'text-[var(--color-brand)] hover:bg-[var(--color-surface-selected)]' : 'text-[var(--color-text-tertiary)] cursor-not-allowed'} disabled:opacity-50`}
106
+ title={task.enabled ? t('tasks.runNow') : undefined}
107
+ >
108
+ <span className={`material-symbols-outlined text-[18px] ${isRunning ? 'animate-spin' : ''}`}>
109
+ {isRunning ? 'sync' : 'play_arrow'}
110
+ </span>
111
+ </button>
112
+ {confirmAction === 'run' && (
113
+ <ConfirmPopover
114
+ message={t('tasks.confirmRun')}
115
+ confirmLabel={t('tasks.runNow')}
116
+ onConfirm={handleRunNow}
117
+ onCancel={() => setConfirmAction(null)}
118
+ cancelLabel={t('common.cancel')}
119
+ />
120
+ )}
121
+ </div>
122
+
123
+ {/* View Logs */}
124
+ <button
125
+ onClick={onToggleLogs}
126
+ className={`${iconBtn} ${showLogs ? 'text-[var(--color-brand)] bg-[var(--color-surface-selected)]' : 'text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-selected)]'}`}
127
+ title={t('tasks.viewLogs')}
128
+ >
129
+ <span className="material-symbols-outlined text-[18px]">receipt_long</span>
130
+ </button>
131
+
132
+ {/* More menu */}
133
+ <div className="relative" ref={menuRef}>
134
+ <button
135
+ onClick={() => { setShowMenu(!showMenu); setConfirmAction(null) }}
136
+ className={`${iconBtn} text-[var(--color-text-tertiary)] hover:bg-[var(--color-surface-selected)]`}
137
+ >
138
+ <span className="material-symbols-outlined text-[18px]">more_vert</span>
139
+ </button>
140
+
141
+ {showMenu && !confirmAction && (
142
+ <div className="absolute right-0 top-full mt-1 z-50 w-44 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-lg py-1">
143
+ {/* Edit */}
144
+ <button
145
+ onClick={() => { setShowMenu(false); setShowEdit(true) }}
146
+ className={`${menuItem} text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)]`}
147
+ >
148
+ <span className="material-symbols-outlined text-[16px] text-[var(--color-text-secondary)]">edit</span>
149
+ {t('tasks.edit')}
150
+ </button>
151
+
152
+ {/* Toggle */}
153
+ <button
154
+ onClick={() => setConfirmAction('toggle')}
155
+ className={`${menuItem} text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)]`}
156
+ >
157
+ <span className="material-symbols-outlined text-[16px] text-[var(--color-text-secondary)]">
158
+ {task.enabled ? 'pause_circle' : 'play_circle'}
159
+ </span>
160
+ {task.enabled ? t('common.disable') : t('common.enable')}
161
+ </button>
162
+
163
+ <div className="my-1 h-px bg-[var(--color-border-separator)]" />
164
+
165
+ {/* Delete */}
166
+ <button
167
+ onClick={() => setConfirmAction('delete')}
168
+ className={`${menuItem} text-[var(--color-error)] hover:bg-[var(--color-error-container)]/18`}
169
+ >
170
+ <span className="material-symbols-outlined text-[16px]">delete</span>
171
+ {t('common.delete')}
172
+ </button>
173
+ </div>
174
+ )}
175
+
176
+ {/* Confirm popovers for menu actions */}
177
+ {confirmAction === 'toggle' && (
178
+ <div ref={confirmRef}>
179
+ <ConfirmPopover
180
+ message={task.enabled ? t('tasks.confirmDisable') : t('tasks.confirmEnable')}
181
+ confirmLabel={task.enabled ? t('common.disable') : t('common.enable')}
182
+ onConfirm={handleToggle}
183
+ onCancel={() => { setConfirmAction(null); setShowMenu(false) }}
184
+ cancelLabel={t('common.cancel')}
185
+ />
186
+ </div>
187
+ )}
188
+ {confirmAction === 'delete' && (
189
+ <div ref={confirmRef}>
190
+ <ConfirmPopover
191
+ message={t('tasks.confirmDelete')}
192
+ confirmLabel={t('common.delete')}
193
+ onConfirm={handleDelete}
194
+ onCancel={() => { setConfirmAction(null); setShowMenu(false) }}
195
+ cancelLabel={t('common.cancel')}
196
+ variant="error"
197
+ />
198
+ </div>
199
+ )}
200
+ </div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ {/* Runs panel */}
206
+ {showLogs && (
207
+ <div className="px-4 pb-3">
208
+ <TaskRunsPanel taskId={task.id} onClose={onToggleLogs} refreshKey={logsRefreshKey} />
209
+ </div>
210
+ )}
211
+
212
+ {/* Edit modal */}
213
+ {showEdit && (
214
+ <NewTaskModal open editTask={task} onClose={() => setShowEdit(false)} />
215
+ )}
216
+ </div>
217
+ )
218
+ }
219
+
220
+ // ─── Confirm Popover ─────────────────────────────────────────────────────────
221
+
222
+ function ConfirmPopover({ message, confirmLabel, onConfirm, onCancel, cancelLabel, variant = 'brand' }: {
223
+ message: string
224
+ confirmLabel: string
225
+ onConfirm: () => void
226
+ onCancel: () => void
227
+ cancelLabel: string
228
+ variant?: 'brand' | 'error'
229
+ }) {
230
+ return (
231
+ <div className="absolute right-0 top-full mt-1.5 z-50 w-52 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-lg p-3">
232
+ <p className="text-xs text-[var(--color-text-secondary)] mb-2.5">{message}</p>
233
+ <div className="flex justify-end gap-1.5">
234
+ <button
235
+ onClick={onCancel}
236
+ className="px-2.5 py-1 text-xs rounded-[var(--radius-sm)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors"
237
+ >
238
+ {cancelLabel}
239
+ </button>
240
+ <button
241
+ onClick={onConfirm}
242
+ className={`px-2.5 py-1 text-xs rounded-[var(--radius-sm)] hover:opacity-90 transition-opacity ${
243
+ variant === 'error'
244
+ ? 'bg-[var(--color-error-container)] text-[var(--color-on-error-container)]'
245
+ : 'bg-[image:var(--gradient-btn-primary)] text-[var(--color-btn-primary-fg)]'
246
+ }`}
247
+ >
248
+ {confirmLabel}
249
+ </button>
250
+ </div>
251
+ </div>
252
+ )
253
+ }
@@ -0,0 +1,195 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { useTaskStore } from '../../stores/taskStore'
3
+ import { useChatStore } from '../../stores/chatStore'
4
+ import { useTabStore } from '../../stores/tabStore'
5
+ import { useTranslation } from '../../i18n'
6
+ import { parseRunOutput } from '../../lib/parseRunOutput'
7
+ import type { TaskRun } from '../../types/task'
8
+
9
+ function RunOutput({ run }: { run: TaskRun }) {
10
+ const t = useTranslation()
11
+
12
+ // Show error prominently if present
13
+ if (run.error) {
14
+ return (
15
+ <div className="mt-2 max-h-40 overflow-y-auto whitespace-pre-wrap break-words rounded-[var(--radius-sm)] border border-[var(--color-error)]/20 bg-[var(--color-error-container)]/28 p-2.5 text-xs text-[var(--color-error)]">
16
+ {run.error}
17
+ </div>
18
+ )
19
+ }
20
+
21
+ const text = parseRunOutput(run.output || '')
22
+
23
+ if (!text) {
24
+ return (
25
+ <div className="mt-2 p-2.5 rounded-[var(--radius-sm)] bg-[var(--color-surface-container)] text-xs text-[var(--color-text-tertiary)] italic">
26
+ {run.sessionId ? t('tasks.outputHintSession') : t('tasks.noOutputText')}
27
+ </div>
28
+ )
29
+ }
30
+
31
+ // Render AI text response with proper formatting (not monospace <pre>)
32
+ return (
33
+ <div className="mt-2 p-2.5 rounded-[var(--radius-sm)] bg-[var(--color-surface-container)] text-xs text-[var(--color-text-secondary)] whitespace-pre-wrap break-words max-h-48 overflow-y-auto leading-relaxed">
34
+ {text}
35
+ </div>
36
+ )
37
+ }
38
+
39
+ type Props = {
40
+ taskId: string
41
+ onClose: () => void
42
+ refreshKey?: number
43
+ }
44
+
45
+ const STATUS_CONFIG: Record<string, { icon: string; color: string }> = {
46
+ running: { icon: 'sync', color: 'var(--color-warning)' },
47
+ completed: { icon: 'check_circle', color: 'var(--color-success)' },
48
+ failed: { icon: 'error', color: 'var(--color-error)' },
49
+ timeout: { icon: 'timer_off', color: 'var(--color-error)' },
50
+ }
51
+
52
+ export function TaskRunsPanel({ taskId, onClose, refreshKey }: Props) {
53
+ const t = useTranslation()
54
+ const { fetchTaskRuns } = useTaskStore()
55
+ const connectToSession = useChatStore((s) => s.connectToSession)
56
+ const openTab = useTabStore((s) => s.openTab)
57
+ const [runs, setRuns] = useState<TaskRun[]>([])
58
+ const [loading, setLoading] = useState(true)
59
+ const [expandedId, setExpandedId] = useState<string | null>(null)
60
+
61
+ const openSession = (sessionId: string, taskName?: string) => {
62
+ openTab(sessionId, taskName || 'Task Run')
63
+ connectToSession(sessionId)
64
+ }
65
+
66
+ const refresh = () => {
67
+ fetchTaskRuns(taskId).then((r) => {
68
+ setRuns(r)
69
+ setLoading(false)
70
+ }).catch(() => setLoading(false))
71
+ }
72
+
73
+ // Initial fetch + re-fetch when refreshKey changes
74
+ useEffect(() => {
75
+ setLoading(true)
76
+ refresh()
77
+ }, [taskId, fetchTaskRuns, refreshKey])
78
+
79
+ // Auto-poll while any run is "running" or shortly after a manual trigger.
80
+ // Uses faster 1s polling for the first 10s after refreshKey changes, then 3s.
81
+ const hasRunning = runs.some((r) => r.status === 'running')
82
+ useEffect(() => {
83
+ if (!hasRunning && refreshKey === 0) return // no reason to poll initially
84
+ // Start with fast polling (1s) to give snappy feedback after "Run Now"
85
+ let interval = 1000
86
+ let timer = setInterval(refresh, interval)
87
+ // After 10s, switch to slower 3s polling if still running
88
+ const slowDown = setTimeout(() => {
89
+ clearInterval(timer)
90
+ if (hasRunning) {
91
+ timer = setInterval(refresh, 3000)
92
+ }
93
+ }, 10000)
94
+ // If nothing is running and initial window passes, stop entirely
95
+ const stopTimer = hasRunning ? undefined : setTimeout(() => clearInterval(timer), 12000)
96
+ return () => {
97
+ clearInterval(timer)
98
+ clearTimeout(slowDown)
99
+ if (stopTimer) clearTimeout(stopTimer)
100
+ }
101
+ }, [hasRunning, taskId, refreshKey])
102
+
103
+ return (
104
+ <div className="mt-2 mb-1 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] overflow-hidden">
105
+ {/* Header */}
106
+ <div className="flex items-center justify-between px-4 py-2.5 bg-[var(--color-surface-container)]">
107
+ <span className="text-xs font-medium text-[var(--color-text-primary)]">{t('tasks.logsTitle')}</span>
108
+ <button
109
+ onClick={onClose}
110
+ className="p-0.5 text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] transition-colors"
111
+ >
112
+ <span className="material-symbols-outlined text-[16px]">close</span>
113
+ </button>
114
+ </div>
115
+
116
+ {/* Content */}
117
+ <div className="max-h-64 overflow-y-auto">
118
+ {loading ? (
119
+ <div className="flex items-center justify-center py-6">
120
+ <div className="animate-spin w-4 h-4 border-2 border-[var(--color-brand)] border-t-transparent rounded-full" />
121
+ </div>
122
+ ) : runs.length === 0 ? (
123
+ <div className="px-4 py-6 text-center text-xs text-[var(--color-text-tertiary)]">
124
+ {t('tasks.noLogs')}
125
+ </div>
126
+ ) : (
127
+ <div className="divide-y divide-[var(--color-border-separator)]">
128
+ {runs.map((run) => {
129
+ const cfg = STATUS_CONFIG[run.status] || STATUS_CONFIG.failed!
130
+ const isExpanded = expandedId === run.id
131
+ return (
132
+ <div key={run.id} className="px-4 py-2.5">
133
+ <div className="flex items-center gap-3">
134
+ {/* Status icon */}
135
+ <span
136
+ className={`material-symbols-outlined text-[16px] ${run.status === 'running' ? 'animate-spin' : ''}`}
137
+ style={{ color: cfg.color, fontVariationSettings: "'FILL' 1" }}
138
+ >
139
+ {cfg.icon}
140
+ </span>
141
+
142
+ {/* Status text */}
143
+ <span className="text-xs font-medium" style={{ color: cfg.color }}>
144
+ {t(`tasks.runStatus.${run.status}` as any)} {/* dynamic key */}
145
+ </span>
146
+
147
+ {/* Time */}
148
+ <span className="text-xs text-[var(--color-text-tertiary)]">
149
+ {new Date(run.startedAt).toLocaleString()}
150
+ </span>
151
+
152
+ {/* Duration */}
153
+ {run.durationMs != null && (
154
+ <span className="text-xs text-[var(--color-text-tertiary)]">
155
+ {t('tasks.duration', { s: Math.round(run.durationMs / 1000) })}
156
+ </span>
157
+ )}
158
+
159
+ <div className="ml-auto flex items-center gap-2">
160
+ {/* Open session — only after run completes (session is empty while running) */}
161
+ {run.sessionId && run.status !== 'running' && (
162
+ <button
163
+ onClick={() => openSession(run.sessionId!, run.taskName)}
164
+ className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-[var(--color-brand)] bg-[var(--color-brand)]/8 hover:bg-[var(--color-brand)]/15 rounded-[var(--radius-sm)] transition-colors"
165
+ >
166
+ <span className="material-symbols-outlined text-[14px]">open_in_new</span>
167
+ {t('tasks.openSession')}
168
+ </button>
169
+ )}
170
+
171
+ {/* Summary toggle */}
172
+ {(run.output || run.error) && (
173
+ <button
174
+ onClick={() => setExpandedId(isExpanded ? null : run.id)}
175
+ className="text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
176
+ >
177
+ {isExpanded ? t('tasks.hideOutput') : t('tasks.viewOutput')}
178
+ </button>
179
+ )}
180
+ </div>
181
+ </div>
182
+
183
+ {/* Expanded output */}
184
+ {isExpanded && (
185
+ <RunOutput run={run} />
186
+ )}
187
+ </div>
188
+ )
189
+ })}
190
+ </div>
191
+ )}
192
+ </div>
193
+ </div>
194
+ )
195
+ }
@@ -0,0 +1,147 @@
1
+ import { useState } from 'react'
2
+ import { useTeamStore } from '../../stores/teamStore'
3
+ import { useTranslation } from '../../i18n'
4
+ import type { TeamMember } from '../../types/team'
5
+
6
+ const memberStatusConfig = {
7
+ running: {
8
+ icon: 'pending',
9
+ color: 'var(--color-warning)',
10
+ pulse: true,
11
+ },
12
+ idle: {
13
+ icon: 'radio_button_unchecked',
14
+ color: 'var(--color-text-tertiary)',
15
+ pulse: false,
16
+ },
17
+ completed: {
18
+ icon: 'check_circle',
19
+ color: 'var(--color-success)',
20
+ pulse: false,
21
+ },
22
+ error: {
23
+ icon: 'error',
24
+ color: 'var(--color-error)',
25
+ pulse: false,
26
+ },
27
+ } as const
28
+
29
+ export function TeamStatusBar() {
30
+ const t = useTranslation()
31
+ const { activeTeam, openMemberSession } = useTeamStore()
32
+ const [expanded, setExpanded] = useState(true)
33
+
34
+ if (!activeTeam) return null
35
+
36
+ // Filter out leader — main window is already the leader's view
37
+ const members = activeTeam.members.filter(
38
+ (m) => !activeTeam.leadAgentId || m.agentId !== activeTeam.leadAgentId,
39
+ )
40
+ const runningCount = members.filter((m) => m.status === 'running').length
41
+ const completedCount = members.filter((m) => m.status === 'completed').length
42
+ const totalCount = members.length
43
+ const progressPercent = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0
44
+ const allDone = runningCount === 0 && totalCount > 0
45
+
46
+ return (
47
+ <div className="shrink-0 px-8">
48
+ <div className="mx-auto max-w-[860px] rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-lowest)] overflow-hidden mb-2">
49
+ {/* Header */}
50
+ <button
51
+ onClick={() => setExpanded((v) => !v)}
52
+ className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-[var(--color-surface-container-low)] transition-colors bg-[var(--color-surface-container)]"
53
+ >
54
+ <div className="flex items-center justify-center w-6 h-6 rounded-[var(--radius-md)] bg-[var(--color-brand)]/10">
55
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-brand)]">groups</span>
56
+ </div>
57
+
58
+ <span className="text-xs font-semibold text-[var(--color-text-primary)]">
59
+ {t('teams.team')} {activeTeam.name}
60
+ </span>
61
+
62
+ {/* Progress bar */}
63
+ <div className="flex-1 h-1.5 rounded-full bg-[var(--color-border)] overflow-hidden max-w-[200px]">
64
+ <div
65
+ className="h-full rounded-full transition-all duration-300"
66
+ style={{
67
+ width: `${progressPercent}%`,
68
+ backgroundColor: allDone ? 'var(--color-success)' : 'var(--color-brand)',
69
+ }}
70
+ />
71
+ </div>
72
+
73
+ <span className="text-[10px] text-[var(--color-text-tertiary)] tabular-nums">
74
+ {completedCount}/{totalCount}
75
+ </span>
76
+
77
+ {runningCount > 0 && (
78
+ <span className="flex items-center gap-1 text-[10px] text-[var(--color-warning)]">
79
+ <span className="w-1.5 h-1.5 rounded-full bg-[var(--color-warning)] animate-pulse-dot" />
80
+ {runningCount} {t('teams.running')}
81
+ </span>
82
+ )}
83
+
84
+ <span
85
+ className="material-symbols-outlined text-[14px] text-[var(--color-text-tertiary)] transition-transform duration-200"
86
+ style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
87
+ >
88
+ expand_less
89
+ </span>
90
+ </button>
91
+
92
+ {/* Expanded member list */}
93
+ {expanded && (
94
+ <div className="px-4 pb-2 pt-1 flex flex-col gap-0.5 max-h-[240px] overflow-y-auto border-t border-[var(--color-outline-variant)]/20">
95
+ {members.map((member) => (
96
+ <MemberRow key={member.agentId} member={member} onView={() => openMemberSession(member)} />
97
+ ))}
98
+ </div>
99
+ )}
100
+ </div>
101
+ </div>
102
+ )
103
+ }
104
+
105
+ function MemberRow({ member, onView }: { member: TeamMember; onView: () => void }) {
106
+ const config = memberStatusConfig[member.status] || memberStatusConfig.idle
107
+
108
+ return (
109
+ <button
110
+ onClick={onView}
111
+ className="w-full flex items-center gap-2 py-1.5 px-1 rounded-md text-left hover:bg-[var(--color-surface-container-low)] transition-colors group"
112
+ >
113
+ <span
114
+ className={`material-symbols-outlined text-[16px] shrink-0 ${config.pulse ? 'animate-pulse-dot' : ''}`}
115
+ style={{ color: config.color, fontVariationSettings: "'FILL' 1" }}
116
+ >
117
+ {config.icon}
118
+ </span>
119
+
120
+ <div className="flex-1 min-w-0">
121
+ <div className="flex items-center gap-1.5">
122
+ <span className="material-symbols-outlined text-[12px] text-[var(--color-text-tertiary)]">smart_toy</span>
123
+ <span className={`text-xs ${
124
+ member.status === 'completed'
125
+ ? 'text-[var(--color-text-tertiary)]'
126
+ : 'text-[var(--color-text-primary)]'
127
+ }`}>
128
+ {member.role}
129
+ </span>
130
+ </div>
131
+
132
+ {member.status === 'running' && member.currentTask && (
133
+ <div className="flex items-center gap-1 mt-0.5">
134
+ <span className="w-1.5 h-1.5 rounded-full bg-[var(--color-warning)] animate-pulse-dot" />
135
+ <span className="text-[10px] text-[var(--color-warning)] truncate">
136
+ {member.currentTask}
137
+ </span>
138
+ </div>
139
+ )}
140
+ </div>
141
+
142
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-text-tertiary)] opacity-0 group-hover:opacity-100 transition-opacity">
143
+ open_in_new
144
+ </span>
145
+ </button>
146
+ )
147
+ }
@@ -0,0 +1,78 @@
1
+ // Provider presets inspired by cc-switch (https://github.com/farion1231/cc-switch)
2
+ // Original work by Jason Young, MIT License
3
+
4
+ import type { ApiFormat } from '../types/provider'
5
+
6
+ export type ModelMapping = {
7
+ main: string
8
+ haiku: string
9
+ sonnet: string
10
+ opus: string
11
+ }
12
+
13
+ export type ProviderPreset = {
14
+ id: string
15
+ name: string
16
+ baseUrl: string
17
+ apiFormat: ApiFormat
18
+ defaultModels: ModelMapping
19
+ needsApiKey: boolean
20
+ websiteUrl: string
21
+ }
22
+
23
+ export const PROVIDER_PRESETS: ProviderPreset[] = [
24
+ {
25
+ id: 'official',
26
+ name: 'Claude Official',
27
+ baseUrl: '',
28
+ apiFormat: 'anthropic',
29
+ defaultModels: { main: '', haiku: '', sonnet: '', opus: '' },
30
+ needsApiKey: false,
31
+ websiteUrl: 'https://www.anthropic.com/claude-code',
32
+ },
33
+ {
34
+ id: 'deepseek',
35
+ name: 'DeepSeek',
36
+ baseUrl: 'https://api.deepseek.com/anthropic',
37
+ apiFormat: 'anthropic',
38
+ defaultModels: { main: 'DeepSeek-V3.2', haiku: 'DeepSeek-V3.2', sonnet: 'DeepSeek-V3.2', opus: 'DeepSeek-V3.2' },
39
+ needsApiKey: true,
40
+ websiteUrl: 'https://platform.deepseek.com',
41
+ },
42
+ {
43
+ id: 'zhipuglm',
44
+ name: 'Zhipu GLM',
45
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
46
+ apiFormat: 'anthropic',
47
+ defaultModels: { main: 'glm-5', haiku: 'glm-5', sonnet: 'glm-5', opus: 'glm-5' },
48
+ needsApiKey: true,
49
+ websiteUrl: 'https://open.bigmodel.cn',
50
+ },
51
+ {
52
+ id: 'kimi',
53
+ name: 'Kimi',
54
+ baseUrl: 'https://api.moonshot.cn/anthropic',
55
+ apiFormat: 'anthropic',
56
+ defaultModels: { main: 'kimi-k2.5', haiku: 'kimi-k2.5', sonnet: 'kimi-k2.5', opus: 'kimi-k2.5' },
57
+ needsApiKey: true,
58
+ websiteUrl: 'https://platform.moonshot.cn',
59
+ },
60
+ {
61
+ id: 'minimax',
62
+ name: 'MiniMax',
63
+ baseUrl: 'https://api.minimaxi.com/anthropic',
64
+ apiFormat: 'anthropic',
65
+ defaultModels: { main: 'MiniMax-M2.7', haiku: 'MiniMax-M2.7', sonnet: 'MiniMax-M2.7', opus: 'MiniMax-M2.7' },
66
+ needsApiKey: true,
67
+ websiteUrl: 'https://platform.minimaxi.com',
68
+ },
69
+ {
70
+ id: 'custom',
71
+ name: 'Custom',
72
+ baseUrl: '',
73
+ apiFormat: 'anthropic',
74
+ defaultModels: { main: '', haiku: '', sonnet: '', opus: '' },
75
+ needsApiKey: true,
76
+ websiteUrl: '',
77
+ },
78
+ ]