@yancyyu/openhermit 1.6.4 → 1.6.6

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 (76) hide show
  1. package/README.md +52 -140
  2. package/bin/hermit.mjs +79 -297
  3. package/dist-renderer/assets/{ProjectEditorOverlay-BcjkdR8y.js → ProjectEditorOverlay-pgmdlWxa.js} +1 -1
  4. package/dist-renderer/assets/{TeamGraphOverlay-B9PP0b_t.js → TeamGraphOverlay-hN5q6AG0.js} +1 -1
  5. package/dist-renderer/assets/{_basePickBy-CPquAmj5.js → _basePickBy-Bk3vVggp.js} +1 -1
  6. package/dist-renderer/assets/{_baseUniq-A66EsJn2.js → _baseUniq-dMa0BJXs.js} +1 -1
  7. package/dist-renderer/assets/{arc-YLxbV3Qw.js → arc-DJlWrmw7.js} +1 -1
  8. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-wwpiLSwy.js → architectureDiagram-VXUJARFQ-BEk2m-RH.js} +1 -1
  9. package/dist-renderer/assets/{blockDiagram-VD42YOAC-3CHE3NYR.js → blockDiagram-VD42YOAC-DikRTLHM.js} +1 -1
  10. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-K8hDNmEC.js → c4Diagram-YG6GDRKO-2zOWoKX8.js} +1 -1
  11. package/dist-renderer/assets/channel-BL6oUYPL.js +1 -0
  12. package/dist-renderer/assets/{chunk-4BX2VUAB-5OabZrhH.js → chunk-4BX2VUAB-7PeMCF0g.js} +1 -1
  13. package/dist-renderer/assets/{chunk-55IACEB6-v2kdM_aT.js → chunk-55IACEB6-6LuNK7l-.js} +1 -1
  14. package/dist-renderer/assets/{chunk-B4BG7PRW-C0Ju56SH.js → chunk-B4BG7PRW-9Q4M3OpN.js} +1 -1
  15. package/dist-renderer/assets/{chunk-DI55MBZ5-DPTWTKRm.js → chunk-DI55MBZ5-mGGPKOCm.js} +1 -1
  16. package/dist-renderer/assets/{chunk-FMBD7UC4-DSkYppkv.js → chunk-FMBD7UC4-BTZYjAAP.js} +1 -1
  17. package/dist-renderer/assets/{chunk-QN33PNHL-C_4cCLCl.js → chunk-QN33PNHL-D9_yb-Pl.js} +1 -1
  18. package/dist-renderer/assets/{chunk-QZHKN3VN-ojL7PmOD.js → chunk-QZHKN3VN-CVMnt9Of.js} +1 -1
  19. package/dist-renderer/assets/{chunk-TZMSLE5B-D1g7Vl_v.js → chunk-TZMSLE5B-B11BOOB_.js} +1 -1
  20. package/dist-renderer/assets/classDiagram-2ON5EDUG-DLZQJPSd.js +1 -0
  21. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DLZQJPSd.js +1 -0
  22. package/dist-renderer/assets/clone-CRwa9g7P.js +1 -0
  23. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-TJnGh924.js → cose-bilkent-S5V4N54A-Dx1iOVS_.js} +1 -1
  24. package/dist-renderer/assets/{dagre-6UL2VRFP-cPgfHhoX.js → dagre-6UL2VRFP-BfFhjHyX.js} +1 -1
  25. package/dist-renderer/assets/{diagram-PSM6KHXK-BS5Y-RR6.js → diagram-PSM6KHXK-BjauYjwN.js} +1 -1
  26. package/dist-renderer/assets/{diagram-QEK2KX5R-D9AF7AGJ.js → diagram-QEK2KX5R-DTBnRCua.js} +1 -1
  27. package/dist-renderer/assets/{diagram-S2PKOQOG-DTFUadMS.js → diagram-S2PKOQOG-CBhKzb3E.js} +1 -1
  28. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-DB_StEwC.js → erDiagram-Q2GNP2WA-B0xgzQzS.js} +1 -1
  29. package/dist-renderer/assets/{flowDiagram-NV44I4VS-DGn40aPj.js → flowDiagram-NV44I4VS-klmMoWI3.js} +1 -1
  30. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-9NiFCSBT.js → ganttDiagram-JELNMOA3-BSv5ddzU.js} +1 -1
  31. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BdveeU3c.js → gitGraphDiagram-V2S2FVAM-CEUBnydU.js} +1 -1
  32. package/dist-renderer/assets/{graph-aQYbgTDH.js → graph-CIysEkgE.js} +1 -1
  33. package/dist-renderer/assets/{index-CrCHolXN.js → index-7EkmxIrp.js} +1 -1
  34. package/dist-renderer/assets/{index-CWqPn0NY.js → index-BK6dwrxm.js} +1 -1
  35. package/dist-renderer/assets/{index-CaG9mf8s.css → index-C4x095x4.css} +1 -1
  36. package/dist-renderer/assets/{index-oyepEosi.js → index-CJLDGPfy.js} +1 -1
  37. package/dist-renderer/assets/{index-DiAK42nd.js → index-CSheMJTk.js} +1 -1
  38. package/dist-renderer/assets/{index-DmgKTZAa.js → index-CcdwUZk9.js} +529 -524
  39. package/dist-renderer/assets/{index-DyEKO6GV.js → index-DA5wdGF_.js} +1 -1
  40. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-Dmc_xn8U.js → infoDiagram-HS3SLOUP-BgF7b8R8.js} +1 -1
  41. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-D9LJr-B5.js → journeyDiagram-XKPGCS4Q-Brnq6sZ9.js} +1 -1
  42. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CjOWoNys.js → kanban-definition-3W4ZIXB7-oEx_7thy.js} +1 -1
  43. package/dist-renderer/assets/{layout-D6GzYK4K.js → layout-Bb-Myf8p.js} +1 -1
  44. package/dist-renderer/assets/{linear-Dt3GyUQf.js → linear-BC5s25rz.js} +1 -1
  45. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-XwY2hZr8.js → mindmap-definition-VGOIOE7T-MB1jxroI.js} +1 -1
  46. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BU4nfYd7.js → pieDiagram-ADFJNKIX-DSEfLnAV.js} +1 -1
  47. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BYk6f63x.js → quadrantDiagram-AYHSOK5B-Bm0Xw7gV.js} +1 -1
  48. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-kbadr_bU.js → requirementDiagram-UZGBJVZJ-Cjn0opjv.js} +1 -1
  49. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-ZstP2Vth.js → sankeyDiagram-TZEHDZUN-P7KZIQkA.js} +1 -1
  50. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-obK_-ssz.js → sequenceDiagram-WL72ISMW-DHDX_5Fl.js} +1 -1
  51. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BgZDg0VT.js → stateDiagram-FKZM4ZOC-DGyXzpw9.js} +1 -1
  52. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CMa5sz7x.js → stateDiagram-v2-4FDKWEC3-sTtOoyBH.js} +1 -1
  53. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BOmCNnab.js → timeline-definition-IT6M3QCI-BLoCrtzz.js} +1 -1
  54. package/dist-renderer/assets/treemap-GDKQZRPO-CI5sRz-0.js +162 -0
  55. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-BzAHNASi.js → xychartDiagram-PRI3JC2R-CkvLu9vG.js} +1 -1
  56. package/dist-renderer/index.html +2 -2
  57. package/package.json +23 -17
  58. package/src/main/server.ts +179 -23
  59. package/src/main/services/teams-mvp/TaskDispatchService.ts +440 -0
  60. package/src/main/services/teams-mvp/TeamProvisioningService.ts +36 -33
  61. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +2 -0
  62. package/src/renderer/components/settings/SettingsTabs.tsx +8 -2
  63. package/src/renderer/components/settings/SettingsView.tsx +4 -0
  64. package/src/renderer/components/settings/sections/GeneralSection.tsx +168 -206
  65. package/src/renderer/components/settings/sections/TaskBusSection.tsx +176 -0
  66. package/src/renderer/components/sidebar/SidebarSessions.tsx +31 -4
  67. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +37 -0
  68. package/src/renderer/components/team/messages/MessageComposer.tsx +36 -228
  69. package/src/renderer/components/team/messages/MessagesPanel.tsx +0 -3
  70. package/src/renderer/store/slices/teamSlice.ts +30 -1
  71. package/src/shared/types/team.ts +73 -0
  72. package/dist-renderer/assets/channel-BSWYOYIc.js +0 -1
  73. package/dist-renderer/assets/classDiagram-2ON5EDUG-mw4yABob.js +0 -1
  74. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-mw4yABob.js +0 -1
  75. package/dist-renderer/assets/clone-KtZfFt-o.js +0 -1
  76. package/dist-renderer/assets/treemap-GDKQZRPO-BU0ha0Ww.js +0 -162
