@yancyyu/openhermit 1.6.27 → 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 (101) hide show
  1. package/README.md +7 -1
  2. package/bin/hermit.mjs +2 -2
  3. package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
  4. package/dist-renderer/assets/{TeamGraphOverlay-DVq8rt6_.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
  5. package/dist-renderer/assets/{_basePickBy-ZbF0pKvS.js → _basePickBy-CgG_tjgX.js} +1 -1
  6. package/dist-renderer/assets/{_baseUniq-BBLBOeXc.js → _baseUniq-DwPTU9lP.js} +1 -1
  7. package/dist-renderer/assets/{arc-wGaEgkCf.js → arc-7nIrGRzY.js} +1 -1
  8. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BpMkdC35.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
  9. package/dist-renderer/assets/{blockDiagram-VD42YOAC-C8Z1xhG4.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
  10. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CJmlw9LA.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
  11. package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
  12. package/dist-renderer/assets/{chunk-4BX2VUAB-CHPHiRPP.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
  13. package/dist-renderer/assets/{chunk-55IACEB6-DyVohOQb.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
  14. package/dist-renderer/assets/{chunk-B4BG7PRW-p5bffh_R.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
  15. package/dist-renderer/assets/{chunk-DI55MBZ5-BnfGPSUu.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
  16. package/dist-renderer/assets/{chunk-FMBD7UC4-B6SCKseX.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
  17. package/dist-renderer/assets/{chunk-QN33PNHL-L12RvLBR.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
  18. package/dist-renderer/assets/{chunk-QZHKN3VN-DeH1Kxge.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
  19. package/dist-renderer/assets/{chunk-TZMSLE5B-BWnjzSlI.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
  20. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
  21. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
  22. package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
  23. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BtzoT5fu.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
  24. package/dist-renderer/assets/{dagre-6UL2VRFP-CBBvuoUD.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
  25. package/dist-renderer/assets/{diagram-PSM6KHXK-Be9BAKws.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
  26. package/dist-renderer/assets/{diagram-QEK2KX5R-BDS4PI_i.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
  27. package/dist-renderer/assets/{diagram-S2PKOQOG-2Rameaq7.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
  28. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CSIzCEZD.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
  29. package/dist-renderer/assets/{flowDiagram-NV44I4VS-ForEIVM5.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
  30. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-BJrli_xr.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
  31. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-C_4GuLno.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
  32. package/dist-renderer/assets/{graph-B1EAT_gw.js → graph-BiFNoBjP.js} +1 -1
  33. package/dist-renderer/assets/{index-eKRmS5kI.js → index-6m1ZAymG.js} +1 -1
  34. package/dist-renderer/assets/index-BhellmRb.css +1 -0
  35. package/dist-renderer/assets/{index-DYdseEwc.js → index-BowUl0Jb.js} +518 -514
  36. package/dist-renderer/assets/{index-DR602dwJ.js → index-Dp3kJTEe.js} +1 -1
  37. package/dist-renderer/assets/{index-Dwr5wu5x.js → index-TOpt_T7A.js} +1 -1
  38. package/dist-renderer/assets/{index-DOA_jbYb.js → index-qNBNjW4K.js} +1 -1
  39. package/dist-renderer/assets/{index-k4tnOFC5.js → index-vAykq1H1.js} +1 -1
  40. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DjI0uaMz.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
  41. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-jQ6Thae-.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
  42. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CKw6InbL.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
  43. package/dist-renderer/assets/{layout-Dad20y3V.js → layout-DNANbrI4.js} +1 -1
  44. package/dist-renderer/assets/{linear-vMgo_2Cv.js → linear-DxEJi1yT.js} +1 -1
  45. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-DYp6YoHL.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
  46. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BytBecG9.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
  47. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-RUaspLsc.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
  48. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-rR2B1Use.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
  49. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-BJi5qYhq.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
  50. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BM-wggUb.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
  51. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BqmcVjnj.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
  52. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-By3JDVbB.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
  53. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-szH0GUyk.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
  54. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
  55. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-dwDpvw0w.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
  56. package/dist-renderer/index.html +2 -2
  57. package/package.json +1 -1
  58. package/src/main/server.ts +800 -163
  59. package/src/main/services/session-intelligence/SessionUsageParser.ts +446 -0
  60. package/src/main/services/session-intelligence/UsageTelemetryService.ts +252 -0
  61. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  62. package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -95
  63. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  64. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  65. package/src/main/services/teams-mvp/index.ts +3 -0
  66. package/src/renderer/App.tsx +5 -0
  67. package/src/renderer/api/httpClient.ts +67 -0
  68. package/src/renderer/components/dashboard/DashboardView.tsx +6 -105
  69. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  70. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  71. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  72. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  73. package/src/renderer/components/settings/SettingsTabs.tsx +2 -2
  74. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  75. package/src/renderer/components/settings/sections/TaskBusSection.tsx +511 -81
  76. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  77. package/src/renderer/components/team/TeamDetailView.tsx +20 -98
  78. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  79. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  80. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  81. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  82. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  83. package/src/renderer/components/team/kanban/KanbanBoard.tsx +5 -1
  84. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  85. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  86. package/src/renderer/components/team/messages/MessagesPanel.tsx +72 -2
  87. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  88. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  89. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  90. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  91. package/src/renderer/store/slices/teamSlice.ts +59 -23
  92. package/src/renderer/types/tabs.ts +1 -0
  93. package/src/shared/types/api.ts +29 -0
  94. package/src/shared/types/team.ts +109 -1
  95. package/dist-renderer/assets/ProjectEditorOverlay-BBwYdXPv.js +0 -57
  96. package/dist-renderer/assets/channel-DJUrwVrK.js +0 -1
  97. package/dist-renderer/assets/classDiagram-2ON5EDUG-blc3DrH7.js +0 -1
  98. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-blc3DrH7.js +0 -1
  99. package/dist-renderer/assets/clone-BftjWakJ.js +0 -1
  100. package/dist-renderer/assets/index-CWpFqEvz.css +0 -1
  101. package/dist-renderer/assets/treemap-GDKQZRPO-BCMlh-Ex.js +0 -162
@@ -628,12 +628,6 @@ export const ProjectEditorOverlay = ({
628
628
  <EditorFileTree
629
629
  selectedFilePath={activeTabId}
630
630
  onFileSelect={handleFileSelect}
631
- onCreateTask={
632
- onEditorAction
633
- ? (filePath: string) =>
634
- onEditorAction(buildFileAction('createTask', filePath, projectPath))
635
- : undefined
636
- }
637
631
  onSendMessage={
638
632
  onEditorAction
639
633
  ? (filePath: string) =>
@@ -829,10 +823,6 @@ export const ProjectEditorOverlay = ({
829
823
  onEditorAction(buildSelectionAction('sendMessage', selectionInfo));
830
824
  setSelectionInfo(null);
831
825
  }}
832
- onCreateTask={() => {
833
- onEditorAction(buildSelectionAction('createTask', selectionInfo));
834
- setSelectionInfo(null);
835
- }}
836
826
  />
837
827
  )}
838
828
  </div>
@@ -274,7 +274,11 @@ export const KanbanBoard = ({
274
274
  }: KanbanBoardProps): React.JSX.Element => {
275
275
  const boardRef = useRef<HTMLDivElement>(null);
276
276
  const scrollRestoreTimeoutsRef = useRef<number[]>([]);
277
- const [viewMode, setViewMode] = useState<KanbanViewMode>('grid');
277
+ const [viewMode, setViewMode] = useState<KanbanViewMode>('columns');
278
+
279
+ useEffect(() => {
280
+ setViewMode('columns');
281
+ }, []);
278
282
  const enableTaskSorting =
279
283
  viewMode === 'columns' && !!onColumnOrderChange && sort.field === 'manual';
280
284
 
@@ -26,7 +26,6 @@ import { type MemberActivityFilter, type MemberDetailTab } from './memberDetailT
26
26
  import { MemberLaunchDiagnosticsButton } from './MemberLaunchDiagnosticsButton';
27
27
  import { MemberMessagesTab } from './MemberMessagesTab';
28
28
  import { MemberStatsTab } from './MemberStatsTab';
29
- import { MemberTasksTab } from './MemberTasksTab';
30
29
  import { MemberWorkspaceTab } from './MemberWorkspaceTab';
31
30
 
32
31
  import type { TeamLaunchParams } from '@renderer/store/slices/teamSlice';
@@ -56,8 +55,10 @@ interface MemberDetailDialogProps {
56
55
  launchParams?: TeamLaunchParams;
57
56
  onClose: () => void;
58
57
  onSendMessage: () => void;
59
- onAssignTask: () => void;
60
- onTaskClick: (task: TeamTaskWithKanban) => void;
58
+ /** Deprecated: team tasks UI has been removed, kept for compatibility with older callers/tests. */
59
+ onAssignTask?: () => void;
60
+ /** Deprecated: team tasks UI has been removed, kept for compatibility with older callers/tests. */
61
+ onTaskClick?: (task: TeamTaskWithKanban) => void;
61
62
  onRemoveMember?: () => void;
62
63
  onRestartMember?: (memberName: string) => Promise<void> | void;
63
64
  onUpdateRole?: (memberName: string, role: string | undefined) => Promise<void> | void;
@@ -83,18 +84,12 @@ export const MemberDetailDialog = ({
83
84
  launchParams,
84
85
  onClose,
85
86
  onSendMessage,
86
- onAssignTask,
87
- onTaskClick,
88
87
  onRemoveMember,
89
88
  onRestartMember,
90
89
  onUpdateRole,
91
90
  updatingRole,
92
91
  onViewMemberChanges,
93
92
  }: MemberDetailDialogProps): React.JSX.Element | null => {
94
- const memberTasks = useMemo(
95
- () => (member ? tasks.filter((t) => t.owner === member.name) : []),
96
- [tasks, member]
97
- );
98
93
  const memberMessages = useStore((state) =>
99
94
  selectMemberMessagesForTeamMember(state, teamName, member?.name ?? null)
100
95
  );
@@ -111,17 +106,9 @@ export const MemberDetailDialog = ({
111
106
  }).length;
112
107
  }, [member, memberMessages, members, tasks, teamName]);
113
108
 
114
- const inProgressTasks = useMemo(
115
- () => memberTasks.filter((t) => t.status === 'in_progress').length,
116
- [memberTasks]
117
- );
118
-
119
- const completedTasks = useMemo(
120
- () => memberTasks.filter((t) => t.status === 'completed').length,
121
- [memberTasks]
109
+ const [activeTab, setActiveTab] = useState<MemberDetailTab>(
110
+ initialTab === 'tasks' ? 'workspace' : initialTab
122
111
  );
123
-
124
- const [activeTab, setActiveTab] = useState<MemberDetailTab>(initialTab);
125
112
  const [restarting, setRestarting] = useState(false);
126
113
  const [restartError, setRestartError] = useState<string | null>(null);
127
114
 
@@ -202,8 +189,8 @@ export const MemberDetailDialog = ({
202
189
  </DialogHeader>
203
190
 
204
191
  <MemberDetailStats
205
- totalTasks={memberTasks.length}
206
- inProgressTasks={inProgressTasks}
192
+ totalTasks={0}
193
+ inProgressTasks={0}
207
194
  totalTokens={totalTokens}
208
195
  statsLoading={statsLoading}
209
196
  statsComputedAt={memberStats?.computedAt}
@@ -217,14 +204,6 @@ export const MemberDetailDialog = ({
217
204
  className="min-w-0 overflow-hidden"
218
205
  >
219
206
  <TabsList className="w-full">
220
- <TabsTrigger value="tasks" className="flex-1 gap-1.5">
221
- Tasks
222
- {memberTasks.length > 0 && (
223
- <span className="rounded-full bg-[var(--color-surface)] px-1.5 text-[10px]">
224
- {memberTasks.length}
225
- </span>
226
- )}
227
- </TabsTrigger>
228
207
  <TabsTrigger value="workspace" className="flex-1 gap-1.5">
229
208
  <FolderOpen size={12} />
230
209
  Workspace
@@ -242,9 +221,6 @@ export const MemberDetailDialog = ({
242
221
  Stats
243
222
  </TabsTrigger>
244
223
  </TabsList>
245
- <TabsContent value="tasks">
246
- <MemberTasksTab tasks={memberTasks} onTaskClick={onTaskClick} />
247
- </TabsContent>
248
224
  <TabsContent value="workspace">
249
225
  <MemberWorkspaceTab
250
226
  teamName={teamName}
@@ -260,7 +236,6 @@ export const MemberDetailDialog = ({
260
236
  members={members}
261
237
  tasks={tasks}
262
238
  initialFilter={initialActivityFilter}
263
- onTaskClick={onTaskClick}
264
239
  />
265
240
  </TabsContent>
266
241
  <TabsContent value="stats">
@@ -69,6 +69,11 @@ interface MessageComposerProps {
69
69
  actionMode?: AgentActionMode,
70
70
  taskRefs?: TaskRef[]
71
71
  ) => void;
72
+ onDispatchTask?: (
73
+ toTeam: string,
74
+ subject: string,
75
+ description: string
76
+ ) => Promise<boolean | void> | boolean | void;
72
77
  }
73
78
 
74
79
  export const MessageComposer = ({
@@ -86,6 +91,7 @@ export const MessageComposer = ({
86
91
  onSessionChange,
87
92
  textareaRef: externalTextareaRef,
88
93
  onSend,
94
+ onDispatchTask,
89
95
  }: MessageComposerProps): React.JSX.Element => {
90
96
  const internalTextareaRef = useRef<HTMLTextAreaElement>(null);
91
97
  const textareaRef = useMemo(() => {
@@ -223,7 +229,26 @@ export const MessageComposer = ({
223
229
  ? '斜杠命令需要团队负责人在线'
224
230
  : null
225
231
  : null;
226
- const canSend =
232
+ const teamDispatch = useMemo(() => {
233
+ const match = trimmed.match(/^@([^\s]+)\s+([\s\S]+)$/);
234
+ if (!match || !onDispatchTask) return null;
235
+ const mentioned = match[1];
236
+ const subject = match[2]?.trim();
237
+ if (!mentioned || !subject) return null;
238
+ const targetTeam = teamMentionSuggestions.find((team) => {
239
+ const slug = team.id.startsWith('team:') ? team.id.slice('team:'.length) : team.id;
240
+ return slug === mentioned || team.name === mentioned;
241
+ });
242
+ const slug = targetTeam
243
+ ? targetTeam.id.startsWith('team:')
244
+ ? targetTeam.id.slice('team:'.length)
245
+ : targetTeam.id
246
+ : mentioned;
247
+ return { slug, subject };
248
+ }, [onDispatchTask, teamMentionSuggestions, trimmed]);
249
+ const canDispatchToTeam =
250
+ teamDispatch !== null && trimmed.length > 0 && trimmed.length <= MAX_TEXT_LENGTH && !sending;
251
+ const canSendRegularMessage =
227
252
  recipient.length > 0 &&
228
253
  trimmed.length > 0 &&
229
254
  trimmed.length <= MAX_TEXT_LENGTH &&
@@ -231,6 +256,7 @@ export const MessageComposer = ({
231
256
  !isProvisioning &&
232
257
  !attachmentsBlocked &&
233
258
  !slashCommandRestrictionReason;
259
+ const canSend = canDispatchToTeam || canSendRegularMessage;
234
260
 
235
261
  // Track whether we initiated a send — clear draft only on confirmed success
236
262
  const pendingSendRef = useRef(false);
@@ -238,9 +264,19 @@ export const MessageComposer = ({
238
264
  const handleSend = useCallback(() => {
239
265
  if (!canSend) return;
240
266
  dismissMentionsRef.current?.();
241
- pendingSendRef.current = true;
242
267
  const taskRefs = extractTaskRefsFromText(draft.text, taskSuggestions);
243
268
  const serialized = serializeChipsWithText(trimmed, draft.chips);
269
+
270
+ if (teamDispatch && onDispatchTask) {
271
+ void Promise.resolve(
272
+ onDispatchTask(teamDispatch.slug, teamDispatch.subject, serialized)
273
+ ).then((dispatched) => {
274
+ if (dispatched !== false) draft.clearDraft();
275
+ });
276
+ return;
277
+ }
278
+
279
+ pendingSendRef.current = true;
244
280
  onSend(
245
281
  recipient,
246
282
  serialized,
@@ -249,7 +285,7 @@ export const MessageComposer = ({
249
285
  undefined,
250
286
  taskRefs
251
287
  );
252
- }, [canSend, recipient, trimmed, onSend, draft.attachments, draft.chips, taskSuggestions]);
288
+ }, [canSend, recipient, trimmed, onSend, draft, taskSuggestions, teamDispatch, onDispatchTask]);
253
289
 
254
290
  // Clear draft only after send completes successfully (sending: true → false, no error)
255
291
  useEffect(() => {
@@ -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;