@yancyyu/openhermit 1.6.3 → 1.6.5

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 (75) hide show
  1. package/README.md +52 -140
  2. package/bin/hermit.mjs +68 -251
  3. package/dist-renderer/assets/{ProjectEditorOverlay-BcjkdR8y.js → ProjectEditorOverlay-14yC9eQy.js} +1 -1
  4. package/dist-renderer/assets/{TeamGraphOverlay-B9PP0b_t.js → TeamGraphOverlay-RAoJDOnS.js} +1 -1
  5. package/dist-renderer/assets/{_basePickBy-CPquAmj5.js → _basePickBy-BhDOA0cG.js} +1 -1
  6. package/dist-renderer/assets/{_baseUniq-A66EsJn2.js → _baseUniq-DjjY0tMN.js} +1 -1
  7. package/dist-renderer/assets/{arc-YLxbV3Qw.js → arc-CzoaaE90.js} +1 -1
  8. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-wwpiLSwy.js → architectureDiagram-VXUJARFQ-D7ZTVCML.js} +1 -1
  9. package/dist-renderer/assets/{blockDiagram-VD42YOAC-3CHE3NYR.js → blockDiagram-VD42YOAC-DDVOvV1H.js} +1 -1
  10. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-K8hDNmEC.js → c4Diagram-YG6GDRKO-CMswQy_R.js} +1 -1
  11. package/dist-renderer/assets/channel-DjoT-21b.js +1 -0
  12. package/dist-renderer/assets/{chunk-4BX2VUAB-5OabZrhH.js → chunk-4BX2VUAB-aYfdMo75.js} +1 -1
  13. package/dist-renderer/assets/{chunk-55IACEB6-v2kdM_aT.js → chunk-55IACEB6-DUhZJ0mV.js} +1 -1
  14. package/dist-renderer/assets/{chunk-B4BG7PRW-C0Ju56SH.js → chunk-B4BG7PRW-BrGjG-E6.js} +1 -1
  15. package/dist-renderer/assets/{chunk-DI55MBZ5-DPTWTKRm.js → chunk-DI55MBZ5-CfPUMKlq.js} +1 -1
  16. package/dist-renderer/assets/{chunk-FMBD7UC4-DSkYppkv.js → chunk-FMBD7UC4-BMr0Vrdu.js} +1 -1
  17. package/dist-renderer/assets/{chunk-QN33PNHL-C_4cCLCl.js → chunk-QN33PNHL-C9gTfFZV.js} +1 -1
  18. package/dist-renderer/assets/{chunk-QZHKN3VN-ojL7PmOD.js → chunk-QZHKN3VN-TTPdfwHP.js} +1 -1
  19. package/dist-renderer/assets/{chunk-TZMSLE5B-D1g7Vl_v.js → chunk-TZMSLE5B-DPh3DBqf.js} +1 -1
  20. package/dist-renderer/assets/classDiagram-2ON5EDUG-C5mL3TLG.js +1 -0
  21. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-C5mL3TLG.js +1 -0
  22. package/dist-renderer/assets/clone-cS8bapaK.js +1 -0
  23. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-TJnGh924.js → cose-bilkent-S5V4N54A-BFbgRWOS.js} +1 -1
  24. package/dist-renderer/assets/{dagre-6UL2VRFP-cPgfHhoX.js → dagre-6UL2VRFP-CfXdU7Il.js} +1 -1
  25. package/dist-renderer/assets/{diagram-PSM6KHXK-BS5Y-RR6.js → diagram-PSM6KHXK-MdOyrxZl.js} +1 -1
  26. package/dist-renderer/assets/{diagram-QEK2KX5R-D9AF7AGJ.js → diagram-QEK2KX5R-DpmnBR-A.js} +1 -1
  27. package/dist-renderer/assets/{diagram-S2PKOQOG-DTFUadMS.js → diagram-S2PKOQOG-JXgp2H5I.js} +1 -1
  28. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-DB_StEwC.js → erDiagram-Q2GNP2WA-CRfYO8W3.js} +1 -1
  29. package/dist-renderer/assets/{flowDiagram-NV44I4VS-DGn40aPj.js → flowDiagram-NV44I4VS-BJvmgply.js} +1 -1
  30. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-9NiFCSBT.js → ganttDiagram-JELNMOA3-BLXnpZat.js} +1 -1
  31. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BdveeU3c.js → gitGraphDiagram-V2S2FVAM-BKtbxazQ.js} +1 -1
  32. package/dist-renderer/assets/{graph-aQYbgTDH.js → graph-D6n4zNVe.js} +1 -1
  33. package/dist-renderer/assets/{index-DmgKTZAa.js → index-3JdA9Dab.js} +529 -524
  34. package/dist-renderer/assets/{index-CaG9mf8s.css → index-C4x095x4.css} +1 -1
  35. package/dist-renderer/assets/{index-CWqPn0NY.js → index-CVdwMXdQ.js} +1 -1
  36. package/dist-renderer/assets/{index-DyEKO6GV.js → index-CkO1A9ft.js} +1 -1
  37. package/dist-renderer/assets/{index-oyepEosi.js → index-ar0tAtBS.js} +1 -1
  38. package/dist-renderer/assets/{index-CrCHolXN.js → index-c2GABSvo.js} +1 -1
  39. package/dist-renderer/assets/{index-DiAK42nd.js → index-trDFOqz-.js} +1 -1
  40. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-Dmc_xn8U.js → infoDiagram-HS3SLOUP-Bqq_toop.js} +1 -1
  41. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-D9LJr-B5.js → journeyDiagram-XKPGCS4Q-BRQs07r0.js} +1 -1
  42. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CjOWoNys.js → kanban-definition-3W4ZIXB7-DHQnAijJ.js} +1 -1
  43. package/dist-renderer/assets/{layout-D6GzYK4K.js → layout-BljiazG5.js} +1 -1
  44. package/dist-renderer/assets/{linear-Dt3GyUQf.js → linear-fx8cDfux.js} +1 -1
  45. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-XwY2hZr8.js → mindmap-definition-VGOIOE7T-DCfQbCFK.js} +1 -1
  46. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BU4nfYd7.js → pieDiagram-ADFJNKIX-DyAFYy6H.js} +1 -1
  47. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BYk6f63x.js → quadrantDiagram-AYHSOK5B-CCvqn9gd.js} +1 -1
  48. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-kbadr_bU.js → requirementDiagram-UZGBJVZJ-JYde-Xl2.js} +1 -1
  49. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-ZstP2Vth.js → sankeyDiagram-TZEHDZUN-C2Im6-aG.js} +1 -1
  50. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-obK_-ssz.js → sequenceDiagram-WL72ISMW-X6JGIoEB.js} +1 -1
  51. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BgZDg0VT.js → stateDiagram-FKZM4ZOC-BJTDs8MY.js} +1 -1
  52. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CMa5sz7x.js → stateDiagram-v2-4FDKWEC3-DUrYslPS.js} +1 -1
  53. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BOmCNnab.js → timeline-definition-IT6M3QCI-C7ECznev.js} +1 -1
  54. package/dist-renderer/assets/{treemap-GDKQZRPO-BU0ha0Ww.js → treemap-GDKQZRPO-BRg3Zpk4.js} +1 -1
  55. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-BzAHNASi.js → xychartDiagram-PRI3JC2R-CoZGyc2f.js} +1 -1
  56. package/dist-renderer/index.html +2 -2
  57. package/package.json +23 -17
  58. package/src/main/server.ts +179 -23
  59. package/src/main/services/teams-mvp/TaskDispatchService.ts +440 -0
  60. package/src/main/services/teams-mvp/TeamProvisioningService.ts +36 -33
  61. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +2 -0
  62. package/src/renderer/components/settings/SettingsTabs.tsx +8 -2
  63. package/src/renderer/components/settings/SettingsView.tsx +4 -0
  64. package/src/renderer/components/settings/sections/GeneralSection.tsx +168 -206
  65. package/src/renderer/components/settings/sections/TaskBusSection.tsx +176 -0
  66. package/src/renderer/components/sidebar/SidebarSessions.tsx +31 -4
  67. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +37 -0
  68. package/src/renderer/components/team/messages/MessageComposer.tsx +36 -228
  69. package/src/renderer/components/team/messages/MessagesPanel.tsx +0 -3
  70. package/src/renderer/store/slices/teamSlice.ts +30 -1
  71. package/src/shared/types/team.ts +73 -0
  72. package/dist-renderer/assets/channel-BSWYOYIc.js +0 -1
  73. package/dist-renderer/assets/classDiagram-2ON5EDUG-mw4yABob.js +0 -1
  74. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-mw4yABob.js +0 -1
  75. package/dist-renderer/assets/clone-KtZfFt-o.js +0 -1