@@ -15,27 +15,31 @@ import { Check, Copy, FolderOpen, Laptop, Loader2, RotateCcw } from 'lucide-reac
15
15
  import { useShallow } from 'zustand/react/shallow';
16
16
 
17
17
  import { Button } from '@renderer/components/ui/button';
18
- import { SettingRow, SettingsSectionHeader, SettingsToggle } from '../components';
18
+ import { SettingRow, SettingsSectionHeader, SettingsSelect, SettingsToggle } from '../components';
19
19
 
20
20
  import type { SafeConfig } from '../hooks/useSettingsConfig';
21
21
  import type { ClaudeRootInfo, WslClaudeRootCandidate } from '@shared/types';
22
22
  import type { HttpServerStatus } from '@shared/types/api';
23
23
  import type { AppConfig } from '@shared/types/notifications';
24
24
 
25
- // Theme options
26
25
  const THEME_OPTIONS = [
27
26
  { value: 'dark', label: '深色' },
28
27
  { value: 'light', label: '浅色' },
29
28
  { value: 'system', label: '跟随系统' },
30
29
  ] as const;
31
30
 
32
- const CC_LANGUAGE_OPTIONS = ['en', 'zh', 'zh-TW', 'ja', 'es'] as const;
33
- const CC_LOG_LEVEL_OPTIONS = ['debug', 'info', 'warn', 'error'] as const;
31
+ const CC_LOG_LEVEL_OPTIONS = [
32
+ { value: 'debug', label: 'Debug' },
33
+ { value: 'info', label: 'Info' },
34
+ { value: 'warn', label: 'Warn' },
35
+ { value: 'error', label: 'Error' },
36
+ ];
37
+
34
38
  const CC_ATTACHMENT_OPTIONS = [
35
39
  { value: '', label: '默认' },
36
40
  { value: 'on', label: '开启' },
37
41
  { value: 'off', label: '关闭' },
38
- ] as const;
42
+ ];
39
43
 
