@yancyyu/openhermit 1.6.28 → 1.6.29
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.
- package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
- package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CgG_tjgX.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-DwPTU9lP.js} +1 -1
- package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-7nIrGRzY.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
- package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
- package/dist-renderer/assets/{graph-Enirf-f8.js → graph-BiFNoBjP.js} +1 -1
- package/dist-renderer/assets/{index-AjxP_rE_.js → index-6m1ZAymG.js} +1 -1
- package/dist-renderer/assets/index-BhellmRb.css +1 -0
- package/dist-renderer/assets/{index-DY1zqsb6.js → index-BowUl0Jb.js} +540 -536
- package/dist-renderer/assets/{index-CtlzGepK.js → index-Dp3kJTEe.js} +1 -1
- package/dist-renderer/assets/{index-COZPUWJW.js → index-TOpt_T7A.js} +1 -1
- package/dist-renderer/assets/{index-DdhqolqE.js → index-qNBNjW4K.js} +1 -1
- package/dist-renderer/assets/{index-ChR1D6ZF.js → index-vAykq1H1.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
- package/dist-renderer/assets/{layout-CPFgj98r.js → layout-DNANbrI4.js} +1 -1
- package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-DxEJi1yT.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/server.ts +699 -179
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -95
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
- package/src/main/services/teams-mvp/index.ts +3 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/api/httpClient.ts +67 -0
- package/src/renderer/components/layout/PaneContent.tsx +2 -0
- package/src/renderer/components/layout/SortableTab.tsx +1 -0
- package/src/renderer/components/layout/TabBarActions.tsx +12 -12
- package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +129 -79
- package/src/renderer/components/tasks/TasksView.tsx +343 -0
- package/src/renderer/components/team/TeamDetailView.tsx +20 -98
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
- package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
- package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
- package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
- package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +5 -1
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
- package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
- package/src/renderer/components/team/messages/MessagesPanel.tsx +72 -2
- package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
- package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
- package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
- package/src/renderer/store/slices/scheduleSlice.ts +21 -0
- package/src/renderer/store/slices/teamSlice.ts +59 -23
- package/src/renderer/types/tabs.ts +1 -0
- package/src/shared/types/api.ts +29 -0
- package/src/shared/types/team.ts +104 -1
- package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
- package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
- package/dist-renderer/assets/index-BIOJremZ.css +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-CVd5GNDw.js +0 -162
|
@@ -202,7 +202,9 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
202
202
|
const [connected, setConnected] = useState(false);
|
|
203
203
|
const [message, setMessage] = useState<string | null>(null);
|
|
204
204
|
|
|
205
|
-
const [
|
|
205
|
+
const [collectionEnabled, setCollectionEnabled] = useState(false);
|
|
206
|
+
const [uploadEnabled, setUploadEnabled] = useState(false);
|
|
207
|
+
const [collaborationEnabled, setCollaborationEnabled] = useState(false);
|
|
206
208
|
const [telemetryPlatform, setTelemetryPlatform] = useState('claudecode');
|
|
207
209
|
const [scanning, setScanning] = useState(false);
|
|
208
210
|
const [telemetryStatus, setTelemetryStatus] = useState<TelemetryStatus | null>(null);
|
|
@@ -218,9 +220,11 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
218
220
|
setPassword(data.redis.password ?? '');
|
|
219
221
|
}
|
|
220
222
|
if (data.telemetry) {
|
|
221
|
-
|
|
223
|
+
setCollectionEnabled(data.telemetry.enabled);
|
|
224
|
+
setUploadEnabled(data.telemetry.uploadEnabled ?? false);
|
|
222
225
|
setTelemetryPlatform(data.telemetry.platform ?? 'claudecode');
|
|
223
226
|
}
|
|
227
|
+
setCollaborationEnabled(data.collaboration ?? false);
|
|
224
228
|
})
|
|
225
229
|
.catch(() => {})
|
|
226
230
|
.finally(() => setLoading(false));
|
|
@@ -235,7 +239,7 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
235
239
|
.catch(() => {});
|
|
236
240
|
|
|
237
241
|
const poll = setInterval(() => {
|
|
238
|
-
if (
|
|
242
|
+
if (collectionEnabled) {
|
|
239
243
|
fetch('/api/telemetry/status')
|
|
240
244
|
.then((r) => r.json())
|
|
241
245
|
.then((s: TelemetryStatus) => setTelemetryStatus(s))
|
|
@@ -243,19 +247,23 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
243
247
|
}
|
|
244
248
|
}, 30000);
|
|
245
249
|
return () => clearInterval(poll);
|
|
246
|
-
}, [
|
|
250
|
+
}, [collectionEnabled]);
|
|
247
251
|
|
|
248
252
|
const buildConfig = (
|
|
249
253
|
overrides: Partial<{
|
|
250
254
|
enabled: boolean;
|
|
251
|
-
|
|
255
|
+
collectionEnabled: boolean;
|
|
256
|
+
uploadEnabled: boolean;
|
|
257
|
+
collaborationEnabled: boolean;
|
|
252
258
|
telemetryPlatform: string;
|
|
253
259
|
}> = {}
|
|
254
260
|
): TaskBusConfig => ({
|
|
255
261
|
enabled: overrides.enabled ?? enabled,
|
|
256
262
|
redis: { host, port, password: password || undefined },
|
|
263
|
+
collaboration: overrides.collaborationEnabled ?? collaborationEnabled,
|
|
257
264
|
telemetry: {
|
|
258
|
-
enabled: overrides.
|
|
265
|
+
enabled: overrides.collectionEnabled ?? collectionEnabled,
|
|
266
|
+
uploadEnabled: overrides.uploadEnabled ?? uploadEnabled,
|
|
259
267
|
platform: (overrides.telemetryPlatform ?? telemetryPlatform) as 'claudecode',
|
|
260
268
|
},
|
|
261
269
|
});
|
|
@@ -296,45 +304,58 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
296
304
|
.catch(() => setMessage('操作失败'));
|
|
297
305
|
};
|
|
298
306
|
|
|
299
|
-
const
|
|
307
|
+
const toggleCollection = async (value: boolean) => {
|
|
308
|
+
setCollectionEnabled(value);
|
|
309
|
+
const config = buildConfig({ collectionEnabled: value });
|
|
310
|
+
try {
|
|
311
|
+
await fetch('/api/settings/task-bus', {
|
|
312
|
+
method: 'PUT',
|
|
313
|
+
headers: { 'Content-Type': 'application/json' },
|
|
314
|
+
body: JSON.stringify(config),
|
|
315
|
+
});
|
|
316
|
+
if (value) triggerScan();
|
|
317
|
+
else setTelemetryStatus(null);
|
|
318
|
+
} catch {
|
|
319
|
+
setCollectionEnabled(!value);
|
|
320
|
+
setMessage('操作失败');
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const toggleUpload = async (value: boolean) => {
|
|
300
325
|
if (!value) {
|
|
301
|
-
|
|
302
|
-
const config = buildConfig({
|
|
326
|
+
setUploadEnabled(false);
|
|
327
|
+
const config = buildConfig({ uploadEnabled: false });
|
|
303
328
|
fetch('/api/settings/task-bus', {
|
|
304
329
|
method: 'PUT',
|
|
305
330
|
headers: { 'Content-Type': 'application/json' },
|
|
306
331
|
body: JSON.stringify(config),
|
|
307
332
|
}).catch(() => setMessage('操作失败'));
|
|
308
|
-
setTelemetryStatus(null);
|
|
309
333
|
return;
|
|
310
334
|
}
|
|
311
335
|
|
|
312
|
-
//
|
|
313
|
-
setTelemetryEnabled(true);
|
|
314
|
-
|
|
315
|
-
// Test Redis if not already connected
|
|
336
|
+
// Upload requires Redis
|
|
316
337
|
let redisReady = connected;
|
|
317
338
|
if (!redisReady) {
|
|
318
339
|
setMessage('正在测试 Redis 连接...');
|
|
319
340
|
redisReady = await testRedisConnection();
|
|
320
341
|
if (!redisReady) {
|
|
321
|
-
|
|
342
|
+
setUploadEnabled(false);
|
|
322
343
|
setMessage('Redis 连接失败,无法启用数据上报');
|
|
323
344
|
return;
|
|
324
345
|
}
|
|
325
346
|
}
|
|
326
347
|
|
|
348
|
+
setUploadEnabled(true);
|
|
327
349
|
setMessage(null);
|
|
328
|
-
const config = buildConfig({
|
|
350
|
+
const config = buildConfig({ uploadEnabled: true });
|
|
329
351
|
try {
|
|
330
352
|
await fetch('/api/settings/task-bus', {
|
|
331
353
|
method: 'PUT',
|
|
332
354
|
headers: { 'Content-Type': 'application/json' },
|
|
333
355
|
body: JSON.stringify(config),
|
|
334
356
|
});
|
|
335
|
-
triggerScan();
|
|
336
357
|
} catch {
|
|
337
|
-
|
|
358
|
+
setUploadEnabled(false);
|
|
338
359
|
setMessage('操作失败');
|
|
339
360
|
}
|
|
340
361
|
};
|
|
@@ -349,7 +370,7 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
349
370
|
setTelemetryStatus(result);
|
|
350
371
|
}
|
|
351
372
|
})
|
|
352
|
-
.catch(() => setMessage('
|
|
373
|
+
.catch(() => setMessage('采集失败,请检查本地 Claude Code 会话目录'))
|
|
353
374
|
.finally(() => setScanning(false));
|
|
354
375
|
};
|
|
355
376
|
|
|
@@ -362,6 +383,18 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
362
383
|
}).catch(() => {});
|
|
363
384
|
};
|
|
364
385
|
|
|
386
|
+
const toggleCollaboration = (value: boolean) => {
|
|
387
|
+
setCollaborationEnabled(value);
|
|
388
|
+
const config = buildConfig({ collaborationEnabled: value });
|
|
389
|
+
fetch('/api/settings/task-bus', {
|
|
390
|
+
method: 'PUT',
|
|
391
|
+
headers: { 'Content-Type': 'application/json' },
|
|
392
|
+
body: JSON.stringify(config),
|
|
393
|
+
})
|
|
394
|
+
.then(() => setMessage(value ? '已开启分布式团队协作' : '已关闭分布式团队协作'))
|
|
395
|
+
.catch(() => setMessage('操作失败'));
|
|
396
|
+
};
|
|
397
|
+
|
|
365
398
|
if (loading) {
|
|
366
399
|
return (
|
|
367
400
|
<div className="flex items-center justify-center py-12">
|
|
@@ -372,11 +405,65 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
372
405
|
|
|
373
406
|
return (
|
|
374
407
|
<div>
|
|
408
|
+
<SettingsSectionHeader title="本地数据采集" icon={<BarChart3 size={12} />} />
|
|
409
|
+
|
|
410
|
+
<SettingRow
|
|
411
|
+
label="数据采集"
|
|
412
|
+
description="扫描本机 ~/.claude/projects 会话文件,采集使用指标;不需要 Redis,也不会上传对话内容"
|
|
413
|
+
>
|
|
414
|
+
<div className="flex items-center gap-2">
|
|
415
|
+
<select
|
|
416
|
+
value={telemetryPlatform}
|
|
417
|
+
onChange={(e) => {
|
|
418
|
+
const nextPlatform = e.target.value;
|
|
419
|
+
setTelemetryPlatform(nextPlatform);
|
|
420
|
+
saveTelemetryPlatform(nextPlatform);
|
|
421
|
+
}}
|
|
422
|
+
className="rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1 text-xs outline-none focus:border-indigo-500/50"
|
|
423
|
+
>
|
|
424
|
+
<option value="claudecode">Claude Code</option>
|
|
425
|
+
</select>
|
|
426
|
+
<SettingsToggle
|
|
427
|
+
enabled={collectionEnabled}
|
|
428
|
+
onChange={(value) => void toggleCollection(value)}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
</SettingRow>
|
|
432
|
+
|
|
433
|
+
{collectionEnabled && (
|
|
434
|
+
<>
|
|
435
|
+
<div
|
|
436
|
+
className="flex items-center gap-3 border-b py-3"
|
|
437
|
+
style={{ borderColor: 'var(--color-border-subtle)' }}
|
|
438
|
+
>
|
|
439
|
+
<Button
|
|
440
|
+
size="sm"
|
|
441
|
+
variant="outline"
|
|
442
|
+
onClick={triggerScan}
|
|
443
|
+
disabled={scanning}
|
|
444
|
+
className="gap-1.5"
|
|
445
|
+
>
|
|
446
|
+
{scanning ? <Loader2 size={12} className="animate-spin" /> : <BarChart3 size={12} />}
|
|
447
|
+
{scanning ? '采集中...' : '立即采集'}
|
|
448
|
+
</Button>
|
|
449
|
+
<span className="text-[10px] text-[var(--color-text-muted)]">
|
|
450
|
+
本地扫描,不依赖团队总线或 Redis。
|
|
451
|
+
</span>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
{telemetryStatus && (
|
|
455
|
+
<div className="py-3">
|
|
456
|
+
<UsageDashboard status={telemetryStatus} />
|
|
457
|
+
</div>
|
|
458
|
+
)}
|
|
459
|
+
</>
|
|
460
|
+
)}
|
|
461
|
+
|
|
375
462
|
<SettingsSectionHeader title="团队总线" icon={<Radio size={12} />} />
|
|
376
463
|
|
|
377
464
|
<SettingRow
|
|
378
465
|
label="启用团队总线"
|
|
379
|
-
description="
|
|
466
|
+
description="用于 Redis 连接、数据上报和跨团队协作;本地数据采集不依赖它"
|
|
380
467
|
>
|
|
381
468
|
<SettingsToggle enabled={enabled} onChange={toggle} />
|
|
382
469
|
</SettingRow>
|
|
@@ -388,7 +475,9 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
388
475
|
<div className="flex items-center gap-2 px-1 pb-2">
|
|
389
476
|
<span className="text-sm font-medium text-red-500">*</span>
|
|
390
477
|
<span className="text-sm font-medium">Redis</span>
|
|
391
|
-
<span className="text-xs text-[var(--color-text-muted)]"
|
|
478
|
+
<span className="text-xs text-[var(--color-text-muted)]">
|
|
479
|
+
(上报和跨团队协作必填)
|
|
480
|
+
</span>
|
|
392
481
|
<div className="ml-auto flex items-center gap-2">
|
|
393
482
|
{connected ? (
|
|
394
483
|
<span className="flex items-center gap-1 text-xs text-emerald-500">
|
|
@@ -465,78 +554,39 @@ export function TaskBusSection(): React.JSX.Element {
|
|
|
465
554
|
</div>
|
|
466
555
|
</div>
|
|
467
556
|
|
|
468
|
-
{/*
|
|
469
|
-
<div
|
|
470
|
-
<SettingRow
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
<select
|
|
476
|
-
value={telemetryPlatform}
|
|
477
|
-
onChange={(e) => {
|
|
478
|
-
const nextPlatform = e.target.value;
|
|
479
|
-
setTelemetryPlatform(nextPlatform);
|
|
480
|
-
saveTelemetryPlatform(nextPlatform);
|
|
481
|
-
}}
|
|
482
|
-
className="rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1 text-xs outline-none focus:border-indigo-500/50"
|
|
483
|
-
>
|
|
484
|
-
<option value="claudecode">Claude Code</option>
|
|
485
|
-
</select>
|
|
486
|
-
<SettingsToggle
|
|
487
|
-
enabled={telemetryEnabled}
|
|
488
|
-
onChange={(value) => void toggleTelemetry(value)}
|
|
489
|
-
/>
|
|
490
|
-
</div>
|
|
557
|
+
{/* 数据上报 - 依赖 Redis,最下面 */}
|
|
558
|
+
<div>
|
|
559
|
+
<SettingRow label="数据上报" description="将采集数据上报到 Redis,供团队看板使用">
|
|
560
|
+
<SettingsToggle
|
|
561
|
+
enabled={uploadEnabled}
|
|
562
|
+
onChange={(value) => void toggleUpload(value)}
|
|
563
|
+
/>
|
|
491
564
|
</SettingRow>
|
|
492
565
|
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
<
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
variant="outline"
|
|
499
|
-
onClick={triggerScan}
|
|
500
|
-
disabled={scanning}
|
|
501
|
-
className="gap-1.5"
|
|
502
|
-
>
|
|
503
|
-
{scanning ? (
|
|
504
|
-
<Loader2 size={12} className="animate-spin" />
|
|
505
|
-
) : (
|
|
506
|
-
<BarChart3 size={12} />
|
|
507
|
-
)}
|
|
508
|
-
{scanning ? '采集中...' : '立即采集'}
|
|
509
|
-
</Button>
|
|
510
|
-
<span className="text-[10px] text-[var(--color-text-muted)]">
|
|
511
|
-
扫描本地 ~/.claude/projects 下的会话文件
|
|
512
|
-
</span>
|
|
513
|
-
</div>
|
|
514
|
-
|
|
515
|
-
{telemetryStatus && (
|
|
516
|
-
<div className="py-3">
|
|
517
|
-
<UsageDashboard status={telemetryStatus} />
|
|
518
|
-
</div>
|
|
519
|
-
)}
|
|
520
|
-
</>
|
|
566
|
+
{!connected && (
|
|
567
|
+
<div className="flex items-center gap-2 px-1 py-2 text-xs text-amber-500">
|
|
568
|
+
<AlertCircle size={12} />
|
|
569
|
+
<span>数据上报需要 Redis;请先配置并测试 Redis 连接。</span>
|
|
570
|
+
</div>
|
|
521
571
|
)}
|
|
522
572
|
</div>
|
|
523
573
|
|
|
524
|
-
{/*
|
|
574
|
+
{/* 分布式团队协作 - 最下面 */}
|
|
525
575
|
<div>
|
|
526
576
|
<SettingRow
|
|
527
|
-
label="
|
|
528
|
-
description="
|
|
577
|
+
label="分布式团队协作"
|
|
578
|
+
description="开启后 Hermit 平台识别 @团队 并创建跨团队协作任务;Agent 只读取团队列表,不直接派发"
|
|
529
579
|
>
|
|
530
580
|
<SettingsToggle
|
|
531
|
-
enabled={
|
|
532
|
-
onChange={(value) => void
|
|
581
|
+
enabled={collaborationEnabled}
|
|
582
|
+
onChange={(value) => void toggleCollaboration(value)}
|
|
533
583
|
/>
|
|
534
584
|
</SettingRow>
|
|
535
585
|
|
|
536
|
-
{!connected && (
|
|
586
|
+
{!connected && collaborationEnabled && (
|
|
537
587
|
<div className="flex items-center gap-2 px-1 py-2 text-xs text-amber-500">
|
|
538
588
|
<AlertCircle size={12} />
|
|
539
|
-
<span
|
|
589
|
+
<span>跨团队协作需要 Redis 连接。</span>
|
|
540
590
|
</div>
|
|
541
591
|
)}
|
|
542
592
|
</div>
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@renderer/components/ui/button';
|
|
4
|
+
import { cn } from '@renderer/lib/utils';
|
|
5
|
+
import { useStore } from '@renderer/store';
|
|
6
|
+
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
|
|
7
|
+
import { Calendar, CheckCircle2, Circle, Columns3, Loader2, RefreshCw } from 'lucide-react';
|
|
8
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
9
|
+
|
|
10
|
+
import { SchedulesView } from '../schedules/SchedulesView';
|
|
11
|
+
|
|
12
|
+
import type { GlobalTask, TeamTaskStatus } from '@shared/types';
|
|
13
|
+
|
|
14
|
+
type TasksSubTab = 'overview' | 'schedules';
|
|
15
|
+
type OverviewStatus = Extract<TeamTaskStatus, 'pending' | 'in_progress' | 'completed'>;
|
|
16
|
+
|
|
17
|
+
const SUB_TABS: { id: TasksSubTab; label: string; icon: React.ReactNode }[] = [
|
|
18
|
+
{ id: 'overview', label: '任务总览', icon: <Columns3 size={14} /> },
|
|
19
|
+
{ id: 'schedules', label: '定时任务', icon: <Calendar size={14} /> },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const COLUMNS: {
|
|
23
|
+
id: OverviewStatus;
|
|
24
|
+
title: string;
|
|
25
|
+
icon: React.ReactNode;
|
|
26
|
+
headerBg: string;
|
|
27
|
+
bodyBg: string;
|
|
28
|
+
}[] = [
|
|
29
|
+
{
|
|
30
|
+
id: 'pending',
|
|
31
|
+
title: 'TODO',
|
|
32
|
+
icon: <Circle size={14} className="shrink-0 text-[var(--color-text-muted)]" />,
|
|
33
|
+
headerBg: 'rgba(59, 130, 246, 0.22)',
|
|
34
|
+
bodyBg: 'rgba(59, 130, 246, 0.05)',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'in_progress',
|
|
38
|
+
title: 'IN PROGRESS',
|
|
39
|
+
icon: <Loader2 size={14} className="shrink-0 text-[var(--color-text-muted)]" />,
|
|
40
|
+
headerBg: 'rgba(234, 179, 8, 0.24)',
|
|
41
|
+
bodyBg: 'rgba(234, 179, 8, 0.06)',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'completed',
|
|
45
|
+
title: 'DONE',
|
|
46
|
+
icon: <CheckCircle2 size={14} className="shrink-0 text-[var(--color-text-muted)]" />,
|
|
47
|
+
headerBg: 'rgba(34, 197, 94, 0.22)',
|
|
48
|
+
bodyBg: 'rgba(34, 197, 94, 0.05)',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function isOverviewStatus(status: TeamTaskStatus): status is OverviewStatus {
|
|
53
|
+
return status === 'pending' || status === 'in_progress' || status === 'completed';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getTaskUpdatedAt(task: GlobalTask): number {
|
|
57
|
+
const raw = task.updatedAt ?? task.createdAt;
|
|
58
|
+
const time = raw ? new Date(raw).getTime() : 0;
|
|
59
|
+
return Number.isFinite(time) ? time : 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildOptionLabel(value: string | null | undefined, fallback: string): string {
|
|
63
|
+
const trimmed = value?.trim();
|
|
64
|
+
return trimmed && trimmed.length > 0 ? trimmed : fallback;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const TasksView = (): React.JSX.Element => {
|
|
68
|
+
const [activeTab, setActiveTab] = useState<TasksSubTab>('overview');
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex h-full flex-col">
|
|
72
|
+
<div className="flex items-center border-b border-[var(--color-border)] px-4 pt-2">
|
|
73
|
+
{SUB_TABS.map((tab) => (
|
|
74
|
+
<button
|
|
75
|
+
key={tab.id}
|
|
76
|
+
onClick={() => setActiveTab(tab.id)}
|
|
77
|
+
className={cn(
|
|
78
|
+
'flex items-center gap-1.5 border-b-2 px-4 pb-2 text-sm font-medium transition-colors',
|
|
79
|
+
activeTab === tab.id
|
|
80
|
+
? 'border-[var(--color-primary)] text-[var(--color-text)]'
|
|
81
|
+
: 'border-transparent text-[var(--color-text-muted)] hover:text-[var(--color-text)]'
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{tab.icon}
|
|
85
|
+
{tab.label}
|
|
86
|
+
</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
<div className="flex-1 overflow-auto">
|
|
90
|
+
{activeTab === 'overview' && <TaskOverviewPool />}
|
|
91
|
+
{activeTab === 'schedules' && <SchedulesView />}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const TaskOverviewPool = (): React.JSX.Element => {
|
|
98
|
+
const {
|
|
99
|
+
globalTasks,
|
|
100
|
+
globalTasksLoading,
|
|
101
|
+
globalTasksInitialized,
|
|
102
|
+
fetchAllTasks,
|
|
103
|
+
openGlobalTaskDetail,
|
|
104
|
+
} = useStore(
|
|
105
|
+
useShallow((s) => ({
|
|
106
|
+
globalTasks: s.globalTasks,
|
|
107
|
+
globalTasksLoading: s.globalTasksLoading,
|
|
108
|
+
globalTasksInitialized: s.globalTasksInitialized,
|
|
109
|
+
fetchAllTasks: s.fetchAllTasks,
|
|
110
|
+
openGlobalTaskDetail: s.openGlobalTaskDetail,
|
|
111
|
+
}))
|
|
112
|
+
);
|
|
113
|
+
const [teamFilter, setTeamFilter] = useState('all');
|
|
114
|
+
const [statusFilter, setStatusFilter] = useState<'all' | OverviewStatus>('all');
|
|
115
|
+
const [ownerFilter, setOwnerFilter] = useState('all');
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
void fetchAllTasks();
|
|
119
|
+
}, [fetchAllTasks]);
|
|
120
|
+
|
|
121
|
+
const overviewTasks = useMemo(
|
|
122
|
+
() => globalTasks.filter((task) => isOverviewStatus(task.status) && !task.teamDeleted),
|
|
123
|
+
[globalTasks]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const teamOptions = useMemo(
|
|
127
|
+
() =>
|
|
128
|
+
Array.from(
|
|
129
|
+
new Map(overviewTasks.map((task) => [task.teamName, task.teamDisplayName])).entries()
|
|
130
|
+
).sort((a, b) => a[1].localeCompare(b[1])),
|
|
131
|
+
[overviewTasks]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const ownerOptions = useMemo(() => {
|
|
135
|
+
const owners = new Set<string>();
|
|
136
|
+
for (const task of overviewTasks) {
|
|
137
|
+
if (task.owner?.trim()) owners.add(task.owner.trim());
|
|
138
|
+
}
|
|
139
|
+
return Array.from(owners).sort((a, b) => a.localeCompare(b));
|
|
140
|
+
}, [overviewTasks]);
|
|
141
|
+
|
|
142
|
+
const filteredTasks = useMemo(
|
|
143
|
+
() =>
|
|
144
|
+
overviewTasks
|
|
145
|
+
.filter((task) => teamFilter === 'all' || task.teamName === teamFilter)
|
|
146
|
+
.filter((task) => statusFilter === 'all' || task.status === statusFilter)
|
|
147
|
+
.filter((task) => ownerFilter === 'all' || task.owner === ownerFilter)
|
|
148
|
+
.sort((a, b) => getTaskUpdatedAt(b) - getTaskUpdatedAt(a)),
|
|
149
|
+
[overviewTasks, ownerFilter, statusFilter, teamFilter]
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const grouped = useMemo(() => {
|
|
153
|
+
const map = new Map<OverviewStatus, GlobalTask[]>();
|
|
154
|
+
for (const column of COLUMNS) {
|
|
155
|
+
map.set(column.id, []);
|
|
156
|
+
}
|
|
157
|
+
for (const task of filteredTasks) {
|
|
158
|
+
if (isOverviewStatus(task.status)) {
|
|
159
|
+
map.get(task.status)?.push(task);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return map;
|
|
163
|
+
}, [filteredTasks]);
|
|
164
|
+
|
|
165
|
+
const clearFilters = useCallback(() => {
|
|
166
|
+
setTeamFilter('all');
|
|
167
|
+
setStatusFilter('all');
|
|
168
|
+
setOwnerFilter('all');
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
if (globalTasksLoading && !globalTasksInitialized) {
|
|
172
|
+
return (
|
|
173
|
+
<div className="flex h-full items-center justify-center text-sm text-[var(--color-text-muted)]">
|
|
174
|
+
加载团队任务…
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="flex h-full min-w-0 flex-col gap-3 p-4">
|
|
181
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
182
|
+
<div className="min-w-[180px]">
|
|
183
|
+
<label
|
|
184
|
+
htmlFor="tasks-overview-team-filter"
|
|
185
|
+
className="mb-1 block text-[11px] font-medium text-[var(--color-text-muted)]"
|
|
186
|
+
>
|
|
187
|
+
团队
|
|
188
|
+
</label>
|
|
189
|
+
<select
|
|
190
|
+
id="tasks-overview-team-filter"
|
|
191
|
+
value={teamFilter}
|
|
192
|
+
onChange={(event) => setTeamFilter(event.target.value)}
|
|
193
|
+
className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
|
|
194
|
+
>
|
|
195
|
+
<option value="all">全部团队</option>
|
|
196
|
+
{teamOptions.map(([teamName, displayName]) => (
|
|
197
|
+
<option key={teamName} value={teamName}>
|
|
198
|
+
{displayName}
|
|
199
|
+
</option>
|
|
200
|
+
))}
|
|
201
|
+
</select>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div className="min-w-[160px]">
|
|
205
|
+
<label
|
|
206
|
+
htmlFor="tasks-overview-status-filter"
|
|
207
|
+
className="mb-1 block text-[11px] font-medium text-[var(--color-text-muted)]"
|
|
208
|
+
>
|
|
209
|
+
状态
|
|
210
|
+
</label>
|
|
211
|
+
<select
|
|
212
|
+
id="tasks-overview-status-filter"
|
|
213
|
+
value={statusFilter}
|
|
214
|
+
onChange={(event) => setStatusFilter(event.target.value as 'all' | OverviewStatus)}
|
|
215
|
+
className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
|
|
216
|
+
>
|
|
217
|
+
<option value="all">全部状态</option>
|
|
218
|
+
<option value="pending">TODO</option>
|
|
219
|
+
<option value="in_progress">IN PROGRESS</option>
|
|
220
|
+
<option value="completed">DONE</option>
|
|
221
|
+
</select>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div className="min-w-[160px]">
|
|
225
|
+
<label
|
|
226
|
+
htmlFor="tasks-overview-owner-filter"
|
|
227
|
+
className="mb-1 block text-[11px] font-medium text-[var(--color-text-muted)]"
|
|
228
|
+
>
|
|
229
|
+
负责人
|
|
230
|
+
</label>
|
|
231
|
+
<select
|
|
232
|
+
id="tasks-overview-owner-filter"
|
|
233
|
+
value={ownerFilter}
|
|
234
|
+
onChange={(event) => setOwnerFilter(event.target.value)}
|
|
235
|
+
className="h-8 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-2 text-xs text-[var(--color-text)]"
|
|
236
|
+
>
|
|
237
|
+
<option value="all">全部负责人</option>
|
|
238
|
+
{ownerOptions.map((owner) => (
|
|
239
|
+
<option key={owner} value={owner}>
|
|
240
|
+
{owner}
|
|
241
|
+
</option>
|
|
242
|
+
))}
|
|
243
|
+
</select>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" onClick={clearFilters}>
|
|
247
|
+
清空筛选
|
|
248
|
+
</Button>
|
|
249
|
+
<Button
|
|
250
|
+
variant="ghost"
|
|
251
|
+
size="sm"
|
|
252
|
+
className="ml-auto h-8 gap-1.5 text-xs text-[var(--color-text-muted)]"
|
|
253
|
+
onClick={() => void fetchAllTasks()}
|
|
254
|
+
>
|
|
255
|
+
<RefreshCw size={12} />
|
|
256
|
+
刷新
|
|
257
|
+
</Button>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden pb-6">
|
|
261
|
+
<div className="grid min-w-[900px] grid-cols-3 items-start gap-3">
|
|
262
|
+
{COLUMNS.map((column) => {
|
|
263
|
+
const tasks = grouped.get(column.id) ?? [];
|
|
264
|
+
return (
|
|
265
|
+
<section
|
|
266
|
+
key={column.id}
|
|
267
|
+
className="relative rounded-md"
|
|
268
|
+
style={{ backgroundColor: column.bodyBg }}
|
|
269
|
+
>
|
|
270
|
+
{tasks.length > 0 ? (
|
|
271
|
+
<span className="absolute -right-2 -top-2 z-10 min-w-5 rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0 text-center text-[10px] font-medium leading-5 text-[var(--color-text-secondary)] ring-1 ring-[var(--color-border)]">
|
|
272
|
+
{tasks.length}
|
|
273
|
+
</span>
|
|
274
|
+
) : null}
|
|
275
|
+
<header
|
|
276
|
+
className="rounded-t-md px-3 py-2"
|
|
277
|
+
style={{ backgroundColor: column.headerBg }}
|
|
278
|
+
>
|
|
279
|
+
<h4 className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text)]">
|
|
280
|
+
{column.icon}
|
|
281
|
+
{column.title}
|
|
282
|
+
</h4>
|
|
283
|
+
</header>
|
|
284
|
+
<div className="flex flex-col gap-1.5 p-2">
|
|
285
|
+
{tasks.length === 0 ? (
|
|
286
|
+
<div className="rounded-md border border-dashed border-[var(--color-border)] p-3 text-xs text-[var(--color-text-muted)]">
|
|
287
|
+
No tasks
|
|
288
|
+
</div>
|
|
289
|
+
) : (
|
|
290
|
+
tasks.map((task) => (
|
|
291
|
+
<GlobalOverviewTaskCard
|
|
292
|
+
key={`${task.teamName}:${task.id}`}
|
|
293
|
+
task={task}
|
|
294
|
+
onOpen={() => openGlobalTaskDetail(task.teamName, task.id)}
|
|
295
|
+
/>
|
|
296
|
+
))
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
</section>
|
|
300
|
+
);
|
|
301
|
+
})}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const GlobalOverviewTaskCard = ({
|
|
309
|
+
task,
|
|
310
|
+
onOpen,
|
|
311
|
+
}: {
|
|
312
|
+
task: GlobalTask;
|
|
313
|
+
onOpen: () => void;
|
|
314
|
+
}): React.JSX.Element => {
|
|
315
|
+
const ownerLabel = buildOptionLabel(task.owner, '未分配');
|
|
316
|
+
const dispatchFrom = task.dispatchMeta?.originTeam;
|
|
317
|
+
const dispatchTo = task.dispatchMeta?.targetTeam;
|
|
318
|
+
return (
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
className="relative w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-1.5 py-3 text-left text-xs transition-colors hover:border-[var(--color-border-emphasis)]"
|
|
322
|
+
onClick={onOpen}
|
|
323
|
+
>
|
|
324
|
+
<span className="absolute left-[3px] top-[2px] text-[9px] leading-none text-[var(--color-text-muted)]">
|
|
325
|
+
#{task.displayId ?? deriveTaskDisplayId(task.id)}
|
|
326
|
+
</span>
|
|
327
|
+
<div className="mb-2 pt-[11px]">
|
|
328
|
+
<h5 className="line-clamp-2 text-xs font-medium text-[var(--color-text)]">
|
|
329
|
+
{task.subject}
|
|
330
|
+
</h5>
|
|
331
|
+
{task.dispatchMeta ? (
|
|
332
|
+
<span className="mt-1 inline-flex items-center rounded-full bg-yellow-500/15 px-1.5 py-0.5 text-[10px] font-medium text-yellow-600 dark:text-yellow-400">
|
|
333
|
+
{dispatchFrom} 给 {dispatchTo} 派单
|
|
334
|
+
</span>
|
|
335
|
+
) : null}
|
|
336
|
+
</div>
|
|
337
|
+
<div className="flex flex-wrap items-center gap-1.5 text-[10px] text-[var(--color-text-muted)]">
|
|
338
|
+
<span className="rounded bg-white/5 px-1.5 py-0.5">{task.teamDisplayName}</span>
|
|
339
|
+
<span className="rounded bg-white/5 px-1.5 py-0.5">{ownerLabel}</span>
|
|
340
|
+
</div>
|
|
341
|
+
</button>
|
|
342
|
+
);
|
|
343
|
+
};
|