@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.
Files changed (96) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CgG_tjgX.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-DwPTU9lP.js} +1 -1
  5. package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-7nIrGRzY.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
  9. package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
  20. package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
  30. package/dist-renderer/assets/{graph-Enirf-f8.js → graph-BiFNoBjP.js} +1 -1
  31. package/dist-renderer/assets/{index-AjxP_rE_.js → index-6m1ZAymG.js} +1 -1
  32. package/dist-renderer/assets/index-BhellmRb.css +1 -0
  33. package/dist-renderer/assets/{index-DY1zqsb6.js → index-BowUl0Jb.js} +540 -536
  34. package/dist-renderer/assets/{index-CtlzGepK.js → index-Dp3kJTEe.js} +1 -1
  35. package/dist-renderer/assets/{index-COZPUWJW.js → index-TOpt_T7A.js} +1 -1
  36. package/dist-renderer/assets/{index-DdhqolqE.js → index-qNBNjW4K.js} +1 -1
  37. package/dist-renderer/assets/{index-ChR1D6ZF.js → index-vAykq1H1.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
  41. package/dist-renderer/assets/{layout-CPFgj98r.js → layout-DNANbrI4.js} +1 -1
  42. package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-DxEJi1yT.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +1 -1
  56. package/src/main/server.ts +699 -179
  57. package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
  58. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  59. package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -95
  60. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  61. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  62. package/src/main/services/teams-mvp/index.ts +3 -0
  63. package/src/renderer/App.tsx +5 -0
  64. package/src/renderer/api/httpClient.ts +67 -0
  65. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  66. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  67. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  68. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  69. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  70. package/src/renderer/components/settings/sections/TaskBusSection.tsx +129 -79
  71. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  72. package/src/renderer/components/team/TeamDetailView.tsx +20 -98
  73. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  74. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  75. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  76. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  77. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  78. package/src/renderer/components/team/kanban/KanbanBoard.tsx +5 -1
  79. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  80. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  81. package/src/renderer/components/team/messages/MessagesPanel.tsx +72 -2
  82. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  83. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  84. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  85. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  86. package/src/renderer/store/slices/teamSlice.ts +59 -23
  87. package/src/renderer/types/tabs.ts +1 -0
  88. package/src/shared/types/api.ts +29 -0
  89. package/src/shared/types/team.ts +104 -1
  90. package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
  91. package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
  92. package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
  93. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
  94. package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
  95. package/dist-renderer/assets/index-BIOJremZ.css +0 -1
  96. package/dist-renderer/assets/treemap-GDKQZRPO-CVd5GNDw.js +0 -162
@@ -211,6 +211,7 @@ export const MessagesPanel = memo(function MessagesPanel({
211
211
  messagesState,
212
212
  loadOlderTeamMessages,
213
213
  refreshTeamMessagesHead,
214
+ addOptimisticTeamMessage,
214
215
  } = useStore(
215
216
  useShallow((s) => ({
216
217
  sendTeamMessage: s.sendTeamMessage,
@@ -226,6 +227,7 @@ export const MessagesPanel = memo(function MessagesPanel({
226
227
  messagesState: teamName ? s.teamMessagesByName[teamName] : undefined,
227
228
  loadOlderTeamMessages: s.loadOlderTeamMessages,
228
229
  refreshTeamMessagesHead: s.refreshTeamMessagesHead,
230
+ addOptimisticTeamMessage: s.addOptimisticTeamMessage,
229
231
  }))
230
232
  );
231
233
  const bootstrapHeadRefreshAttemptedForTeamRef = useRef<string | null>(null);
@@ -534,7 +536,7 @@ export const MessagesPanel = memo(function MessagesPanel({
534
536
  const sessionScopedMessages = useMemo(() => {
535
537
  const newestFirst = (items: InboxMessage[]) =>
536
538
  [...items].sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
537
- if (!selectedSessionKey) return [];
539
+ if (!selectedSessionKey) return newestFirst(effectiveMessages);
538
540
  if (selectedSession && !selectedIsHermitLocalSession) {
539
541
  if (!selectedSessionDetail) {
540
542
  return [];
@@ -731,12 +733,77 @@ export const MessagesPanel = memo(function MessagesPanel({
731
733
  fromMember: 'user',
732
734
  toTeam,
733
735
  text,
736
+ sessionKey:
737
+ selectedSessionKey && selectedSessionKey !== '__unassigned__'
738
+ ? selectedSessionKey
739
+ : undefined,
734
740
  taskRefs,
735
741
  actionMode,
736
742
  summary,
737
743
  });
738
744
  },
739
- [teamName, sendCrossTeamMessage]
745
+ [teamName, selectedSessionKey, sendCrossTeamMessage]
746
+ );
747
+
748
+ const handleDispatchTaskToTeam = useCallback(
749
+ async (toTeam: string, subject: string, description: string) => {
750
+ const now = Date.now();
751
+ const optimisticMessageId = `optimistic-cross-team-${now}`;
752
+ addOptimisticTeamMessage(teamName, {
753
+ from: 'user',
754
+ to: toTeam,
755
+ text: `@${toTeam} ${subject}`,
756
+ timestamp: new Date(now).toISOString(),
757
+ read: true,
758
+ messageId: optimisticMessageId,
759
+ source: 'cross_team_sent',
760
+ session:
761
+ selectedSessionKey && selectedSessionKey !== '__unassigned__'
762
+ ? { key: selectedSessionKey }
763
+ : undefined,
764
+ });
765
+ try {
766
+ await sendCrossTeamMessage({
767
+ fromTeam: teamName,
768
+ fromMember: 'user',
769
+ toTeam,
770
+ text: description,
771
+ messageId: optimisticMessageId,
772
+ });
773
+ } catch (error) {
774
+ const rawMessage = error instanceof Error ? error.message : '跨团队任务派发失败';
775
+ const readableMessage = rawMessage.includes('Redis not configured')
776
+ ? '无法派发给其他团队:Redis 未配置或未连接。请先在设置里开启团队总线并配置 Redis。'
777
+ : rawMessage.includes('Distributed collaboration is not enabled')
778
+ ? '无法派发给其他团队:团队总线/分布式团队协作未开启。请先在设置里开启。'
779
+ : `无法派发给 ${toTeam}:${rawMessage}`;
780
+ addOptimisticTeamMessage(teamName, {
781
+ from: 'system',
782
+ to: 'user',
783
+ text: readableMessage,
784
+ timestamp: new Date(Date.now()).toISOString(),
785
+ read: true,
786
+ messageId: `optimistic-cross-team-error-${Date.now()}`,
787
+ source: 'system_notification',
788
+ });
789
+ window.dispatchEvent(new CustomEvent('collab:refresh'));
790
+ await refreshTeamMessagesHead(teamName);
791
+ return false;
792
+ }
793
+ window.dispatchEvent(new CustomEvent('collab:refresh'));
794
+ await refreshTeamMessagesHead(teamName);
795
+ window.setTimeout(() => {
796
+ void refreshTeamMessagesHead(teamName);
797
+ }, 300);
798
+ return true;
799
+ },
800
+ [
801
+ addOptimisticTeamMessage,
802
+ teamName,
803
+ refreshTeamMessagesHead,
804
+ selectedSessionKey,
805
+ sendCrossTeamMessage,
806
+ ]
740
807
  );
741
808
 
742
809
  const moveToInline = useCallback(() => {
@@ -869,6 +936,7 @@ export const MessagesPanel = memo(function MessagesPanel({
869
936
  onSessionChange={setSelectedSessionKey}
870
937
  textareaRef={composerTextareaRef}
871
938
  onSend={handleSend}
939
+ onDispatchTask={handleDispatchTaskToTeam}
872
940
  />
873
941
  <StatusBlock
874
942
  members={members}
@@ -1065,6 +1133,7 @@ export const MessagesPanel = memo(function MessagesPanel({
1065
1133
  onSessionChange={setSelectedSessionKey}
1066
1134
  textareaRef={composerTextareaRef}
1067
1135
  onSend={handleSend}
1136
+ onDispatchTask={handleDispatchTaskToTeam}
1068
1137
  />
1069
1138
  <StatusBlock
1070
1139
  members={members}
@@ -1352,6 +1421,7 @@ export const MessagesPanel = memo(function MessagesPanel({
1352
1421
  onSessionChange={setSelectedSessionKey}
1353
1422
  textareaRef={composerTextareaRef}
1354
1423
  onSend={handleSend}
1424
+ onDispatchTask={handleDispatchTaskToTeam}
1355
1425
  />
1356
1426
  </div>
1357
1427
  </div>
@@ -3,7 +3,6 @@ import { useEffect, useMemo, useState } from 'react';
3
3
  import { computePendingCrossTeamReplies } from '@renderer/utils/crossTeamPendingReplies';
4
4
  import { ChevronRight } from 'lucide-react';
5
5
 
6
- import { ActiveTasksBlock } from '../activity/ActiveTasksBlock';
7
6
  import { PendingRepliesBlock } from '../activity/PendingRepliesBlock';
8
7
 
9
8
  import type { InboxMessage, ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
@@ -29,13 +28,10 @@ interface StatusBlockProps {
29
28
  */
30
29
  export const StatusBlock = ({
31
30
  members,
32
- tasks,
33
31
  messages,
34
32
  pendingRepliesByMember,
35
- position,
36
33
  layout = 'overlay',
37
34
  onMemberClick,
38
- onTaskClick,
39
35
  }: StatusBlockProps): React.JSX.Element | null => {
40
36
  const [collapsed, setCollapsed] = useState(false);
41
37
  const [nowMs, setNowMs] = useState(() => Date.now());
@@ -50,21 +46,11 @@ export const StatusBlock = ({
50
46
  );
51
47
  return hasMemberPendingReplies || pendingCrossTeamReplies.length > 0;
52
48
  }, [members, pendingRepliesByMember, pendingCrossTeamReplies.length]);
53
- const hasActiveTasks = useMemo(() => {
54
- const tMap = new Map(tasks.map((t) => [t.id, t]));
55
- return members.some((m) => {
56
- if (!m.currentTaskId) return false;
57
- const task = tMap.get(m.currentTaskId);
58
- if (task && (task.reviewState === 'approved' || task.status === 'completed')) return false;
59
- return true;
60
- });
61
- }, [members, tasks]);
62
49
 
63
50
  /** Whether the Status block has any visible items. */
64
51
  const hasItems = useMemo(() => {
65
- if (hasPendingReplies) return true;
66
- return hasActiveTasks;
67
- }, [hasActiveTasks, hasPendingReplies]);
52
+ return hasPendingReplies;
53
+ }, [hasPendingReplies]);
68
54
 
69
55
  // Only run the 1-second timer when the block actually has content to show.
70
56
  useEffect(() => {
@@ -111,14 +97,6 @@ export const StatusBlock = ({
111
97
  onMemberClick={onMemberClick}
112
98
  />
113
99
  ) : null}
114
- <ActiveTasksBlock
115
- members={members}
116
- tasks={tasks}
117
- defaultCollapsed={position === 'sidebar'}
118
- headerRight={!hasPendingReplies ? flowInlineToggle : undefined}
119
- onMemberClick={onMemberClick}
120
- onTaskClick={onTaskClick}
121
- />
122
100
  </div>
123
101
  )}
124
102
  </>
@@ -8,7 +8,7 @@ export const ScheduleEmptyState = (): React.JSX.Element => (
8
8
  <div className="space-y-1">
9
9
  <p className="text-xs font-medium text-[var(--color-text-secondary)]">暂无定时计划</p>
10
10
  <p className="text-[11px] text-[var(--color-text-muted)]">
11
- 创建计划后,可按 cron 时间自动运行团队任务。
11
+ 创建计划后,可按 cron 时间自动运行团队。
12
12
  </p>
13
13
  </div>
14
14
  </div>
@@ -1069,7 +1069,6 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
1069
1069
  const rotatingTips = React.useMemo(
1070
1070
  () => [
1071
1071
  'Tips:输入 @ 可提及成员、团队或文件,输入 # 可引用任务。',
1072
- 'Tips:输入“创建任务”可以把事项加入任务看板。',
1073
1072
  'Tips:不要把所有工作都堆给团队负责人,可以让负责人分配给合适的成员。',
1074
1073
  ...extraTips,
1075
1074
  ],
@@ -39,6 +39,9 @@ export interface ScheduleSlice {
39
39
 
40
40
  /** Open a standalone Schedules tab (or focus existing) */
41
41
  openSchedulesTab(): void;
42
+
43
+ /** Open the standalone task overview tab. */
44
+ openTasksTab(): void;
42
45
  }
43
46
 
44
47
  // =============================================================================
@@ -246,4 +249,22 @@ export const createScheduleSlice: StateCreator<AppState, [], [], ScheduleSlice>
246
249
  // Ensure schedules are fresh when opening
247
250
  void get().fetchSchedules();
248
251
  },
252
+
253
+ openTasksTab: () => {
254
+ const state = get();
255
+ const focusedPane = state.paneLayout.panes.find((p) => p.id === state.paneLayout.focusedPaneId);
256
+ const existingTab = focusedPane?.tabs.find((tab) => tab.type === 'tasks');
257
+ if (existingTab) {
258
+ state.setActiveTab(existingTab.id);
259
+ return;
260
+ }
261
+
262
+ state.openTab({
263
+ type: 'tasks',
264
+ label: '任务',
265
+ });
266
+
267
+ void get().fetchAllTasks();
268
+ void get().fetchSchedules();
269
+ },
249
270
  });
@@ -2067,6 +2067,7 @@ export interface TeamSlice {
2067
2067
  delayMs?: number
2068
2068
  ) => void;
2069
2069
  sendTeamMessage: (teamName: string, request: SendMessageRequest) => Promise<SendMessageResult>;
2070
+ addOptimisticTeamMessage: (teamName: string, message: InboxMessage) => void;
2070
2071
  crossTeamTargets: {
2071
2072
  teamName: string;
2072
2073
  displayName: string;
@@ -3988,6 +3989,39 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
3988
3989
  },
3989
3990
 
3990
3991
  sendTeamMessage: async (teamName: string, request: SendMessageRequest) => {
3992
+ const optimisticMessageId =
3993
+ request.messageId ?? `optimistic-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3994
+ const optimisticTimestamp = request.timestamp ?? nowIso();
3995
+ const requestWithMessageId: SendMessageRequest = {
3996
+ ...request,
3997
+ messageId: optimisticMessageId,
3998
+ timestamp: optimisticTimestamp,
3999
+ };
4000
+ const optimisticMessage: InboxMessage = {
4001
+ from: requestWithMessageId.from ?? 'user',
4002
+ to: requestWithMessageId.to ?? requestWithMessageId.member,
4003
+ text: requestWithMessageId.text,
4004
+ timestamp: optimisticTimestamp,
4005
+ read: true,
4006
+ taskRefs: requestWithMessageId.taskRefs?.length ? requestWithMessageId.taskRefs : undefined,
4007
+ actionMode: requestWithMessageId.actionMode,
4008
+ summary: requestWithMessageId.summary,
4009
+ color: requestWithMessageId.color,
4010
+ messageId: optimisticMessageId,
4011
+ relayOfMessageId: requestWithMessageId.relayOfMessageId,
4012
+ source: requestWithMessageId.source ?? 'user_sent',
4013
+ attachments: requestWithMessageId.attachments?.length
4014
+ ? requestWithMessageId.attachments
4015
+ : undefined,
4016
+ leadSessionId: requestWithMessageId.leadSessionId,
4017
+ conversationId: requestWithMessageId.conversationId,
4018
+ replyToConversationId: requestWithMessageId.replyToConversationId,
4019
+ toolSummary: requestWithMessageId.toolSummary,
4020
+ toolCalls: requestWithMessageId.toolCalls,
4021
+ messageKind: requestWithMessageId.messageKind,
4022
+ slashCommand: requestWithMessageId.slashCommand,
4023
+ commandOutput: requestWithMessageId.commandOutput,
4024
+ };
3991
4025
  set({
3992
4026
  sendingMessage: true,
3993
4027
  sendMessageError: null,
@@ -3995,35 +4029,25 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
3995
4029
  sendMessageDebugDetails: null,
3996
4030
  lastSendMessageResult: null,
3997
4031
  });
4032
+ set((state) => ({
4033
+ teamMessagesByName: {
4034
+ ...state.teamMessagesByName,
4035
+ [teamName]: upsertOptimisticTeamMessage(
4036
+ getTeamMessagesCacheEntry(state, teamName),
4037
+ optimisticMessage
4038
+ ),
4039
+ },
4040
+ }));
3998
4041
  try {
3999
4042
  const result = await unwrapIpc('team:sendMessage', () =>
4000
- api.teams.sendMessage(teamName, request)
4043
+ api.teams.sendMessage(teamName, requestWithMessageId)
4001
4044
  );
4002
4045
  const runtimeDeliveryFailed =
4003
4046
  result.runtimeDelivery?.attempted === true && result.runtimeDelivery.delivered === false;
4004
4047
  const runtimeDeliveryDiagnostics = buildOpenCodeRuntimeDeliveryDiagnostics(result);
4005
- const optimisticMessage: InboxMessage = {
4006
- from: request.from ?? 'user',
4007
- to: request.to ?? request.member,
4008
- text: request.text,
4009
- timestamp: request.timestamp ?? nowIso(),
4010
- read: true,
4011
- taskRefs: request.taskRefs?.length ? request.taskRefs : undefined,
4012
- actionMode: request.actionMode,
4013
- summary: request.summary,
4014
- color: request.color,
4048
+ const confirmedOptimisticMessage: InboxMessage = {
4049
+ ...optimisticMessage,
4015
4050
  messageId: result.messageId,
4016
- relayOfMessageId: request.relayOfMessageId,
4017
- source: request.source ?? 'user_sent',
4018
- attachments: request.attachments?.length ? request.attachments : undefined,
4019
- leadSessionId: request.leadSessionId,
4020
- conversationId: request.conversationId,
4021
- replyToConversationId: request.replyToConversationId,
4022
- toolSummary: request.toolSummary,
4023
- toolCalls: request.toolCalls,
4024
- messageKind: request.messageKind,
4025
- slashCommand: request.slashCommand,
4026
- commandOutput: request.commandOutput,
4027
4051
  };
4028
4052
  set((state) => ({
4029
4053
  sendingMessage: false,
@@ -4035,7 +4059,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
4035
4059
  ...state.teamMessagesByName,
4036
4060
  [teamName]: upsertOptimisticTeamMessage(
4037
4061
  getTeamMessagesCacheEntry(state, teamName),
4038
- optimisticMessage
4062
+ confirmedOptimisticMessage
4039
4063
  ),
4040
4064
  },
4041
4065
  }));
@@ -4053,6 +4077,18 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
4053
4077
  }
4054
4078
  },
4055
4079
 
4080
+ addOptimisticTeamMessage: (teamName: string, message: InboxMessage) => {
4081
+ set((state) => ({
4082
+ teamMessagesByName: {
4083
+ ...state.teamMessagesByName,
4084
+ [teamName]: upsertOptimisticTeamMessage(
4085
+ getTeamMessagesCacheEntry(state, teamName),
4086
+ message
4087
+ ),
4088
+ },
4089
+ }));
4090
+ },
4091
+
4056
4092
  fetchCrossTeamTargets: async () => {
4057
4093
  set({ crossTeamTargetsLoading: true });
4058
4094
  try {
@@ -86,6 +86,7 @@ export interface Tab {
86
86
  | 'report'
87
87
  | 'extensions'
88
88
  | 'schedules'
89
+ | 'tasks'
89
90
  | 'graph';
90
91
 
91
92
  /** Session ID (required when type === 'session') */
@@ -46,6 +46,7 @@ import type {
46
46
  BoardTaskExactLogSummariesResponse,
47
47
  BoardTaskLogStreamResponse,
48
48
  BoardTaskLogStreamSummary,
49
+ CollabTask,
49
50
  CreateTaskRequest,
50
51
  CrossTeamMessage,
51
52
  CrossTeamSendRequest,
@@ -723,6 +724,33 @@ export interface CrossTeamAPI {
723
724
  getOutbox: (teamName: string) => Promise<CrossTeamMessage[]>;
724
725
  }
725
726
 
727
+ // =============================================================================
728
+ // Collaboration Board API
729
+ // =============================================================================
730
+
731
+ export interface CollabBoardAPI {
732
+ getBoard: () => Promise<{ tasks: CollabTask[] }>;
733
+ getTask: (dispatchId: string) => Promise<{ task: CollabTask }>;
734
+ getEvents: (dispatchId: string) => Promise<{ events: import('./team').CollabTaskEvent[] }>;
735
+ accept: (teamSlug: string, dispatchId: string) => Promise<{ ok: boolean; taskId: string }>;
736
+ reject: (teamSlug: string, dispatchId: string, reason?: string) => Promise<{ ok: boolean }>;
737
+ deliver: (teamSlug: string, dispatchId: string, result: string) => Promise<{ ok: boolean }>;
738
+ approve: (teamSlug: string, dispatchId: string) => Promise<{ ok: boolean }>;
739
+ revision: (teamSlug: string, dispatchId: string, feedback: string) => Promise<{ ok: boolean }>;
740
+ dispatch: (
741
+ fromTeam: string,
742
+ toTeam: string,
743
+ subject: string,
744
+ opts?: {
745
+ description?: string;
746
+ deadlineMinutes?: number;
747
+ needsHumanReview?: boolean;
748
+ messageId?: string;
749
+ sessionKey?: string;
750
+ }
751
+ ) => Promise<{ ok: boolean; dispatchId: string; status: string; message: string }>;
752
+ }
753
+
726
754
  // =============================================================================
727
755
  // Schedule API
728
756
  // =============================================================================
@@ -1060,6 +1088,7 @@ export interface ElectronAPI extends RecentProjectsElectronApi {
1060
1088
 
1061
1089
  // Cross-Team Communication API
1062
1090
  crossTeam: CrossTeamAPI;
1091
+ collab: CollabBoardAPI;
1063
1092
 
1064
1093
  // Review API
1065
1094
  review: ReviewAPI;
@@ -133,6 +133,9 @@ export type TeamReviewState = 'none' | 'review' | 'needsFix' | 'approved';
133
133
 
134
134
  export type DispatchStatus =
135
135
  | 'dispatched'
136
+ | 'pending_accept'
137
+ | 'accepted'
138
+ | 'rejected'
136
139
  | 'received'
137
140
  | 'in_progress'
138
141
  | 'completed'
@@ -148,6 +151,15 @@ export interface DispatchMeta {
148
151
  receivedAt?: string;
149
152
  completedAt?: string;
150
153
  remoteTaskId?: string;
154
+ deadline?: string;
155
+ acceptedAt?: string;
156
+ rejectedAt?: string;
157
+ rejectionReason?: string;
158
+ }
159
+
160
+ export interface AgentCapability {
161
+ skill: string;
162
+ description: string;
151
163
  }
152
164
 
153
165
  export interface DiscoverableTeam {
@@ -156,6 +168,9 @@ export interface DiscoverableTeam {
156
168
  location: 'local' | 'remote';
157
169
  status: 'online' | 'offline';
158
170
  collaboration: boolean;
171
+ capabilities?: AgentCapability[];
172
+ description?: string;
173
+ harness?: string;
159
174
  }
160
175
 
161
176
  export interface TaskBusConfig {
@@ -166,8 +181,10 @@ export interface TaskBusConfig {
166
181
  password?: string;
167
182
  db?: number;
168
183
  };
184
+ collaboration?: boolean;
169
185
  telemetry?: {
170
186
  enabled: boolean;
187
+ uploadEnabled?: boolean;
171
188
  /** Data source platform. Currently only 'claudecode'. */
172
189
  platform: 'claudecode';
173
190
  };
@@ -185,6 +202,8 @@ export interface TaskDispatchPayload {
185
202
  promptTaskRefs?: string[];
186
203
  };
187
204
  dispatchedAt: string;
205
+ deadline?: string;
206
+ needsHumanReview?: boolean;
188
207
  }
189
208
 
190
209
  export interface TaskStatusUpdate {
@@ -203,6 +222,81 @@ export interface TaskAckPayload {
203
222
  timestamp: string;
204
223
  }
205
224
 
225
+ export interface TaskHandshakeResponse {
226
+ dispatchId: string;
227
+ type: 'task_accept' | 'task_reject' | 'task_deliver' | 'task_approve' | 'task_revision';
228
+ fromTeam: string;
229
+ toTeam: string;
230
+ remoteTaskId?: string;
231
+ reason?: string;
232
+ result?: string;
233
+ feedback?: string;
234
+ acceptedAt?: string;
235
+ rejectedAt?: string;
236
+ deliveredAt?: string;
237
+ approvedAt?: string;
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Collaboration Board — global cross-team task view
242
+ // ---------------------------------------------------------------------------
243
+
244
+ export type CollabTaskStatus =
245
+ | 'pending_accept'
246
+ | 'accepted'
247
+ | 'delivered'
248
+ | 'approved'
249
+ | 'revision'
250
+ | 'rejected'
251
+ | 'failed';
252
+
253
+ export type CollabTaskEventType =
254
+ | 'task_sent'
255
+ | 'task_accepted'
256
+ | 'task_rejected'
257
+ | 'task_delivered'
258
+ | 'revision_requested'
259
+ | 'task_approved'
260
+ | 'task_failed';
261
+
262
+ export interface CollabTaskEvent {
263
+ eventId: string;
264
+ dispatchId: string;
265
+ version: number;
266
+ type: CollabTaskEventType;
267
+ actor: {
268
+ type: 'user' | 'team' | 'agent' | 'system';
269
+ id: string;
270
+ };
271
+ payload?: Record<string, unknown>;
272
+ createdAt: string;
273
+ }
274
+
275
+ export interface CollabTask {
276
+ id: string;
277
+ dispatchId: string;
278
+ subject: string;
279
+ description?: string;
280
+ fromTeam: string;
281
+ fromTeamDisplay: string;
282
+ toTeam: string;
283
+ toTeamDisplay: string;
284
+ status: CollabTaskStatus;
285
+ version?: number;
286
+ reason?: string;
287
+ result?: string;
288
+ feedback?: string;
289
+ deadline?: string;
290
+ needsHumanReview: boolean;
291
+ revisionCount: number;
292
+ createdAt: string;
293
+ updatedAt: string;
294
+ acceptedAt?: string;
295
+ rejectedAt?: string;
296
+ deliveredAt?: string;
297
+ approvedAt?: string;
298
+ }
299
+
206
300
  export interface TaskWorkInterval {
207
301
  /** ISO timestamp when task entered in_progress */
208
302
  startedAt: string;
@@ -863,7 +957,15 @@ export type TeamLaunchAggregateState =
863
957
  | 'partial_skipped';
864
958
  export type PersistedTeamLaunchPhase = 'active' | 'finished' | 'reconciled';
865
959
 
866
- export type KanbanColumnId = 'todo' | 'in_progress' | 'done' | 'review' | 'approved';
960
+ export type KanbanColumnId =
961
+ | 'todo'
962
+ | 'in_progress'
963
+ | 'done'
964
+ | 'review'
965
+ | 'approved'
966
+ | 'pending_accept'
967
+ | 'delivered'
968
+ | 'revision';
867
969
 
868
970
  export interface KanbanTaskState {
869
971
  column: Extract<KanbanColumnId, 'review' | 'approved'>;
@@ -1699,6 +1801,7 @@ export interface CrossTeamSendRequest {
1699
1801
  toTeam: string;
1700
1802
  timestamp?: string;
1701
1803
  messageId?: string;
1804
+ sessionKey?: string;
1702
1805
  conversationId?: string;
1703
1806
  replyToConversationId?: string;
1704
1807
  text: string;