@@ -28,7 +28,7 @@ import {
28
28
  import type { CcSession, CcSessionDetail, TeamSummary } from '@shared/types';
29
29
 
30
30
  const PAGE_SIZE = 8;
31
- const REFRESH_INTERVAL_MS = 5000;
31
+ const REFRESH_INTERVAL_MS = 2000;
32
32
  const SESSION_DETAIL_PAGE_SIZE = 50;
33
33
 
34
34
  interface TaggedSession extends CcSession {
@@ -50,6 +50,7 @@ export const SidebarSessions = (): React.JSX.Element => {
50
50
  const [cancellingId, setCancellingId] = useState<string | null>(null);
51
51
  const [expandedId, setExpandedId] = useState<string | null>(null);
52
52
  const refreshInFlightRef = useRef(false);
53
+ const needsRefreshRef = useRef(false);
53
54
 
54
55
  const activeTeamTabName = useMemo(() => {
55
56
  if (!activeTabId) return null;
@@ -67,7 +68,10 @@ export const SidebarSessions = (): React.JSX.Element => {
67
68
 
68
69
  const fetchAll = useCallback(
69
70
  async (opts: { silent?: boolean } = {}) => {
70
- if (refreshInFlightRef.current) return;
71
+ if (refreshInFlightRef.current) {
72
+ needsRefreshRef.current = true;
73
+ return;
74
+ }
71
75
  const { silent = false } = opts;
72
76
  refreshInFlightRef.current = true;
73
77
  if (!silent) {
@@ -108,11 +112,33 @@ export const SidebarSessions = (): React.JSX.Element => {
108
112
  if (!silent) {
109
113
  setLoading(false);
110
114
  }
115
+ if (needsRefreshRef.current) {
116
+ needsRefreshRef.current = false;
117
+ void fetchAll({ silent: true });
118
+ }
111
119
  }
112
120
  },
113
121
  [scopedTeamName]
114
122
  );
115
123
 
124
+ /** Incremental refresh: only re-fetch sessions for one team */
125
+ const refreshTeam = useCallback(async (teamName: string) => {
126
+ try {
127
+ const sessions = await api.teams.getTeamSessions(teamName);
128
+ const tagged = sessions.map((s) => ({
129
+ ...s,
130
+ teamName,
131
+ teamDisplayName: teamName,
132
+ }));
133
+ setAllSessions((prev) => {
134
+ const others = prev.filter((s) => s.teamName !== teamName);
135
+ return [...others, ...tagged];
136
+ });
137
+ } catch {
138
+ // silent
139
+ }
140
+ }, []);
141
+
116
142
  useEffect(() => {
117
143
  void fetchAll();
118
144
  }, [fetchAll]);
@@ -135,7 +161,8 @@ export const SidebarSessions = (): React.JSX.Element => {
135
161
  if (scopedTeamName && change.teamName !== scopedTeamName) {
136
162
  return;
137
163
  }
138
- void fetchAll({ silent: true });
164
+ // Incremental refresh for the changed team — lightweight, no blocking
165
+ void refreshTeam(change.teamName);
139
166
  });
140
167
  return () => {
141
168
  if (typeof unsubscribe === 'function') {
@@ -333,7 +360,7 @@ const SessionRow = ({
333
360
  cancelling,
334
361
  }: Readonly<SessionRowProps>): React.JSX.Element => {
335
362
  const timeAgo = formatShortTime(new Date(session.updatedAt));
336
- const label = session.title || session.chatName || session.userName || session.sessionKey;
363
+ const label = session.chatName || session.title || session.userName || session.sessionKey;
337
364
  const platformLabel = session.platform === 'bridge' ? 'Bridge' : session.platform;
338
365
  const [detail, setDetail] = useState<CcSessionDetail | null>(null);
339
366
  const [loadingDetail, setLoadingDetail] = useState(false);
@@ -21,10 +21,12 @@ import {
21
21
  HelpCircle,
22
22
  Loader2,
23
23
  Play,
24
+ Send,
24
25
  Trash2,
25
26
  XCircle,
26
27
  } from 'lucide-react';
27
28
 
29
+ import type { DispatchMeta } from '@shared/types/team';
28
30
  import type { KanbanColumnId, KanbanTaskState, TeamTask, TeamTaskWithKanban } from '@shared/types';
29
31
 
30
32
  interface KanbanTaskCardProps {
@@ -55,6 +57,40 @@ interface DependencyBadgeProps {
55
57
  onScrollToTask?: (taskId: string) => void;
56
58
  }
57
59
 
60
+ const DISPATCH_STATUS_STYLE: Record<string, { bg: string; text: string; label: string }> = {
61
+ dispatched: {
62
+ bg: 'bg-yellow-500/15',
63
+ text: 'text-yellow-600 dark:text-yellow-400',
64
+ label: '已派发',
65
+ },
66
+ received: { bg: 'bg-blue-500/15', text: 'text-blue-600 dark:text-blue-400', label: '已接收' },
67
+ in_progress: { bg: 'bg-blue-500/15', text: 'text-blue-600 dark:text-blue-400', label: '执行中' },
68
+ completed: {
69
+ bg: 'bg-emerald-500/15',
70
+ text: 'text-emerald-600 dark:text-emerald-400',
71
+ label: '已完成',
72
+ },
73
+ synced_back: {
74
+ bg: 'bg-emerald-500/15',
75
+ text: 'text-emerald-600 dark:text-emerald-400',
76
+ label: '已同步',
77
+ },
78
+ failed: { bg: 'bg-red-500/15', text: 'text-red-600 dark:text-red-400', label: '失败' },
79
+ };
80
+
81
+ const DispatchBadge = ({ meta }: { meta: DispatchMeta }): React.JSX.Element => {
82
+ const style = DISPATCH_STATUS_STYLE[meta.status] ?? DISPATCH_STATUS_STYLE.dispatched;
83
+ const target = meta.targetTeam;
84
+ return (
85
+ <span
86
+ className={`mt-1 inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium ${style.bg} ${style.text}`}
87
+ >
88
+ <Send size={10} />
89
+ {style.label} → {target}
90
+ </span>
91
+ );
92
+ };
93
+
58
94
  const DependencyBadge = ({
59
95
  taskId,
60
96
  taskMap,
@@ -333,6 +369,7 @@ export const KanbanTaskCard = memo(
333
369
  {REVIEW_STATE_DISPLAY.needsFix.label}
334
370
  </span>
335
371
  ) : null}
372
+ {task.dispatchMeta ? <DispatchBadge meta={task.dispatchMeta} /> : null}
336
373
  {compact && <TruncatedTitle text={task.subject} className="mt-1" />}
337
374
  </div>
338
375
 
@@ -69,13 +69,6 @@ interface MessageComposerProps {
69
69
  actionMode?: AgentActionMode,
70
70
  taskRefs?: TaskRef[]
71
71
  ) => void;
72
- onCrossTeamSend?: (
73
- toTeam: string,
74
- text: string,
75
- summary?: string,
76
- actionMode?: AgentActionMode,
77
- taskRefs?: TaskRef[]
78
- ) => void;
79
72
  }
80
73
 
81
74
  export const MessageComposer = ({
@@ -93,7 +86,6 @@ export const MessageComposer = ({
93
86
  onSessionChange,
94
87
  textareaRef: externalTextareaRef,
95
88
  onSend,
96
- onCrossTeamSend,
97
89
  }: MessageComposerProps): React.JSX.Element => {
98
90
  const internalTextareaRef = useRef<HTMLTextAreaElement>(null);
99
91
  const textareaRef = useMemo(() => {
@@ -120,69 +112,7 @@ export const MessageComposer = ({
120
112
  const [fileRestrictionError, setFileRestrictionError] = useState<string | null>(null);
121
113
  const fileRestrictionTimerRef = useRef(0);
122
114
  const dismissMentionsRef = useRef<(() => void) | null>(null);
123
-
124
- // Cross-team state
125
- const [selectedTeam, setSelectedTeam] = useState<string | null>(null);
126
115
  const [teamSelectorOpen, setTeamSelectorOpen] = useState(false);
127
- const [aliveTeams, setAliveTeams] = useState<Set<string>>(new Set());
128
- const allCrossTeamTargets = useStore(useShallow((s) => s.crossTeamTargets));
129
- const fetchCrossTeamTargets = useStore((s) => s.fetchCrossTeamTargets);
130
-
131
- useEffect(() => {
132
- void fetchCrossTeamTargets();
133
- }, [fetchCrossTeamTargets]);
134
-
135
- const refreshAliveTeams = useCallback(async () => {
136
- try {
137
- const list = await api.teams.aliveList();
138
- setAliveTeams(new Set(list));
139
- } catch {
140
- // best-effort
141
- }
142
- }, []);
143
-
144
- useEffect(() => {
145
- void refreshAliveTeams();
146
- }, [refreshAliveTeams]);
147
-
148
- useEffect(() => {
149
- if (!teamSelectorOpen) return;
150
- void refreshAliveTeams();
151
- }, [teamSelectorOpen, refreshAliveTeams]);
152
-
153
- // Always filter out current team on the UI side (store is global, shared across tabs)
154
- const crossTeamTargets = useMemo(
155
- () => allCrossTeamTargets.filter((t) => t.teamName !== teamName),
156
- [allCrossTeamTargets, teamName]
157
- );
158
- const sortedCrossTeamTargets = useMemo(
159
- () =>
160
- crossTeamTargets
161
- .map((target) => ({
162
- ...target,
163
- isOnline: aliveTeams.has(target.teamName),
164
- }))
165
- .sort((a, b) => {
166
- if (a.isOnline && !b.isOnline) return -1;
167
- if (!a.isOnline && b.isOnline) return 1;
168
- return (a.displayName || a.teamName).localeCompare(
169
- b.displayName || b.teamName,
170
- undefined,
171
- {
172
- sensitivity: 'base',
173
- }
174
- );
175
- }),
176
- [aliveTeams, crossTeamTargets]
177
- );
178
- const hasCrossTeamOptions = sortedCrossTeamTargets.length > 0;
179
-
180
- const isCrossTeam = selectedTeam !== null;
181
- const selectedTarget = sortedCrossTeamTargets.find((t) => t.teamName === selectedTeam);
182
- const targetDisplayName = selectedTarget?.displayName ?? selectedTeam;
183
- const crossTeamHintText = isCrossTeam
184
- ? 'Tips:跨团队消息会发送到目标团队负责人。如果你希望回复发回你当前团队负责人而不是你本人,请在消息中明确说明。'
185
- : undefined;
186
116
 
187
117
  // Members load async with team data; keep recipient stable if valid, otherwise default to lead/first.
188
118
  useEffect(() => {
@@ -215,8 +145,8 @@ export const MessageComposer = ({
215
145
  [selectedSessionKey, sessions]
216
146
  );
217
147
  const selectedSessionLabel =
218
- selectedSession?.title ||
219
148
  selectedSession?.chatName ||
149
+ selectedSession?.title ||
220
150
  selectedSession?.userName ||
221
151
  selectedSession?.sessionKey ||
222
152
  '选择会话';
@@ -276,26 +206,22 @@ export const MessageComposer = ({
276
206
  // const leadContext = useStore((s) =>
277
207
  // isLeadAgentRecipient ? s.leadContextByTeam[teamName] : undefined
278
208
  // );
279
- const supportsAttachments = isLeadRecipient && !isCrossTeam && !!isTeamAlive;
209
+ const supportsAttachments = isLeadRecipient && !!isTeamAlive;
280
210
  const canAttach = supportsAttachments && draft.canAddMore;
281
211
  const attachmentRestrictionReason = !supportsAttachments
282
- ? isCrossTeam
283
- ? '跨团队消息暂不支持文件附件'
284
- : !isLeadRecipient
285
- ? '文件只能发送给团队负责人'
286
- : '团队在线时才能添加文件'
212
+ ? !isLeadRecipient
213
+ ? '文件只能发送给团队负责人'
214
+ : '团队在线时才能添加文件'
287
215
  : undefined;
288
216
  const attachmentsBlocked = draft.attachments.length > 0 && !supportsAttachments;
289
217
  const slashCommandRestrictionReason = standaloneSlashCommand
290
218
  ? draft.attachments.length > 0
291
219
  ? '斜杠命令需要团队负责人在线,且不能与附件同时发送'
292
- : isCrossTeam
293
- ? '斜杠命令只能在当前团队负责人上执行'
294
- : !isLeadRecipient
295
- ? '斜杠命令只能发送给团队负责人'
296
- : !isTeamAlive
297
- ? '斜杠命令需要团队负责人在线'
298
- : null
220
+ : !isLeadRecipient
221
+ ? '斜杠命令只能发送给团队负责人'
222
+ : !isTeamAlive
223
+ ? '斜杠命令需要团队负责人在线'
224
+ : null
299
225
  : null;
300
226
  const canSend =
301
227
  recipient.length > 0 &&
@@ -304,8 +230,7 @@ export const MessageComposer = ({
304
230
  !sending &&
305
231
  !isProvisioning &&
306
232
  !attachmentsBlocked &&
307
- !slashCommandRestrictionReason &&
308
- (!isCrossTeam || onCrossTeamSend !== undefined);
233
+ !slashCommandRestrictionReason;
309
234
 
310
235
  // Track whether we initiated a send — clear draft only on confirmed success
311
236
  const pendingSendRef = useRef(false);
@@ -316,31 +241,15 @@ export const MessageComposer = ({
316
241
  pendingSendRef.current = true;
317
242
  const taskRefs = extractTaskRefsFromText(draft.text, taskSuggestions);
318
243
  const serialized = serializeChipsWithText(trimmed, draft.chips);
319
- if (isCrossTeam && selectedTeam && onCrossTeamSend) {
320
- onCrossTeamSend(selectedTeam, serialized, trimmed, undefined, taskRefs);
321
- } else {
322
- // Summary should stay compact (no expanded chip markdown)
323
- onSend(
324
- recipient,
325
- serialized,
326
- trimmed,
327
- draft.attachments.length > 0 ? draft.attachments : undefined,
328
- undefined,
329
- taskRefs
330
- );
331
- }
332
- }, [
333
- canSend,
334
- recipient,
335
- trimmed,
336
- onSend,
337
- onCrossTeamSend,
338
- isCrossTeam,
339
- selectedTeam,
340
- draft.attachments,
341
- draft.chips,
342
- taskSuggestions,
343
- ]);
244
+ onSend(
245
+ recipient,
246
+ serialized,
247
+ trimmed,
248
+ draft.attachments.length > 0 ? draft.attachments : undefined,
249
+ undefined,
250
+ taskRefs
251
+ );
252
+ }, [canSend, recipient, trimmed, onSend, draft.attachments, draft.chips, taskSuggestions]);
344
253
 
345
254
  // Clear draft only after send completes successfully (sending: true → false, no error)
346
255
  useEffect(() => {
@@ -525,7 +434,7 @@ export const MessageComposer = ({
525
434
  shouldDockRecipientSelector
526
435
  ? 'relative z-10 -mb-2 overflow-hidden rounded-b-none rounded-t-[1.35rem] border-b-0 bg-[var(--color-surface-raised)]'
527
436
  : 'rounded-full',
528
- isCrossTeam ? 'border-[var(--cross-team-border)]' : 'border-[var(--color-border)]'
437
+ 'border-[var(--color-border)]'
529
438
  )}
530
439
  >
531
440
  <Popover open={teamSelectorOpen} onOpenChange={setTeamSelectorOpen}>
@@ -537,49 +446,23 @@ export const MessageComposer = ({
537
446
  shouldDockRecipientSelector
538
447
  ? 'rounded-bl-none rounded-tl-[1.35rem]'
539
448
  : 'rounded-l-full',
540
- isCrossTeam
541
- ? 'hover:bg-[var(--cross-team-bg)]/80 bg-[var(--cross-team-bg)] text-purple-400'
542
- : 'hover:bg-[var(--color-surface-raised)]'
449
+ 'hover:bg-[var(--color-surface-raised)]'
543
450
  )}
544
451
  >
545
- {isCrossTeam ? (
546
- <>
547
- <span
548
- className={cn(
549
- 'inline-block size-2 shrink-0 rounded-full',
550
- selectedTarget?.isOnline && 'animate-pulse'
551
- )}
552
- style={{
553
- backgroundColor: selectedTarget?.isOnline
554
- ? '#22c55e'
555
- : selectedTarget
556
- ? selectedTarget.color
557
- ? getTeamColorSet(selectedTarget.color).border
558
- : nameColorSet(selectedTarget.displayName).border
559
- : undefined,
560
- }}
561
- />
562
- <span className="max-w-[100px] truncate">{targetDisplayName}</span>
563
- </>
564
- ) : (
565
- <>
566
- {currentTeamColor ? (
567
- <span
568
- className="inline-block size-2 shrink-0 rounded-full"
569
- style={{ backgroundColor: currentTeamColor }}
570
- />
571
- ) : null}
572
- <span className="max-w-[120px] truncate text-[var(--color-text-secondary)]">
573
- {selectedSessionLabel}
574
- </span>
575
- </>
576
- )}
452
+ {currentTeamColor ? (
453
+ <span
454
+ className="inline-block size-2 shrink-0 rounded-full"
455
+ style={{ backgroundColor: currentTeamColor }}
456
+ />
457
+ ) : null}
458
+ <span className="max-w-[120px] truncate text-[var(--color-text-secondary)]">
459
+ {selectedSessionLabel}
460
+ </span>
577
461
  <ChevronDown size={12} className="shrink-0 text-[var(--color-text-muted)]" />
578
462
  </button>
579
463
  </PopoverTrigger>
580
464
  <PopoverContent align="end" className="w-56 p-1.5">
581
465
  <div className="max-h-48 space-y-0.5 overflow-y-auto">
582
- {/* Session options */}
583
466
  {sessions.length > 0 && (
584
467
  <>
585
468
  <div className="px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
@@ -588,8 +471,8 @@ export const MessageComposer = ({
588
471
  {sessions.map((session) => {
589
472
  const isSelected = selectedSessionKey === session.sessionKey;
590
473
  const label =
591
- session.title ||
592
474
  session.chatName ||
475
+ session.title ||
593
476
  session.userName ||
594
477
  session.sessionKey;
595
478
  return (
@@ -602,7 +485,6 @@ export const MessageComposer = ({
602
485
  )}
603
486
  onClick={() => {
604
487
  onSessionChange?.(session.sessionKey);
605
- setSelectedTeam(null);
606
488
  setTeamSelectorOpen(false);
607
489
  }}
608
490
  >
@@ -627,82 +509,13 @@ export const MessageComposer = ({
627
509
  </button>
628
510
  );
629
511
  })}
630
- <div className="my-1 h-px bg-[var(--color-border)]" />
631
512
  </>
632
513
  )}
633
-
634
- {hasCrossTeamOptions ? (
635
- <>
636
- <div className="my-1 h-px bg-[var(--color-border)]" />
637
-
638
- {sortedCrossTeamTargets.map((target) => {
639
- const isSelected = selectedTeam === target.teamName;
640
- return (
641
- <button
642
- key={target.teamName}
643
- type="button"
644
- className={cn(
645
- 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors hover:bg-[var(--color-surface-raised)]',
646
- isSelected && 'bg-[var(--cross-team-bg)]'
647
- )}
648
- onClick={() => {
649
- setSelectedTeam(target.teamName);
650
- setRecipient(CANONICAL_LEAD_MEMBER_NAME);
651
- setTeamSelectorOpen(false);
652
- }}
653
- >
654
- <span
655
- className={cn(
656
- 'inline-block size-2 shrink-0 rounded-full',
657
- target.isOnline && 'animate-pulse'
658
- )}
659
- style={{
660
- backgroundColor: target.isOnline
661
- ? '#22c55e'
662
- : target.color
663
- ? getTeamColorSet(target.color).border
664
- : nameColorSet(target.displayName).border,
665
- }}
666
- title={target.isOnline ? '在线' : '离线'}
667
- />
668
- <div className="min-w-0 flex-1">
669
- <div className="flex items-center gap-1.5">
670
- <div className="truncate text-[var(--color-text)]">
671
- {target.displayName}
672
- </div>
673
- <span
674
- className={cn(
675
- 'shrink-0 text-[10px]',
676
- target.isOnline
677
- ? 'text-green-400'
678
- : 'text-[var(--color-text-muted)]'
679
- )}
680
- >
681
- {target.isOnline ? '在线' : '离线'}
682
- </span>
683
- </div>
684
- {target.description ? (
685
- <div className="truncate text-[10px] text-[var(--color-text-muted)]">
686
- {target.description}
687
- </div>
688
- ) : null}
689
- </div>
690
- {isSelected ? (
691
- <Check size={12} className="ml-auto shrink-0 text-purple-400" />
692
- ) : null}
693
- </button>
694
- );
695
- })}
696
- </>
697
- ) : null}
698
514
  </div>
699
515
  </PopoverContent>
700
516
  </Popover>
701
517
 
702
- <Popover
703
- open={isCrossTeam ? false : recipientOpen}
704
- onOpenChange={isCrossTeam ? undefined : setRecipientOpen}
705
- >
518
+ <Popover open={recipientOpen} onOpenChange={setRecipientOpen}>
706
519
  <PopoverTrigger asChild>
707
520
  <button
708
521
  type="button"
@@ -711,11 +524,8 @@ export const MessageComposer = ({
711
524
  shouldDockRecipientSelector
712
525
  ? 'rounded-br-none rounded-tr-[1.35rem]'
713
526
  : 'rounded-r-full',
714
- isCrossTeam
715
- ? 'cursor-default bg-[var(--cross-team-bg)] opacity-60'
716
- : 'hover:bg-[var(--color-surface-raised)]'
527
+ 'hover:bg-[var(--color-surface-raised)]'
717
528
  )}
718
- disabled={isCrossTeam}
719
529
  >
720
530
  {recipient ? (
721
531
  <MemberBadge
@@ -843,9 +653,7 @@ export const MessageComposer = ({
843
653
  placeholder={
844
654
  isProvisioning
845
655
  ? '团队正在启动中... 消息将排队并在稍后投递到收件箱。'
846
- : isCrossTeam
847
- ? `发送跨团队消息到 ${targetDisplayName ?? '目标团队'}...`
848
- : '输入消息...(回车发送,Shift+Enter 换行)'
656
+ : '输入消息...(回车发送,Shift+Enter 换行)'
849
657
  }
850
658
  value={draft.text}
851
659
  onValueChange={draft.setText}
@@ -875,7 +683,7 @@ export const MessageComposer = ({
875
683
  maxRows={6}
876
684
  maxLength={MAX_TEXT_LENGTH}
877
685
  disabled={sending}
878
- hintText={crossTeamHintText}
686
+ hintText={undefined}
879
687
  showHint={!isCompactLayout}
880
688
  cornerActionInset="compact"
881
689
  cornerAction={
@@ -869,7 +869,6 @@ export const MessagesPanel = memo(function MessagesPanel({
869
869
  onSessionChange={setSelectedSessionKey}
870
870
  textareaRef={composerTextareaRef}
871
871
  onSend={handleSend}
872
- onCrossTeamSend={handleCrossTeamSend}
873
872
  />
874
873
  <StatusBlock
875
874
  members={members}
@@ -1066,7 +1065,6 @@ export const MessagesPanel = memo(function MessagesPanel({
1066
1065
  onSessionChange={setSelectedSessionKey}
1067
1066
  textareaRef={composerTextareaRef}
1068
1067
  onSend={handleSend}
1069
- onCrossTeamSend={handleCrossTeamSend}
1070
1068
  />
1071
1069
  <StatusBlock
1072
1070
  members={members}
@@ -1354,7 +1352,6 @@ export const MessagesPanel = memo(function MessagesPanel({
1354
1352
  onSessionChange={setSelectedSessionKey}
1355
1353
  textareaRef={composerTextareaRef}
1356
1354
  onSend={handleSend}
1357
- onCrossTeamSend={handleCrossTeamSend}
1358
1355
  />
1359
1356
  </div>
1360
1357
  </div>
@@ -4266,6 +4266,35 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
4266
4266
 
4267
4267
  softDeleteTask: async (teamName: string, taskId: string) => {
4268
4268
  await unwrapIpc('team:softDeleteTask', () => api.teams.softDeleteTask(teamName, taskId));
4269
+ set((state) => {
4270
+ const removeTaskFromSnapshot = (snapshot: TeamViewSnapshot): TeamViewSnapshot => ({
4271
+ ...snapshot,
4272
+ tasks: snapshot.tasks.filter((task) => task.id !== taskId),
4273
+ kanbanState: {
4274
+ ...snapshot.kanbanState,
4275
+ tasks: Object.fromEntries(
4276
+ Object.entries(snapshot.kanbanState.tasks).filter(([id]) => id !== taskId)
4277
+ ),
4278
+ },
4279
+ });
4280
+ const cached = state.teamDataCacheByName[teamName];
4281
+ return {
4282
+ ...(cached
4283
+ ? {
4284
+ teamDataCacheByName: {
4285
+ ...state.teamDataCacheByName,
4286
+ [teamName]: removeTaskFromSnapshot(cached),
4287
+ },
4288
+ }
4289
+ : {}),
4290
+ ...(state.selectedTeamName === teamName && state.selectedTeamData
4291
+ ? { selectedTeamData: removeTaskFromSnapshot(state.selectedTeamData) }
4292
+ : {}),
4293
+ globalTasks: state.globalTasks.filter(
4294
+ (task) => !(task.teamName === teamName && task.id === taskId)
4295
+ ),
4296
+ };
4297
+ });
4269
4298
  await get().refreshTeamData(teamName);
4270
4299
  await get().fetchDeletedTasks(teamName);
4271
4300
  },
@@ -4298,8 +4327,8 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
4298
4327
  set((state) => ({
4299
4328
  ...collectTeamScopedStateRemovals(state, teamName),
4300
4329
  teams: state.teams.filter((team) => team.teamName !== teamName),
4301
- selectedTeam: state.selectedTeam?.teamName === teamName ? null : state.selectedTeam,
4302
4330
  selectedTeamName: state.selectedTeamName === teamName ? null : state.selectedTeamName,
4331
+ selectedTeamData: state.selectedTeamName === teamName ? null : state.selectedTeamData,
4303
4332
  selectedTeamError: state.selectedTeamName === teamName ? null : state.selectedTeamError,
4304
4333
  selectedTeamLoading: state.selectedTeamName === teamName ? false : state.selectedTeamLoading,
4305
4334
  }));
@@ -126,6 +126,77 @@ export interface TeamSummary {
126
126
  export type TeamTaskStatus = 'pending' | 'in_progress' | 'completed' | 'deleted';
127
127
  export type TeamReviewState = 'none' | 'review' | 'needsFix' | 'approved';
128
128
 
129
+ // ---------------------------------------------------------------------------
130
+ // Task Dispatch — cross-team task delivery
131
+ // ---------------------------------------------------------------------------
132
+
133
+ export type DispatchStatus =
134
+ | 'dispatched'
135
+ | 'received'
136
+ | 'in_progress'
137
+ | 'completed'
138
+ | 'synced_back'
139
+ | 'failed';
140
+
141
+ export interface DispatchMeta {
142
+ dispatchId: string;
143
+ originTeam: string;
144
+ targetTeam: string;
145
+ status: DispatchStatus;
146
+ dispatchedAt: string;
147
+ receivedAt?: string;
148
+ completedAt?: string;
149
+ remoteTaskId?: string;
150
+ }
151
+
152
+ export interface DiscoverableTeam {
153
+ slug: string;
154
+ displayName: string;
155
+ location: 'local' | 'remote';
156
+ status: 'online' | 'offline';
157
+ collaboration: boolean;
158
+ }
159
+
160
+ export interface TaskBusConfig {
161
+ enabled: boolean;
162
+ redis: {
163
+ host: string;
164
+ port: number;
165
+ password?: string;
166
+ db?: number;
167
+ };
168
+ }
169
+
170
+ export interface TaskDispatchPayload {
171
+ dispatchId: string;
172
+ originTeam: string;
173
+ targetTeam: string;
174
+ task: {
175
+ subject: string;
176
+ description?: string;
177
+ prompt?: string;
178
+ descriptionTaskRefs?: string[];
179
+ promptTaskRefs?: string[];
180
+ };
181
+ dispatchedAt: string;
182
+ }
183
+
184
+ export interface TaskStatusUpdate {
185
+ dispatchId: string;
186
+ originTeam: string;
187
+ status: DispatchStatus;
188
+ remoteTaskId?: string;
189
+ timestamp: string;
190
+ result?: string;
191
+ }
192
+
193
+ export interface TaskAckPayload {
194
+ dispatchId: string;
195
+ status: 'received';
196
+ remoteTaskId: string;
197
+ timestamp: string;
198
+ }
199
+
129
200
  export interface TaskWorkInterval {
130
201
  /** ISO timestamp when task entered in_progress */
131
202
  startedAt: string;
@@ -505,6 +576,8 @@ export interface TeamTask {
505
576
  sourceMessageId?: string;
506
577
  /** Snapshot of the source message at creation time (sanitized, no blobs). */
507
578
  sourceMessage?: SourceMessageSnapshot;
579
+ /** Cross-team dispatch metadata — set when task has been dispatched to or from another team. */
580
+ dispatchMeta?: DispatchMeta;
508
581
  }
509
582
 
510
583
  /** Task enriched for UI/DTO use (overlay from kanban-state.json). */