40
44
  interface CcGlobalSettingsState {
41
45
  language: string;
@@ -52,6 +56,38 @@ interface CcGlobalSettingsState {
52
56
  rate_limit_window_secs: number;
53
57
  }
54
58
 
59
+ /** Compact number input for SettingRow right side */
60
+ const CompactNum = ({
61
+ value,
62
+ onChange,
63
+ onSave,
64
+ min,
65
+ className,
66
+ }: {
67
+ value: number;
68
+ onChange: (v: number) => void;
69
+ onSave: () => void;
70
+ min?: number;
71
+ className?: string;
72
+ }): React.JSX.Element => (
73
+ <input
74
+ type="number"
75
+ min={min}
76
+ value={value}
77
+ onChange={(e) => onChange(Number(e.target.value) || 0)}
78
+ onBlur={onSave}
79
+ onKeyDown={(e) => {
80
+ if (e.key === 'Enter') onSave();
81
+ }}
82
+ className={cn(
83
+ 'h-8 w-24 rounded-md border bg-transparent px-2 text-right text-xs tabular-nums',
84
+ 'focus:outline-none focus:ring-1 focus:ring-zinc-700',
85
+ className
86
+ )}
87
+ style={{ borderColor: 'var(--color-border-subtle)', color: 'var(--color-text)' }}
88
+ />
89
+ );
90
+
55
91
  interface GeneralSectionProps {
56
92
  readonly safeConfig: SafeConfig;
57
93
  readonly saving: boolean;
@@ -75,7 +111,6 @@ export const GeneralSection = ({
75
111
  const [serverError, setServerError] = useState<string | null>(null);
76
112
  const [copied, setCopied] = useState(false);
77
113
 
78
- // Claude Root state
79
114
  const { connectionMode, fetchProjects, fetchRepositoryGroups } = useStore(
80
115
  useShallow((s) => ({
81
116
  connectionMode: s.connectionMode,
@@ -105,10 +140,7 @@ export const GeneralSection = ({
105
140
  rate_limit_window_secs: 60,
106
141
  });
107
142
  const [ccSettingsLoading, setCcSettingsLoading] = useState(false);
108
- const [ccSettingsSaving, setCcSettingsSaving] = useState(false);
109
- const [ccSettingsMessage, setCcSettingsMessage] = useState<string | null>(null);
110
143
 
111
- // Fetch server status and Claude root info on mount
112
144
  useEffect(() => {
113
145
  void api.httpServer
114
146
  .getStatus()
@@ -136,8 +168,8 @@ export const GeneralSection = ({
136
168
  try {
137
169
  const settings = await api.ccSettings.get();
138
170
  setCcSettings((prev) => ({ ...prev, ...(settings as Partial<CcGlobalSettingsState>) }));
139
- } catch (error) {
140
- setCcSettingsMessage(error instanceof Error ? error.message : '加载运行设置失败');
171
+ } catch {
172
+ // best-effort
141
173
  } finally {
142
174
  setCcSettingsLoading(false);
143
175
  }
@@ -151,19 +183,17 @@ export const GeneralSection = ({
151
183
  setCcSettings((prev) => ({ ...prev, ...patch }));
152
184
  }, []);
153
185
 
154
- const saveCcSettings = useCallback(async () => {
155
- setCcSettingsSaving(true);
156
- setCcSettingsMessage(null);
157
- try {
158
- await api.ccSettings.patch(ccSettings as unknown as Record<string, unknown>);
159
- setCcSettingsMessage('已保存');
160
- setTimeout(() => setCcSettingsMessage(null), 2500);
161
- } catch (error) {
162
- setCcSettingsMessage(error instanceof Error ? error.message : '保存失败');
163
- } finally {
164
- setCcSettingsSaving(false);
165
- }
166
- }, [ccSettings]);
186
+ const autoSaveCcSetting = useCallback(
187
+ async <K extends keyof CcGlobalSettingsState>(key: K, value: CcGlobalSettingsState[K]) => {
188
+ patchCcSettings({ [key]: value });
189
+ try {
190
+ await api.ccSettings.patch({ [key]: value } as unknown as Record<string, unknown>);
191
+ } catch {
192
+ // silent
193
+ }
194
+ },
195
+ [patchCcSettings]
196
+ );
167
197
 
168
198
  const handleServerToggle = useCallback(async (enabled: boolean) => {
169
199
  setServerLoading(true);
@@ -186,7 +216,6 @@ export const GeneralSection = ({
186
216
  setTimeout(() => setCopied(false), 2000);
187
217
  }, [serverUrl]);
188
218
 
189
- // Claude Root handlers
190
219
  const resetWorkspaceForRootChange = useCallback((): void => {
191
220
  useStore.setState({
192
221
  projects: [],
@@ -369,8 +398,20 @@ export const GeneralSection = ({
369
398
  []
370
399
  );
371
400
 
401
+ const saveRateLimit = useCallback(async () => {
402
+ try {
403
+ await api.ccSettings.patch({
404
+ rate_limit_max_messages: ccSettings.rate_limit_max_messages,
405
+ rate_limit_window_secs: ccSettings.rate_limit_window_secs,
406
+ } as Record<string, unknown>);
407
+ } catch {
408
+ // silent
409
+ }
410
+ }, [ccSettings.rate_limit_max_messages, ccSettings.rate_limit_window_secs]);
411
+
372
412
  return (
373
413
  <div>
414
+ {/* Language */}
374
415
  <SettingsSectionHeader title="Agent 语言" />
375
416
  <SettingRow label="语言" description={agentLanguageDescription}>
376
417
  <Combobox
@@ -386,6 +427,7 @@ export const GeneralSection = ({
386
427
  />
387
428
  </SettingRow>
388
429
 
430
+ {/* Appearance */}
389
431
  <SettingsSectionHeader title="外观" />
390
432
  <SettingRow label="主题" description="选择你偏好的界面主题">
391
433
  <div className="inline-flex rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5">
@@ -417,6 +459,8 @@ export const GeneralSection = ({
417
459
  disabled={saving}
418
460
  />
419
461
  </SettingRow>
462
+
463
+ {/* Server Status */}
420
464
  <SettingsSectionHeader title="服务状态" />
421
465
  <div
422
466
  className="mb-2 flex items-center gap-3 rounded-md px-3 py-2.5"
@@ -456,192 +500,110 @@ export const GeneralSection = ({
456
500
  当前为 Web 控制台模式。服务由 Hermit 后端托管,不能在浏览器内启动或关闭。
457
501
  </p>
458
502
 
503
+ {/* Runtime Settings */}
459
504
  <SettingsSectionHeader title="运行设置" />
460
- <div className="space-y-3 rounded-md border border-[var(--color-border)] p-3">
461
- {ccSettingsLoading ? (
462
- <div className="flex items-center gap-2 text-xs text-[var(--color-text-muted)]">
463
- <Loader2 className="size-3.5 animate-spin" />
464
- 正在加载运行设置...
465
- </div>
466
- ) : (
467
- <>
468
- <div className="grid gap-3 md:grid-cols-2">
469
- <div>
470
- <label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
471
- 语言
472
- </label>
473
- <select
474
- value={ccSettings.language}
475
- onChange={(event) => patchCcSettings({ language: event.target.value })}
476
- className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
477
- >
478
- {CC_LANGUAGE_OPTIONS.map((language) => (
479
- <option key={language} value={language}>
480
- {language}
481
- </option>
482
- ))}
483
- </select>
484
- </div>
485
- <div>
486
- <label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
487
- 附件回传
488
- </label>
489
- <select
490
- value={ccSettings.attachment_send}
491
- onChange={(event) => patchCcSettings({ attachment_send: event.target.value })}
492
- className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
493
- >
494
- {CC_ATTACHMENT_OPTIONS.map((option) => (
495
- <option key={option.value} value={option.value}>
496
- {option.label}
497
- </option>
498
- ))}
499
- </select>
500
- </div>
501
- <div>
502
- <label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
503
- 空闲超时(分钟)
504
- </label>
505
- <input
506
- type="number"
507
- min={0}
508
- value={ccSettings.idle_timeout_mins}
509
- onChange={(event) =>
510
- patchCcSettings({ idle_timeout_mins: Number(event.target.value) || 0 })
511
- }
512
- className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
513
- />
514
- </div>
515
- <div>
516
- <label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
517
- 日志等级
518
- </label>
519
- <select
520
- value={ccSettings.log_level}
521
- onChange={(event) => patchCcSettings({ log_level: event.target.value })}
522
- className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
523
- >
524
- {CC_LOG_LEVEL_OPTIONS.map((level) => (
525
- <option key={level} value={level}>
526
- {level}
527
- </option>
528
- ))}
529
- </select>
530
- </div>
531
- </div>
532
-
533
- <div className="grid gap-2 md:grid-cols-2">
534
- <SettingsToggle
535
- enabled={ccSettings.thinking_messages}
536
- onChange={(value) => patchCcSettings({ thinking_messages: value })}
537
- />
538
- <span className="text-xs text-[var(--color-text-secondary)]">显示 Thinking 消息</span>
539
- <SettingsToggle
540
- enabled={ccSettings.tool_messages}
541
- onChange={(value) => patchCcSettings({ tool_messages: value })}
505
+ {ccSettingsLoading ? (
506
+ <div className="flex items-center gap-2 py-3 text-xs text-[var(--color-text-muted)]">
507
+ <Loader2 className="size-3.5 animate-spin" />
508
+ 正在加载...
509
+ </div>
510
+ ) : (
511
+ <>
512
+ <SettingRow label="附件回传" description="控制是否将对话中的附件文件回传给 Agent">
513
+ <SettingsSelect
514
+ value={ccSettings.attachment_send}
515
+ options={CC_ATTACHMENT_OPTIONS}
516
+ onChange={(v) => void autoSaveCcSetting('attachment_send', v)}
517
+ />
518
+ </SettingRow>
519
+ <SettingRow label="空闲超时" description="Agent 空闲多久后自动断开(分钟)">
520
+ <CompactNum
521
+ value={ccSettings.idle_timeout_mins}
522
+ onChange={(v) => patchCcSettings({ idle_timeout_mins: v })}
523
+ onSave={() =>
524
+ void autoSaveCcSetting('idle_timeout_mins', ccSettings.idle_timeout_mins)
525
+ }
526
+ min={0}
527
+ />
528
+ </SettingRow>
529
+ <SettingRow label="日志等级" description="cc-connect 日志输出级别">
530
+ <SettingsSelect
531
+ value={ccSettings.log_level}
532
+ options={CC_LOG_LEVEL_OPTIONS}
533
+ onChange={(v) => void autoSaveCcSetting('log_level', v)}
534
+ />
535
+ </SettingRow>
536
+ <SettingRow label="显示 Thinking 消息" description="在对话中展示 Agent 的思考过程">
537
+ <SettingsToggle
538
+ enabled={ccSettings.thinking_messages}
539
+ onChange={(v) => void autoSaveCcSetting('thinking_messages', v)}
540
+ />
541
+ </SettingRow>
542
+ <SettingRow label="显示工具进度" description="在对话中展示 Agent 调用工具的详细信息">
543
+ <SettingsToggle
544
+ enabled={ccSettings.tool_messages}
545
+ onChange={(v) => void autoSaveCcSetting('tool_messages', v)}
546
+ />
547
+ </SettingRow>
548
+ <SettingRow label="启用流式预览" description="实时预览 Agent 的流式输出内容">
549
+ <SettingsToggle
550
+ enabled={ccSettings.stream_preview_enabled}
551
+ onChange={(v) => void autoSaveCcSetting('stream_preview_enabled', v)}
552
+ />
553
+ </SettingRow>
554
+ <SettingRow label="Thinking 最大长度" description="截断展示的 Thinking 消息最大字符数">
555
+ <CompactNum
556
+ value={ccSettings.thinking_max_len}
557
+ onChange={(v) => patchCcSettings({ thinking_max_len: v })}
558
+ onSave={() => void autoSaveCcSetting('thinking_max_len', ccSettings.thinking_max_len)}
559
+ min={0}
560
+ />
561
+ </SettingRow>
562
+ <SettingRow label="工具消息最大长度" description="截断展示的工具消息最大字符数">
563
+ <CompactNum
564
+ value={ccSettings.tool_max_len}
565
+ onChange={(v) => patchCcSettings({ tool_max_len: v })}
566
+ onSave={() => void autoSaveCcSetting('tool_max_len', ccSettings.tool_max_len)}
567
+ min={0}
568
+ />
569
+ </SettingRow>
570
+ <SettingRow label="预览间隔" description="流式预览刷新间隔(毫秒)">
571
+ <CompactNum
572
+ value={ccSettings.stream_preview_interval_ms}
573
+ onChange={(v) => patchCcSettings({ stream_preview_interval_ms: v })}
574
+ onSave={() =>
575
+ void autoSaveCcSetting(
576
+ 'stream_preview_interval_ms',
577
+ ccSettings.stream_preview_interval_ms
578
+ )
579
+ }
580
+ min={100}
581
+ />
582
+ </SettingRow>
583
+ <SettingRow label="频率限制" description="限制时间窗口内发送的最大消息数">
584
+ <div className="flex items-center gap-1.5 text-xs text-[var(--color-text-muted)]">
585
+ <CompactNum
586
+ value={ccSettings.rate_limit_max_messages}
587
+ onChange={(v) => patchCcSettings({ rate_limit_max_messages: v })}
588
+ onSave={() => void saveRateLimit()}
589
+ min={0}
590
+ className="w-20"
542
591
  />
543
- <span className="text-xs text-[var(--color-text-secondary)]">显示工具进度</span>
544
- <SettingsToggle
545
- enabled={ccSettings.stream_preview_enabled}
546
- onChange={(value) => patchCcSettings({ stream_preview_enabled: value })}
592
+ <span>条 /</span>
593
+ <CompactNum
594
+ value={ccSettings.rate_limit_window_secs}
595
+ onChange={(v) => patchCcSettings({ rate_limit_window_secs: v })}
596
+ onSave={() => void saveRateLimit()}
597
+ min={1}
598
+ className="w-20"
547
599
  />
548
- <span className="text-xs text-[var(--color-text-secondary)]">启用流式预览</span>
549
- </div>
550
-
551
- <div className="grid gap-3 md:grid-cols-2">
552
- <div>
553
- <label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
554
- Thinking 最大长度
555
- </label>
556
- <input
557
- type="number"
558
- min={0}
559
- value={ccSettings.thinking_max_len}
560
- onChange={(event) =>
561
- patchCcSettings({ thinking_max_len: Number(event.target.value) || 0 })
562
- }
563
- className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
564
- />
565
- </div>
566
- <div>
567
- <label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
568
- 工具消息最大长度
569
- </label>
570
- <input
571
- type="number"
572
- min={0}
573
- value={ccSettings.tool_max_len}
574
- onChange={(event) =>
575
- patchCcSettings({ tool_max_len: Number(event.target.value) || 0 })
576
- }
577
- className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
578
- />
579
- </div>
580
- <div>
581
- <label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
582
- 预览间隔(毫秒)
583
- </label>
584
- <input
585
- type="number"
586
- min={100}
587
- value={ccSettings.stream_preview_interval_ms}
588
- onChange={(event) =>
589
- patchCcSettings({
590
- stream_preview_interval_ms: Number(event.target.value) || 100,
591
- })
592
- }
593
- className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
594
- />
595
- </div>
596
- <div>
597
- <label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
598
- 频率限制(条/窗口)
599
- </label>
600
- <div className="grid grid-cols-2 gap-2">
601
- <input
602
- type="number"
603
- min={0}
604
- value={ccSettings.rate_limit_max_messages}
605
- onChange={(event) =>
606
- patchCcSettings({ rate_limit_max_messages: Number(event.target.value) || 0 })
607
- }
608
- className="h-8 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
609
- placeholder="数量"
610
- />
611
- <input
612
- type="number"
613
- min={1}
614
- value={ccSettings.rate_limit_window_secs}
615
- onChange={(event) =>
616
- patchCcSettings({ rate_limit_window_secs: Number(event.target.value) || 1 })
617
- }
618
- className="h-8 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
619
- placeholder="秒"
620
- />
621
- </div>
622
- </div>
600
+ <span>秒</span>
623
601
  </div>
602
+ </SettingRow>
603
+ </>
604
+ )}
624
605
 
625
- <div className="flex items-center gap-3">
626
- <Button size="sm" onClick={() => void saveCcSettings()} disabled={ccSettingsSaving}>
627
- {ccSettingsSaving ? <Loader2 className="mr-1.5 size-3.5 animate-spin" /> : null}
628
- 保存运行设置
629
- </Button>
630
- {ccSettingsMessage ? (
631
- <span
632
- className={`text-xs ${
633
- ccSettingsMessage === '已保存' ? 'text-emerald-400' : 'text-red-400'
634
- }`}
635
- >
636
- {ccSettingsMessage}
637
- </span>
638
- ) : null}
639
- </div>
640
- </>
641
- )}
642
- </div>
643
-
644
- {/* Privacy / Telemetry — only visible when Sentry DSN is baked into the build */}
606
+ {/* Privacy */}
645
607
  {import.meta.env.VITE_SENTRY_DSN && (
646
608
  <>
647
609
  <SettingsSectionHeader title="隐私" />
@@ -0,0 +1,176 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import { Button } from '@renderer/components/ui/button';
4
+ import { SettingRow, SettingsSectionHeader, SettingsToggle } from '../components';
5
+ import type { TaskBusConfig } from '@shared/types/team';
6
+ import { Loader2, Radio, Wifi, WifiOff } from 'lucide-react';
7
+
8
+ export function TaskBusSection(): React.JSX.Element {
9
+ const [enabled, setEnabled] = useState(false);
10
+ const [host, setHost] = useState('127.0.0.1');
11
+ const [port, setPort] = useState(6379);
12
+ const [password, setPassword] = useState('');
13
+ const [loading, setLoading] = useState(true);
14
+ const [connecting, setConnecting] = useState(false);
15
+ const [connected, setConnected] = useState(false);
16
+ const [message, setMessage] = useState<string | null>(null);
17
+
18
+ useEffect(() => {
19
+ fetch('/api/settings/task-bus')
20
+ .then((r) => r.json())
21
+ .then((data: TaskBusConfig) => {
22
+ setEnabled(data.enabled);
23
+ if (data.redis) {
24
+ setHost(data.redis.host ?? '127.0.0.1');
25
+ setPort(data.redis.port ?? 6379);
26
+ setPassword(data.redis.password ?? '');
27
+ }
28
+ })
29
+ .catch(() => {})
30
+ .finally(() => setLoading(false));
31
+ }, []);
32
+
33
+ const save = async (connectRedis = false) => {
34
+ setMessage(null);
35
+ if (connectRedis) setConnecting(true);
36
+ const config: TaskBusConfig = {
37
+ enabled,
38
+ redis: { host, port, password: password || undefined },
39
+ };
40
+ try {
41
+ const res = await fetch('/api/settings/task-bus', {
42
+ method: 'PUT',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify(config),
45
+ });
46
+ const data = await res.json();
47
+ if (connectRedis) {
48
+ setConnected(!!data.connected);
49
+ setMessage(
50
+ data.connected ? 'Redis 连接成功,分布式派发已启用' : 'Redis 连接失败,仅本地派发'
51
+ );
52
+ } else {
53
+ setConnected(false);
54
+ setMessage(enabled ? '已开启,指令已注入到团队工作目录' : '已关闭');
55
+ }
56
+ } catch (err) {
57
+ setMessage(`操作失败: ${err}`);
58
+ } finally {
59
+ setConnecting(false);
60
+ }
61
+ };
62
+
63
+ const toggle = (value: boolean) => {
64
+ setEnabled(value);
65
+ const config: TaskBusConfig = {
66
+ enabled: value,
67
+ redis: { host, port, password: password || undefined },
68
+ };
69
+ fetch('/api/settings/task-bus', {
70
+ method: 'PUT',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify(config),
73
+ })
74
+ .then((r) => r.json())
75
+ .then(() => setMessage(value ? '已开启,指令已注入到团队工作目录' : '已关闭'))
76
+ .catch(() => setMessage('操作失败'));
77
+ };
78
+
79
+ if (loading) {
80
+ return (
81
+ <div className="flex items-center justify-center py-12">
82
+ <Loader2 size={16} className="animate-spin text-[var(--color-text-muted)]" />
83
+ </div>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <div>
89
+ <SettingsSectionHeader title="任务总线" icon={<Radio size={12} />} />
90
+
91
+ <SettingRow
92
+ label="启用任务总线"
93
+ description="开启后自动为所有团队注入跨团队任务派发指令到 CLAUDE.md"
94
+ >
95
+ <SettingsToggle enabled={enabled} onChange={toggle} />
96
+ </SettingRow>
97
+
98
+ {/* Redis */}
99
+ <SettingRow label="Redis" description="可选,配置后启用跨主机分布式派发">
100
+ <div className="flex items-center gap-2">
101
+ {connected ? (
102
+ <span className="flex items-center gap-1 text-xs text-emerald-500">
103
+ <Wifi size={12} />
104
+ 已连接
105
+ </span>
106
+ ) : enabled ? (
107
+ <span className="flex items-center gap-1 text-xs text-[var(--color-text-muted)]">
108
+ <WifiOff size={12} />
109
+ 本地模式
110
+ </span>
111
+ ) : null}
112
+ </div>
113
+ </SettingRow>
114
+
115
+ {enabled && (
116
+ <div className="border-b pb-4" style={{ borderColor: 'var(--color-border-subtle)' }}>
117
+ <div className="space-y-3 px-1 pt-2">
118
+ <div className="flex gap-3">
119
+ <div className="flex-1">
120
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">主机</label>
121
+ <input
122
+ type="text"
123
+ value={host}
124
+ onChange={(e) => setHost(e.target.value)}
125
+ className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-1.5 text-sm outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/30"
126
+ placeholder="127.0.0.1"
127
+ />
128
+ </div>
129
+ <div className="w-24">
130
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">端口</label>
131
+ <input
132
+ type="number"
133
+ value={port}
134
+ onChange={(e) => setPort(Number(e.target.value))}
135
+ className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-1.5 text-sm outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/30"
136
+ placeholder="6379"
137
+ />
138
+ </div>
139
+ </div>
140
+ <div>
141
+ <label className="mb-1 block text-xs text-[var(--color-text-muted)]">密码</label>
142
+ <input
143
+ type="password"
144
+ value={password}
145
+ onChange={(e) => setPassword(e.target.value)}
146
+ className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-1.5 text-sm outline-none focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/30"
147
+ placeholder="可选"
148
+ />
149
+ </div>
150
+ <div className="flex items-center gap-3 pt-1">
151
+ <Button
152
+ size="sm"
153
+ onClick={() => save(true)}
154
+ disabled={connecting}
155
+ className="gap-1.5"
156
+ >
157
+ {connecting && <Loader2 size={12} className="animate-spin" />}
158
+ {connecting ? '连接中...' : '测试连接'}
159
+ </Button>
160
+ {message && <span className="text-xs text-[var(--color-text-muted)]">{message}</span>}
161
+ </div>
162
+ </div>
163
+ </div>
164
+ )}
165
+
166
+ {!enabled && message && (
167
+ <div
168
+ className="border-b py-2 text-xs text-[var(--color-text-muted)]"
169
+ style={{ borderColor: 'var(--color-border-subtle)' }}
170
+ >
171
+ {message}
172
+ </div>
173
+ )}
174
+ </div>
175
+ );
176
+ }