@yancyyu/openhermit 1.6.4 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -140
- package/bin/hermit.mjs +79 -297
- package/dist-renderer/assets/{ProjectEditorOverlay-BcjkdR8y.js → ProjectEditorOverlay-pgmdlWxa.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-B9PP0b_t.js → TeamGraphOverlay-hN5q6AG0.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-CPquAmj5.js → _basePickBy-Bk3vVggp.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-A66EsJn2.js → _baseUniq-dMa0BJXs.js} +1 -1
- package/dist-renderer/assets/{arc-YLxbV3Qw.js → arc-DJlWrmw7.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-wwpiLSwy.js → architectureDiagram-VXUJARFQ-BEk2m-RH.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-3CHE3NYR.js → blockDiagram-VD42YOAC-DikRTLHM.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-K8hDNmEC.js → c4Diagram-YG6GDRKO-2zOWoKX8.js} +1 -1
- package/dist-renderer/assets/channel-BL6oUYPL.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-5OabZrhH.js → chunk-4BX2VUAB-7PeMCF0g.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-v2kdM_aT.js → chunk-55IACEB6-6LuNK7l-.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-C0Ju56SH.js → chunk-B4BG7PRW-9Q4M3OpN.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-DPTWTKRm.js → chunk-DI55MBZ5-mGGPKOCm.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DSkYppkv.js → chunk-FMBD7UC4-BTZYjAAP.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-C_4cCLCl.js → chunk-QN33PNHL-D9_yb-Pl.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-ojL7PmOD.js → chunk-QZHKN3VN-CVMnt9Of.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-D1g7Vl_v.js → chunk-TZMSLE5B-B11BOOB_.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DLZQJPSd.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DLZQJPSd.js +1 -0
- package/dist-renderer/assets/clone-CRwa9g7P.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-TJnGh924.js → cose-bilkent-S5V4N54A-Dx1iOVS_.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-cPgfHhoX.js → dagre-6UL2VRFP-BfFhjHyX.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BS5Y-RR6.js → diagram-PSM6KHXK-BjauYjwN.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-D9AF7AGJ.js → diagram-QEK2KX5R-DTBnRCua.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DTFUadMS.js → diagram-S2PKOQOG-CBhKzb3E.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-DB_StEwC.js → erDiagram-Q2GNP2WA-B0xgzQzS.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-DGn40aPj.js → flowDiagram-NV44I4VS-klmMoWI3.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-9NiFCSBT.js → ganttDiagram-JELNMOA3-BSv5ddzU.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BdveeU3c.js → gitGraphDiagram-V2S2FVAM-CEUBnydU.js} +1 -1
- package/dist-renderer/assets/{graph-aQYbgTDH.js → graph-CIysEkgE.js} +1 -1
- package/dist-renderer/assets/{index-CrCHolXN.js → index-7EkmxIrp.js} +1 -1
- package/dist-renderer/assets/{index-CWqPn0NY.js → index-BK6dwrxm.js} +1 -1
- package/dist-renderer/assets/{index-CaG9mf8s.css → index-C4x095x4.css} +1 -1
- package/dist-renderer/assets/{index-oyepEosi.js → index-CJLDGPfy.js} +1 -1
- package/dist-renderer/assets/{index-DiAK42nd.js → index-CSheMJTk.js} +1 -1
- package/dist-renderer/assets/{index-DmgKTZAa.js → index-CcdwUZk9.js} +529 -524
- package/dist-renderer/assets/{index-DyEKO6GV.js → index-DA5wdGF_.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-Dmc_xn8U.js → infoDiagram-HS3SLOUP-BgF7b8R8.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-D9LJr-B5.js → journeyDiagram-XKPGCS4Q-Brnq6sZ9.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CjOWoNys.js → kanban-definition-3W4ZIXB7-oEx_7thy.js} +1 -1
- package/dist-renderer/assets/{layout-D6GzYK4K.js → layout-Bb-Myf8p.js} +1 -1
- package/dist-renderer/assets/{linear-Dt3GyUQf.js → linear-BC5s25rz.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-XwY2hZr8.js → mindmap-definition-VGOIOE7T-MB1jxroI.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BU4nfYd7.js → pieDiagram-ADFJNKIX-DSEfLnAV.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BYk6f63x.js → quadrantDiagram-AYHSOK5B-Bm0Xw7gV.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-kbadr_bU.js → requirementDiagram-UZGBJVZJ-Cjn0opjv.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-ZstP2Vth.js → sankeyDiagram-TZEHDZUN-P7KZIQkA.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-obK_-ssz.js → sequenceDiagram-WL72ISMW-DHDX_5Fl.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BgZDg0VT.js → stateDiagram-FKZM4ZOC-DGyXzpw9.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CMa5sz7x.js → stateDiagram-v2-4FDKWEC3-sTtOoyBH.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BOmCNnab.js → timeline-definition-IT6M3QCI-BLoCrtzz.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-CI5sRz-0.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-BzAHNASi.js → xychartDiagram-PRI3JC2R-CkvLu9vG.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +23 -17
- package/src/main/server.ts +179 -23
- package/src/main/services/teams-mvp/TaskDispatchService.ts +440 -0
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +36 -33
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +2 -0
- package/src/renderer/components/settings/SettingsTabs.tsx +8 -2
- package/src/renderer/components/settings/SettingsView.tsx +4 -0
- package/src/renderer/components/settings/sections/GeneralSection.tsx +168 -206
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +176 -0
- package/src/renderer/components/sidebar/SidebarSessions.tsx +31 -4
- package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +37 -0
- package/src/renderer/components/team/messages/MessageComposer.tsx +36 -228
- package/src/renderer/components/team/messages/MessagesPanel.tsx +0 -3
- package/src/renderer/store/slices/teamSlice.ts +30 -1
- package/src/shared/types/team.ts +73 -0
- package/dist-renderer/assets/channel-BSWYOYIc.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-mw4yABob.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-mw4yABob.js +0 -1
- package/dist-renderer/assets/clone-KtZfFt-o.js +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-BU0ha0Ww.js +0 -162
|
@@ -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 =
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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 &&
|
|
209
|
+
const supportsAttachments = isLeadRecipient && !!isTeamAlive;
|
|
280
210
|
const canAttach = supportsAttachments && draft.canAddMore;
|
|
281
211
|
const attachmentRestrictionReason = !supportsAttachments
|
|
282
|
-
?
|
|
283
|
-
? '
|
|
284
|
-
:
|
|
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
|
-
:
|
|
293
|
-
? '
|
|
294
|
-
: !
|
|
295
|
-
? '
|
|
296
|
-
:
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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={
|
|
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
|
}));
|
package/src/shared/types/team.ts
CHANGED
|
@@ -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). */
|