@wowyuarm/dsh-agent-team 0.1.9 → 0.1.11
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 +17 -11
- package/README.zh.md +17 -11
- package/package.json +61 -47
- package/packages/agent-team/README.md +6 -4
- package/packages/agent-team/README.zh.md +6 -3
- package/packages/agent-team/lib/context-management.js +48 -55
- package/packages/agent-team/lib/context-projection.js +22 -8
- package/packages/agent-team/lib/context-source.js +163 -23
- package/packages/agent-team/lib/index.js +349 -176
- package/packages/agent-team/lib/invariant.js +38 -4
- package/packages/agent-team/lib/ledger.js +658 -294
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-time-context.js +141 -0
- package/packages/agent-team/lib/pressure-policy.js +34 -11
- package/packages/agent-team/lib/progress-nudge.js +12 -4
- package/packages/agent-team/lib/session-event-cursor.js +116 -0
- package/packages/agent-team/lib/session-remediation.js +493 -0
- package/packages/agent-team/lib/spec.js +49 -36
- package/packages/agent-team/lib/stored-session-reader.js +138 -0
- package/packages/agent-team/lib/time-format.js +56 -0
- package/packages/agent-team/lib/typert.host.js +163 -63
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +105 -32
- package/packages/agent-team/lib/types/context-management.d.ts +13 -1
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-source.d.ts +70 -36
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +110 -9
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +205 -7
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
- package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
- package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts +172 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
- package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
- package/packages/agent-team/lib/types/time-format.d.ts +28 -0
- package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
- package/packages/agent-team/lib/types/types/entities.d.ts +36 -2
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +50 -3
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +21 -4
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +1121 -402
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -4
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +42 -22
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +13 -24
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +15 -9
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +8 -1
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +21 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +71 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +13 -6
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +106 -32
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +50 -7
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +25 -9
- package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +18 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts +26 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.js +66 -15
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +34 -6
- package/packages/tool-agent-team/README.md +8 -6
- package/packages/tool-agent-team/README.zh.md +8 -6
- package/packages/tool-agent-team/lib/context-tools.js +18 -14
- package/packages/tool-agent-team/lib/host-access.js +20 -0
- package/packages/tool-agent-team/lib/index.js +314 -123
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
- package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamAgentsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamAgentsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,2BAA2B,EAE5B,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"TeamAgentsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamAgentsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,2BAA2B,EAE5B,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAalD,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,kBAAkB,CAAC,CAAA;IAC/D,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACjD,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAA;IACnD,8EAA8E;IAC9E,QAAQ,CAAC,eAAe,CAAC,EAAE,2BAA2B,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,CAAA;IAC7E,QAAQ,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,mBAAmB,CAAC,CAAA;IACjE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,OAAO,EAAE,yBAAyB,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC1F,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CAClC;AAED,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC,EAAE,EAAE,oBAAoB,+BAwO/N"}
|
|
@@ -6,6 +6,7 @@ import { SortableRow, useSidebarRowDrag } from "./sidebar-drag.js";
|
|
|
6
6
|
import { moveSidebarItem, useSidebarOrder } from "./sidebar-order.js";
|
|
7
7
|
import { useSidebarSectionOpen, setSidebarSectionOpen } from "./sidebar-sections.js";
|
|
8
8
|
import { mintRequestId } from "./requests.js";
|
|
9
|
+
import { diagnosticText, restartOffered } from "./TeamPresenceDot.js";
|
|
9
10
|
import { TeamRowMenu } from "./TeamRowMenu.js";
|
|
10
11
|
import { TeamSidebarSection } from "./TeamSidebarSection.js";
|
|
11
12
|
import { AgentEditorDialog, ModelPickerField, sameModel } from "./TeamMemberEditor.js";
|
|
@@ -87,8 +88,12 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
}, []);
|
|
91
|
+
// Only the first refresh owns the loading surface; presence wakes (Agent
|
|
92
|
+
// running/idle) ride the dedicated scope and refresh rows in place.
|
|
93
|
+
const loadedRef = useRef(false);
|
|
90
94
|
const refresh = useCallback(async () => {
|
|
91
|
-
|
|
95
|
+
if (!loadedRef.current)
|
|
96
|
+
setLoading(true);
|
|
92
97
|
const result = await loadMembers({ workspaceId });
|
|
93
98
|
if (result.ok) {
|
|
94
99
|
// Archived Members are hidden from every surface; the row disappears
|
|
@@ -97,6 +102,7 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
97
102
|
setMembers(next);
|
|
98
103
|
followRollover(next);
|
|
99
104
|
setError(undefined);
|
|
105
|
+
loadedRef.current = true;
|
|
100
106
|
}
|
|
101
107
|
else {
|
|
102
108
|
setError(result.error.message);
|
|
@@ -113,6 +119,15 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
113
119
|
// rides every workspace invalidation.
|
|
114
120
|
void refresh();
|
|
115
121
|
}), [subscribeChanges, refresh, workspaceId]);
|
|
122
|
+
// Presence transitions commit nothing: the Host wakes only this scope, so
|
|
123
|
+
// the green dots stay live without any catalog refetch.
|
|
124
|
+
useEffect(() => subscribeChanges({ kind: 'presence', workspaceId }, update => {
|
|
125
|
+
if (update.type === 'failed') {
|
|
126
|
+
setError(update.message);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
void refresh();
|
|
130
|
+
}), [subscribeChanges, refresh, workspaceId]);
|
|
116
131
|
const closeForm = () => {
|
|
117
132
|
if (creating)
|
|
118
133
|
return;
|
|
@@ -138,7 +153,7 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
138
153
|
setModel(undefined);
|
|
139
154
|
setFormOpen(false);
|
|
140
155
|
if (result.value.status.presence === 'unavailable') {
|
|
141
|
-
setError(result.value.status
|
|
156
|
+
setError(diagnosticText(result.value.status) || t('statusUnavailable'));
|
|
142
157
|
}
|
|
143
158
|
else {
|
|
144
159
|
setRetryRequest(undefined);
|
|
@@ -207,7 +222,12 @@ function AgentRow({ status, current, updateMember, recoverMember, archiveMember,
|
|
|
207
222
|
return;
|
|
208
223
|
}
|
|
209
224
|
if (result.value.status.availability === 'unavailable') {
|
|
210
|
-
|
|
225
|
+
const diagnostic = result.value.status.diagnostic;
|
|
226
|
+
if (diagnostic?.class === 'session-refused' && diagnostic.remediable === false) {
|
|
227
|
+
setRowAlert(t('restartRefusedNoRestart', { detail: diagnosticText(result.value.status) }));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
setRowAlert(t('restartStillUnavailable', { diagnostic: diagnosticText(result.value.status) || t('statusUnavailable') }));
|
|
211
231
|
return;
|
|
212
232
|
}
|
|
213
233
|
setRowAlert(undefined);
|
|
@@ -238,7 +258,7 @@ function AgentRow({ status, current, updateMember, recoverMember, archiveMember,
|
|
|
238
258
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.agentRow, "data-menu-open": menuOpen || undefined, children: [_jsxs("button", { type: "button", className: css.agentSelect, "aria-label": t('openAgentSession', { name: status.member.handle }), "aria-current": current ? 'page' : undefined, disabled: status.availability !== 'active', onClick: () => { openMemberSession(status.member.sessionId); }, children: [_jsx(TeamMemberAvatar, { status: status, t: t }), _jsxs("span", { className: css.agentCopy, children: [_jsx("strong", { children: status.member.handle }), _jsx("small", { children: status.member.description })] })] }), _jsx("span", { className: css.rowMenu, children: _jsx(TeamRowMenu, { label: t('actionsAgent', { name: status.member.handle }), items: [
|
|
239
259
|
{ id: 'edit', label: t('editAgent'), icon: _jsx(IconEditOutline16, {}) },
|
|
240
260
|
...(status.presence === 'error' ? [{ id: 'resume', label: t('resumeAgent'), icon: _jsx(IconPlayOutline16, {}) }] : []),
|
|
241
|
-
...(status.availability === 'unavailable' ? [{ id: 'restart', label: t('restartAgent'), icon: _jsx(IconRefreshOutline16, {}) }] : []),
|
|
261
|
+
...(status.availability === 'unavailable' && restartOffered(status) ? [{ id: 'restart', label: t('restartAgent'), icon: _jsx(IconRefreshOutline16, {}) }] : []),
|
|
242
262
|
{ id: 'archive', label: t('archiveAgent'), icon: _jsx(IconArchiveOutline20, { size: 16 }), danger: true },
|
|
243
263
|
], onSelect: (id) => {
|
|
244
264
|
if (id === 'edit')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamChannelPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA+B,mBAAmB,EAE7D,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"TeamChannelPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA+B,mBAAmB,EAE7D,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEvD,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,aAAa,CAAA;AAe/D,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAA;IACxC,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,qBAAqB,CAAC,CAAA;IAC1E,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,cAAc,EAAE,qBAAqB,CAAC,gBAAgB,CAAC,CAAA;IAChE,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AAmBD,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC,EAAE,EAAE,oBAAoB,+BA4VnR"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useEffect, useRef, useState
|
|
2
|
+
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Button, IconChevronLeftOutline14, IconChevronRightOutline14, Modal } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
-
import {
|
|
5
|
-
import { mintRequestId } from "./requests.js";
|
|
4
|
+
import { mintRequestId, uploadComposerFiles } from "./requests.js";
|
|
6
5
|
import { TeamComposer } from "./TeamComposer.js";
|
|
7
6
|
import { TeamPresenceDot } from "./TeamPresenceDot.js";
|
|
8
7
|
import { TeamMessage } from "./TeamMessage.js";
|
|
@@ -45,11 +44,15 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
45
44
|
const [managingMembers, setManagingMembers] = useState(false);
|
|
46
45
|
// The composer draft lives in the keyed draft cache: view switches unmount
|
|
47
46
|
// this page, and a refresh must not cost the half-written message either.
|
|
47
|
+
// The composer owns the subscription — this page only reads a snapshot when
|
|
48
|
+
// it sends, so typing never re-renders the timeline.
|
|
48
49
|
const draftKey = `channel:${channelRef}`;
|
|
49
|
-
const { draft, recipients } = useSyncExternalStore(drafts.subscribe, () => drafts.getSnapshot(draftKey));
|
|
50
50
|
const manageTriggerRef = useRef(null);
|
|
51
51
|
const memberListRef = useRef(null);
|
|
52
52
|
const mountedRef = useRef(false);
|
|
53
|
+
// Flips after the first successful timeline load; later change wakes
|
|
54
|
+
// refresh in place instead of showing the loading surface again.
|
|
55
|
+
const loadedRef = useRef(false);
|
|
53
56
|
const refreshSequenceRef = useRef(0);
|
|
54
57
|
const channelLastItem = view?.items[view.items.length - 1];
|
|
55
58
|
// Branded-ref navigation for message bodies: channel refs hop directly,
|
|
@@ -106,7 +109,10 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
106
109
|
return false;
|
|
107
110
|
const sequence = refreshSequenceRef.current + 1;
|
|
108
111
|
refreshSequenceRef.current = sequence;
|
|
109
|
-
|
|
112
|
+
// Only the first refresh owns the loading surface; change wakes refresh
|
|
113
|
+
// the rendered timeline in place instead of flashing it back to skeleton.
|
|
114
|
+
if (!loadedRef.current)
|
|
115
|
+
setLoading(true);
|
|
110
116
|
if (clearError) {
|
|
111
117
|
setError(undefined);
|
|
112
118
|
setStatusMessage(undefined);
|
|
@@ -118,8 +124,10 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
118
124
|
]);
|
|
119
125
|
if (!mountedRef.current || sequence !== refreshSequenceRef.current)
|
|
120
126
|
return false;
|
|
121
|
-
if (loaded.ok)
|
|
127
|
+
if (loaded.ok) {
|
|
122
128
|
setView(current => current === undefined ? loaded.value : mergeChannelView(current, loaded.value));
|
|
129
|
+
loadedRef.current = true;
|
|
130
|
+
}
|
|
123
131
|
else
|
|
124
132
|
setError(loaded.error.message);
|
|
125
133
|
if (loadedMembers.ok)
|
|
@@ -159,6 +167,7 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
159
167
|
};
|
|
160
168
|
useEffect(() => {
|
|
161
169
|
mountedRef.current = true;
|
|
170
|
+
loadedRef.current = false;
|
|
162
171
|
setView(undefined);
|
|
163
172
|
setError(undefined);
|
|
164
173
|
setLoading(true);
|
|
@@ -183,6 +192,17 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
183
192
|
}
|
|
184
193
|
void refreshMembers();
|
|
185
194
|
}),
|
|
195
|
+
// Presence transitions commit nothing: only the member rows move, so
|
|
196
|
+
// the header presence counts refresh without a timeline refetch.
|
|
197
|
+
subscribeChanges({ kind: 'presence', workspaceId }, update => {
|
|
198
|
+
if (!mountedRef.current)
|
|
199
|
+
return;
|
|
200
|
+
if (update.type === 'failed') {
|
|
201
|
+
setError(update.message);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
void refreshMembers();
|
|
205
|
+
}),
|
|
186
206
|
];
|
|
187
207
|
return () => {
|
|
188
208
|
mountedRef.current = false;
|
|
@@ -227,7 +247,16 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
227
247
|
// Retained across transport failures so a replayed send dedupes on the Host;
|
|
228
248
|
// definitive outcomes (committed or rejected) start the next send fresh.
|
|
229
249
|
const pendingSendId = useRef();
|
|
250
|
+
// Editing the draft invalidates the one-shot status line. The setter returns
|
|
251
|
+
// the same state when there is nothing to clear, so a keystroke does not
|
|
252
|
+
// re-render the timeline now that the composer owns the draft.
|
|
253
|
+
const clearSendState = useCallback(() => {
|
|
254
|
+
setStatusMessage(current => current === undefined ? current : undefined);
|
|
255
|
+
}, []);
|
|
230
256
|
const send = async () => {
|
|
257
|
+
// Read the draft at send time: this page no longer subscribes to it, so a
|
|
258
|
+
// captured render value would be stale after the composer's own edits.
|
|
259
|
+
const { draft, recipients } = drafts.getSnapshot(draftKey);
|
|
231
260
|
if (pending || draft.trim() === '')
|
|
232
261
|
return;
|
|
233
262
|
const recipientIds = [...recipients].sort();
|
|
@@ -239,22 +268,13 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
239
268
|
try {
|
|
240
269
|
// Upload chosen files first; any failure aborts the send with the
|
|
241
270
|
// existing error surface and keeps the chips for a retry.
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
name: file.name,
|
|
248
|
-
mediaType: file.type === '' ? undefined : file.type,
|
|
249
|
-
bytesBase64: bytesToBase64(new Uint8Array(await file.arrayBuffer())),
|
|
250
|
-
});
|
|
251
|
-
if (!uploaded.ok) {
|
|
252
|
-
pendingSendId.current = undefined;
|
|
253
|
-
setError(uploaded.error.message);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
attachmentIds.push(uploaded.value.attachmentId);
|
|
271
|
+
const upload = await uploadComposerFiles(putAttachment, workspaceId, pendingFiles);
|
|
272
|
+
if (!upload.ok) {
|
|
273
|
+
pendingSendId.current = undefined;
|
|
274
|
+
setError(upload.error);
|
|
275
|
+
return;
|
|
257
276
|
}
|
|
277
|
+
const attachmentIds = upload.attachmentIds;
|
|
258
278
|
const request = {
|
|
259
279
|
requestId, workspaceId,
|
|
260
280
|
channelRef, body: draft.trim(), recipients: recipientIds,
|
|
@@ -306,5 +326,5 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
306
326
|
const turnGap = isRunGap(index > 0 ? block.items[index - 1].message.occurredAt : undefined, item.message.occurredAt);
|
|
307
327
|
// A turn divider already labels the gapped entry; its row stays chrome-free.
|
|
308
328
|
return _jsxs(Fragment, { children: [turnGap && _jsx(TeamRunDivider, { occurredAt: item.message.occurredAt }), _jsxs(TeamMessage, { senderName: sender, memberId: item.message.sender, human: human, body: item.message.body, attachments: item.message.attachments, loadAttachment: getAttachment, t: t, occurredAt: item.message.occurredAt, mentionNames: mentionNamesOf(item.mentions, mentionHandlesMap), onOpenRef: openRef, onResolveTaskRefs: lookupTaskRefs, grouped: index > 0, showGroupedTime: item.message.topLevel === true && !turnGap, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }), children: [item.message.topLevel && item.task !== undefined && item.taskNumber !== undefined && _jsxs("button", { type: "button", className: channelCss.taskFooter, "aria-label": t('openTask', { number: item.taskNumber }), onClick: () => { selectThread(item.thread.threadRef, channelRef, item.task?.taskRef, item.taskNumber); }, children: [_jsx("span", { className: channelCss.taskDot, children: _jsx(TeamStateDot, { size: 8, state: taskStatusDot(item.task.status) }) }), _jsx("span", { className: channelCss.taskNumber, children: t('taskLabel', { number: item.taskNumber }) }), _jsx("span", { className: channelCss.taskStatus, children: formatTaskStatus(item.task.status, t) }), _jsx("span", { className: channelCss.taskCount, children: t('taskMessageCount', { count: item.messageCount }) }), _jsx("span", { className: channelCss.taskArrow, "aria-hidden": "true", children: _jsx(IconChevronRightOutline14, { size: 12 }) })] }), item.message.topLevel && item.task === undefined && _jsxs("button", { type: "button", className: channelCss.taskFooter, "aria-label": t('openThread'), onClick: () => { selectThread(item.thread.threadRef, channelRef); }, children: [_jsx("span", { className: channelCss.taskNumber, children: t('threadLabel') }), _jsx("span", { className: channelCss.taskCount, children: t('taskMessageCount', { count: item.messageCount }) }), _jsx("span", { className: channelCss.taskArrow, "aria-hidden": "true", children: _jsx(IconChevronRightOutline14, { size: 12 }) })] })] })] }, item.message.messageRef);
|
|
309
|
-
}) }, `run-${block.items[0].message.messageRef}`))] }) }), channel !== undefined ? _jsx(TeamComposer, { members: channelMembers,
|
|
329
|
+
}) }, `run-${block.items[0].message.messageRef}`))] }) }), channel !== undefined ? _jsx(TeamComposer, { members: channelMembers, drafts: drafts, draftKey: draftKey, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onEdit: clearSendState, onSubmit: () => { void send(); }, pendingFiles: pendingFiles, onFilesChange: setPendingFiles, asTask: asTask, onAsTaskChange: setAsTask, t: t }, draftKey) : _jsx("div", {})] });
|
|
310
330
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamChannelsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EAEzB,mBAAmB,EAMpB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"TeamChannelsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EAEzB,mBAAmB,EAMpB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAclD,UAAU,sBAAsB;IAC9B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,wGAAwG;IACxG,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,kBAAkB,CAAC,CAAA;IAC/D,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,CAAA;IAC3D,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,qBAAqB,CAAC,CAAA;IACrE,QAAQ,CAAC,cAAc,EAAE,SAAS,yBAAyB,EAAE,CAAA;IAC7D,QAAQ,CAAC,kBAAkB,CAAC,EAAE,mBAAmB,CAAA;IACjD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CAClC;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,+BAmM9D"}
|
|
@@ -10,6 +10,7 @@ import { mintRequestId } from "./requests.js";
|
|
|
10
10
|
import { TeamRowMenu } from "./TeamRowMenu.js";
|
|
11
11
|
import { TeamSidebarSection } from "./TeamSidebarSection.js";
|
|
12
12
|
import { useChannelMembership } from "./team-membership.js";
|
|
13
|
+
import { useEditDialogSave } from "./team-dialog-save.js";
|
|
13
14
|
import createCss from './create.module.css';
|
|
14
15
|
import css from './sidebar.module.css';
|
|
15
16
|
export function TeamChannelsPanel(props) {
|
|
@@ -39,8 +40,12 @@ export function TeamChannelsPanel(props) {
|
|
|
39
40
|
};
|
|
40
41
|
const drag = useSidebarRowDrag({ refs: orderedChannelRefs, onCommit: applyMove });
|
|
41
42
|
const sectionOpen = useSidebarSectionOpen(workspaceId, 'channels');
|
|
43
|
+
// Only the first refresh owns the loading surface; later wakes (workspace
|
|
44
|
+
// catalog changes, member creation) refresh the rendered rows in place.
|
|
45
|
+
const loadedRef = useRef(false);
|
|
42
46
|
const refresh = useCallback(async () => {
|
|
43
|
-
|
|
47
|
+
if (!loadedRef.current)
|
|
48
|
+
setLoading(true);
|
|
44
49
|
const [channelResult, memberResult] = await Promise.all([
|
|
45
50
|
loadChannels({ workspaceId, limit: 1 }),
|
|
46
51
|
loadMembers({ workspaceId }),
|
|
@@ -53,6 +58,7 @@ export function TeamChannelsPanel(props) {
|
|
|
53
58
|
.map(status => status.member.memberId));
|
|
54
59
|
setSelected(current => new Set([...current].filter(memberId => selectable.has(memberId))));
|
|
55
60
|
setError(undefined);
|
|
61
|
+
loadedRef.current = true;
|
|
56
62
|
}
|
|
57
63
|
else if (!channelResult.ok) {
|
|
58
64
|
setError(channelResult.error.message);
|
|
@@ -193,9 +199,11 @@ function ChannelRow({ channel, members, joinedIds, selected, updateChannel, arch
|
|
|
193
199
|
function ChannelEditorDialog({ channel, members, joinedIds, updateChannel, joinChannel, removeChannelMember, onCommitted, onClose, t }) {
|
|
194
200
|
const [name, setName] = useState(channel.name);
|
|
195
201
|
const [description, setDescription] = useState(channel.description);
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
const { saving, error, pendingRequest, save } = useEditDialogSave({
|
|
203
|
+
save: updateChannel,
|
|
204
|
+
onCommitted,
|
|
205
|
+
onClose,
|
|
206
|
+
});
|
|
199
207
|
const membership = useChannelMembership({ joinChannel, removeChannelMember }, change => change.memberId, async () => { await onCommitted(); });
|
|
200
208
|
const dirty = name.trim() !== channel.name || description.trim() !== channel.description;
|
|
201
209
|
const submit = async (event) => {
|
|
@@ -213,26 +221,7 @@ function ChannelEditorDialog({ channel, members, joinedIds, updateChannel, joinC
|
|
|
213
221
|
name: normalizedName,
|
|
214
222
|
description: normalizedDescription,
|
|
215
223
|
};
|
|
216
|
-
|
|
217
|
-
setSaving(true);
|
|
218
|
-
setError(undefined);
|
|
219
|
-
try {
|
|
220
|
-
const result = await updateChannel(request);
|
|
221
|
-
if (result.ok) {
|
|
222
|
-
pendingRequest.current = undefined;
|
|
223
|
-
await onCommitted();
|
|
224
|
-
onClose();
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
setError(result.error.message);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
catch (cause) {
|
|
231
|
-
setError(cause instanceof Error ? cause.message : String(cause));
|
|
232
|
-
}
|
|
233
|
-
finally {
|
|
234
|
-
setSaving(false);
|
|
235
|
-
}
|
|
224
|
+
await save(request);
|
|
236
225
|
};
|
|
237
226
|
return (_jsx(Modal, { open: true, onClose: onClose, title: t('editChannel'), description: `# ${channel.name}`, closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-channel-edit-form", variant: "primary", disabled: saving || !dirty || name.trim() === '', children: saving ? t('editSaving') : t('editSave') })] }), children: _jsxs("form", { id: "team-channel-edit-form", className: createCss.form, onSubmit: event => { void submit(event); }, children: [_jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('channelName') }), _jsx(Input, { className: createCss.input, value: name, onChange: event => { setName(event.target.value); pendingRequest.current = undefined; }, disabled: saving, autoFocus: true })] }), _jsxs("label", { className: createCss.field, children: [_jsxs("span", { children: [t('channelDescription'), t('optionalSuffix')] }), _jsx(Input, { className: createCss.input, value: description, placeholder: t('agentDescriptionPlaceholder'), onChange: event => { setDescription(event.target.value); pendingRequest.current = undefined; }, disabled: saving })] }), _jsxs("fieldset", { className: createCss.memberPicker, disabled: saving, children: [_jsx("legend", { children: t('channelMembersSection') }), members.map(status => {
|
|
238
227
|
const joined = joinedIds.has(status.member.memberId);
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import type { AgentTeamClientMemberStatus, AgentTeamMemberId } from '@wowyuarm/dsh-agent-team/types';
|
|
2
2
|
import type { TeamConversationProps } from './slots.ts';
|
|
3
|
-
|
|
3
|
+
import type { TeamDraftKey, TeamDraftStore } from './drafts.ts';
|
|
4
|
+
export declare function TeamComposer({ members, followerMemberIds, drafts, draftKey, pending, confirmation, error, onEdit, onSubmit, placeholder, pendingFiles, onFilesChange, asTask, onAsTaskChange, t }: {
|
|
4
5
|
readonly members: readonly AgentTeamClientMemberStatus[];
|
|
5
6
|
/** Current Thread followers; the Thread surface passes them so they rank above other candidates. */
|
|
6
7
|
readonly followerMemberIds?: ReadonlySet<AgentTeamMemberId>;
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The draft cache this composer subscribes to: owning the subscription here
|
|
10
|
+
* instead of in the hosting page keeps a keystroke from re-rendering the
|
|
11
|
+
* timeline around it.
|
|
12
|
+
*/
|
|
13
|
+
readonly drafts: TeamDraftStore;
|
|
14
|
+
readonly draftKey: TeamDraftKey;
|
|
9
15
|
readonly pending: boolean;
|
|
10
16
|
readonly confirmation?: string;
|
|
11
17
|
readonly error?: string;
|
|
12
|
-
|
|
13
|
-
readonly
|
|
18
|
+
/** The Human edited the draft: the hosting page drops its one-shot send state. */
|
|
19
|
+
readonly onEdit?: () => void;
|
|
14
20
|
readonly onSubmit: () => void;
|
|
15
21
|
/** Conversation-specific prompt; the shared default fits Channel surfaces. */
|
|
16
22
|
readonly placeholder?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamComposer.d.ts","sourceRoot":"","sources":["../../../src/client/TeamComposer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAEpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"TeamComposer.d.ts","sourceRoot":"","sources":["../../../src/client/TeamComposer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAEpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAqE/D,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE;IAClM,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAA;IACxD,oGAAoG;IACpG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;IAC3D;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,IAAI,EAAE,CAAA;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,KAAK,IAAI,CAAA;IACzD,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;IACnD,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC,+BAyTA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { IconChecklistOutline14, IconPaperclipOutline16, IconSendOutline16, Tooltip, useAnchoredMaxHeight, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
4
|
import { TeamPresenceDot } from "./TeamPresenceDot.js";
|
|
5
5
|
import css from './composer.module.css';
|
|
6
6
|
import { formatByteSize } from "./attachment-preview.js";
|
|
@@ -56,13 +56,17 @@ function draftPreviewUrl(file) {
|
|
|
56
56
|
}
|
|
57
57
|
return url;
|
|
58
58
|
}
|
|
59
|
-
export function TeamComposer({ members, followerMemberIds,
|
|
59
|
+
export function TeamComposer({ members, followerMemberIds, drafts, draftKey, pending, confirmation, error, onEdit, onSubmit, placeholder, pendingFiles, onFilesChange, asTask, onAsTaskChange, t }) {
|
|
60
60
|
const inputRef = useRef(null);
|
|
61
61
|
const fileInputRef = useRef(null);
|
|
62
62
|
const rootRef = useRef(null);
|
|
63
63
|
const menuRef = useRef(null);
|
|
64
64
|
const activeOptionRef = useRef(null);
|
|
65
65
|
const composingRef = useRef(false);
|
|
66
|
+
// The draft lives in the injected cache so a view switch or refresh does not
|
|
67
|
+
// cost the half-written message. Subscribing here — not in the page that
|
|
68
|
+
// hosts this composer — is what keeps a keystroke off the timeline.
|
|
69
|
+
const { draft, recipients } = useSyncExternalStore(drafts.subscribe, () => drafts.getSnapshot(draftKey));
|
|
66
70
|
const [mention, setMention] = useState();
|
|
67
71
|
const [highlight, setHighlight] = useState(0);
|
|
68
72
|
// @all is a composer-layer expansion of the same eligibility filter a
|
|
@@ -128,7 +132,7 @@ export function TeamComposer({ members, followerMemberIds, recipients, draft, pe
|
|
|
128
132
|
return member !== undefined && containsMention(nextDraft, member.handle);
|
|
129
133
|
}));
|
|
130
134
|
if (next.size !== recipients.size)
|
|
131
|
-
|
|
135
|
+
drafts.writeRecipients(draftKey, next);
|
|
132
136
|
};
|
|
133
137
|
// Restored drafts may carry recipients that no longer match the text (or
|
|
134
138
|
// unknown Members); converge on mount and on every state change so the
|
|
@@ -147,15 +151,17 @@ export function TeamComposer({ members, followerMemberIds, recipients, draft, pe
|
|
|
147
151
|
};
|
|
148
152
|
const onChange = (event) => {
|
|
149
153
|
const nextDraft = event.target.value;
|
|
150
|
-
|
|
154
|
+
drafts.writeDraft(draftKey, nextDraft);
|
|
155
|
+
onEdit?.();
|
|
151
156
|
pruneRecipients(nextDraft);
|
|
152
157
|
updateMention(nextDraft, event.target.selectionStart ?? nextDraft.length);
|
|
153
158
|
};
|
|
154
159
|
// Both pick kinds share the same commit path: swap the text in, add the
|
|
155
160
|
// recipients, close the menu, and restore the caret after focus.
|
|
156
161
|
const commitMention = (nextDraft, nextCaret, nextRecipients) => {
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
drafts.writeDraft(draftKey, nextDraft);
|
|
163
|
+
drafts.writeRecipients(draftKey, nextRecipients);
|
|
164
|
+
onEdit?.();
|
|
159
165
|
setMention(undefined);
|
|
160
166
|
setHighlight(0);
|
|
161
167
|
requestAnimationFrame(() => {
|
|
@@ -254,12 +260,12 @@ export function TeamComposer({ members, followerMemberIds, recipients, draft, pe
|
|
|
254
260
|
URL.revokeObjectURL(url);
|
|
255
261
|
onFilesChange(pendingFiles.filter((_, candidate) => candidate !== index));
|
|
256
262
|
}, children: "\u00D7" })] }, `${file.name}-${index}`);
|
|
257
|
-
}) })), _jsxs("div", { className: css.toolbar, children: [
|
|
263
|
+
}) })), _jsxs("div", { className: css.toolbar, children: [onFilesChange !== undefined && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: css.fileInput, "aria-hidden": "true", tabIndex: -1, onChange: event => {
|
|
258
264
|
const chosen = [...event.target.files ?? []];
|
|
259
265
|
if (chosen.length > 0 && pendingFiles !== undefined)
|
|
260
266
|
onFilesChange([...pendingFiles, ...chosen]);
|
|
261
267
|
event.target.value = '';
|
|
262
|
-
} }), _jsx("button", { type: "button", className: css.attachButton, "aria-label": t('attachFiles'), title: t('
|
|
268
|
+
} }), _jsx(Tooltip, { label: t('attachFiles'), side: "top", delayMs: 500, children: _jsx("button", { type: "button", className: css.attachButton, "aria-label": t('attachFiles'), disabled: pending, onClick: () => { fileInputRef.current?.click(); }, children: _jsx(IconPaperclipOutline16, { size: 14 }) }) })] })), onAsTaskChange !== undefined && (_jsxs("button", { type: "button", className: asTask === true ? `${css.asTaskPill} ${css.asTaskPillOn}` : css.asTaskPill, "aria-label": t('asTask'), "aria-pressed": asTask === true, title: t('asTask'), disabled: pending, onClick: () => { onAsTaskChange(asTask !== true); }, children: [_jsx(IconChecklistOutline14, { size: 14 }), _jsx("span", { className: css.asTaskLabel, children: t('asTask') })] })), _jsx("button", { type: "submit", className: css.sendButton, "aria-label": pending ? t('sendingMessage') : t('sendMessage'), disabled: pending || draft.trim() === '', onMouseDown: event => {
|
|
263
269
|
// Keep the composer focused when the send control is clicked.
|
|
264
270
|
event.preventDefault();
|
|
265
271
|
inputRef.current?.focus({ preventScroll: true });
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { TeamConversationProps } from './slots.ts';
|
|
2
|
-
export declare function TeamConversation({ t, useWorkspaces, navigation, drafts, putAttachment, getAttachment, loadChannels, readThread, loadThreadHistory, threadObservations, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, promoteThread, selectThread, selectChannel, backToWorkspace, backToChannels, resolveTaskRefs }: TeamConversationProps): import("react").JSX.Element;
|
|
2
|
+
export declare function TeamConversation({ t, useWorkspaces, navigation, drafts, putAttachment, getAttachment, loadChannels, readThread, loadThreadHistory, threadObservations, subscribeChanges, loadMembers, loadInbox, sendMessage, joinChannel, removeChannelMember, reply, changeTask, promoteThread, selectThread, selectChannel, selectWorkspace, backToWorkspace, backToChannels, resolveTaskRefs }: TeamConversationProps): import("react").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=TeamConversation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamConversation.d.ts","sourceRoot":"","sources":["../../../src/client/TeamConversation.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"TeamConversation.d.ts","sourceRoot":"","sources":["../../../src/client/TeamConversation.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAMvD,wBAAgB,gBAAgB,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,EAAE,qBAAqB,+BA0BzZ"}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useSyncExternalStore } from 'react';
|
|
3
3
|
import { TeamChannelPage } from "./TeamChannelPage.js";
|
|
4
|
+
import { TeamInboxPage } from "./TeamInboxPage.js";
|
|
4
5
|
import { TeamThreadPage } from "./TeamThreadPage.js";
|
|
5
6
|
import css from './conversation.module.css';
|
|
6
|
-
export function TeamConversation({ t, useWorkspaces, navigation, drafts, putAttachment, getAttachment, loadChannels, readThread, loadThreadHistory, threadObservations, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, promoteThread, selectThread, selectChannel, backToWorkspace, backToChannels, resolveTaskRefs }) {
|
|
7
|
+
export function TeamConversation({ t, useWorkspaces, navigation, drafts, putAttachment, getAttachment, loadChannels, readThread, loadThreadHistory, threadObservations, subscribeChanges, loadMembers, loadInbox, sendMessage, joinChannel, removeChannelMember, reply, changeTask, promoteThread, selectThread, selectChannel, selectWorkspace, backToWorkspace, backToChannels, resolveTaskRefs }) {
|
|
7
8
|
const navigationState = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot, navigation.getSnapshot);
|
|
8
9
|
const workspaces = useWorkspaces(state => state.items);
|
|
9
10
|
const current = workspaces.find(workspace => workspace.workspaceId === navigationState.workspaceId);
|
|
11
|
+
if (navigationState.inbox === true) {
|
|
12
|
+
// The Inbox page is global: it merges every visible Workspace and needs no
|
|
13
|
+
// selected Workspace. Selecting Inbox clears the Channel/Thread faces, so
|
|
14
|
+
// this face owns the seat while the flag stands.
|
|
15
|
+
return _jsx(TeamInboxPage, { useWorkspaces: useWorkspaces, loadInbox: loadInbox, subscribeChanges: subscribeChanges, selectWorkspace: selectWorkspace, selectThread: selectThread, t: t }, "inbox");
|
|
16
|
+
}
|
|
10
17
|
if (current !== undefined && navigationState.threadRef !== undefined) {
|
|
11
18
|
return _jsx(TeamThreadPage, { workspaceId: current.workspaceId, putAttachment: putAttachment, threadRef: navigationState.threadRef, backToWorkspace: backToWorkspace, selectChannel: selectChannel, selectThread: selectThread, resolveTaskRefs: resolveTaskRefs, ...(navigationState.channelRef === undefined ? {} : { channelRef: navigationState.channelRef }), ...(navigationState.taskRef === undefined ? {} : { taskRef: navigationState.taskRef }), ...(navigationState.taskNumber === undefined ? {} : { taskNumber: navigationState.taskNumber }), drafts: drafts, getAttachment: getAttachment, readThread: readThread, loadChannels: loadChannels, loadThreadHistory: loadThreadHistory, threadObservations: threadObservations, subscribeChanges: subscribeChanges, loadMembers: loadMembers, reply: reply, changeTask: changeTask, promoteThread: promoteThread, t: t }, navigationState.threadRef);
|
|
12
19
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TeamConversationProps } from './slots.ts';
|
|
2
|
+
interface TeamInboxPageProps {
|
|
3
|
+
readonly useWorkspaces: TeamConversationProps['useWorkspaces'];
|
|
4
|
+
readonly loadInbox: TeamConversationProps['loadInbox'];
|
|
5
|
+
/** Wake source while the page is open: one scope-less subscription, shared with the badge poll. */
|
|
6
|
+
readonly subscribeChanges: TeamConversationProps['subscribeChanges'];
|
|
7
|
+
readonly selectWorkspace: TeamConversationProps['selectWorkspace'];
|
|
8
|
+
readonly selectThread: TeamConversationProps['selectThread'];
|
|
9
|
+
readonly t: TeamConversationProps['t'];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The Human 「提到我」 queue: one direct-only Inbox call per visible Workspace,
|
|
13
|
+
* merged into a single recency-ordered list — newest mention first across every
|
|
14
|
+
* Workspace, since a queue is read by recency rather than by Workspace.
|
|
15
|
+
* Opening the page never acknowledges anything — only a durable Thread read
|
|
16
|
+
* consumes a mention marker, so rows and the badge drop after the Thread is
|
|
17
|
+
* opened through the existing auto-ack path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function TeamInboxPage({ useWorkspaces, loadInbox, subscribeChanges, selectWorkspace, selectThread, t }: TeamInboxPageProps): import("react").JSX.Element;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=TeamInboxPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TeamInboxPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamInboxPage.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAWvD,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAA;IACtD,mGAAmG;IACnG,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AAgBD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,kBAAkB,+BAiFjI"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import { formatAbsoluteTime, formatInboxTime } from "./team-formatters.js";
|
|
5
|
+
import css from './conversation.module.css';
|
|
6
|
+
import inboxCss from './inbox.module.css';
|
|
7
|
+
/**
|
|
8
|
+
* Queue order: newest mention first, one total order across Workspaces. Each
|
|
9
|
+
* Workspace slice arrives in the Host's own order (that order is the Host's
|
|
10
|
+
* truncation policy); the merged display order is decided here, where the
|
|
11
|
+
* merge happens, and the ledger sequence breaks ties so the queue never
|
|
12
|
+
* reshuffles two rows that share an instant.
|
|
13
|
+
*/
|
|
14
|
+
function compareInboxRows(left, right) {
|
|
15
|
+
const leftAt = Date.parse(left.item.newestOccurredAt);
|
|
16
|
+
const rightAt = Date.parse(right.item.newestOccurredAt);
|
|
17
|
+
const byTime = (Number.isNaN(rightAt) ? 0 : rightAt) - (Number.isNaN(leftAt) ? 0 : leftAt);
|
|
18
|
+
return byTime !== 0 ? byTime : right.item.newestSequence - left.item.newestSequence;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The Human 「提到我」 queue: one direct-only Inbox call per visible Workspace,
|
|
22
|
+
* merged into a single recency-ordered list — newest mention first across every
|
|
23
|
+
* Workspace, since a queue is read by recency rather than by Workspace.
|
|
24
|
+
* Opening the page never acknowledges anything — only a durable Thread read
|
|
25
|
+
* consumes a mention marker, so rows and the badge drop after the Thread is
|
|
26
|
+
* opened through the existing auto-ack path.
|
|
27
|
+
*/
|
|
28
|
+
export function TeamInboxPage({ useWorkspaces, loadInbox, subscribeChanges, selectWorkspace, selectThread, t }) {
|
|
29
|
+
const workspaces = useWorkspaces(state => state.items);
|
|
30
|
+
const [rows, setRows] = useState();
|
|
31
|
+
const [loading, setLoading] = useState(true);
|
|
32
|
+
const [error, setError] = useState();
|
|
33
|
+
// Only the first refresh owns the loading surface; later wakes refresh the
|
|
34
|
+
// rendered rows in place instead of flashing them back to skeleton.
|
|
35
|
+
const loadedRef = useRef(false);
|
|
36
|
+
const refresh = useCallback(async () => {
|
|
37
|
+
if (!loadedRef.current)
|
|
38
|
+
setLoading(true);
|
|
39
|
+
const results = await Promise.all(workspaces.map(async (workspace) => {
|
|
40
|
+
const result = await loadInbox({ workspaceId: workspace.workspaceId, directOnly: true, limit: 100 });
|
|
41
|
+
return result.ok
|
|
42
|
+
? { ok: true, workspaceId: workspace.workspaceId, workspaceTitle: workspace.title, items: result.value.items }
|
|
43
|
+
: { ok: false, message: result.error.message };
|
|
44
|
+
}));
|
|
45
|
+
const failure = results.find(result => !result.ok);
|
|
46
|
+
setRows(results.flatMap(result => result.ok
|
|
47
|
+
? result.items.map(item => ({ workspaceId: result.workspaceId, workspaceTitle: result.workspaceTitle, item }))
|
|
48
|
+
: []).sort(compareInboxRows));
|
|
49
|
+
setError(failure?.ok === false ? failure.message : undefined);
|
|
50
|
+
loadedRef.current = true;
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}, [loadInbox, workspaces]);
|
|
53
|
+
useEffect(() => { void refresh(); }, [refresh]);
|
|
54
|
+
useEffect(() => subscribeChanges(undefined, update => {
|
|
55
|
+
if (update.type === 'failed') {
|
|
56
|
+
setError(update.message);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
void refresh();
|
|
60
|
+
}), [subscribeChanges, refresh]);
|
|
61
|
+
const open = (row) => {
|
|
62
|
+
selectWorkspace(row.workspaceId);
|
|
63
|
+
selectThread(row.item.thread.threadRef, row.item.channelRef, row.item.task?.taskRef, row.item.taskNumber);
|
|
64
|
+
};
|
|
65
|
+
// The page rides the shared conversation seat: the same header band, 880px
|
|
66
|
+
// reading column, responsive gutters, and scrollbar gutter as Channel and
|
|
67
|
+
// Thread, so switching surfaces does not shift the content column.
|
|
68
|
+
return _jsxs("main", { className: css.surface, "data-team-inbox": true, children: [_jsx("div", { className: css.surfaceHeader, children: _jsx("header", { className: css.headerRow, children: _jsxs("div", { className: css.headerCopy, children: [_jsx("h1", { children: t('mentionsOfMe') }), rows !== undefined && rows.length > 0 && _jsx("p", { className: inboxCss.headerMeta, children: t('inboxHeaderMeta', { threads: rows.length, mentions: rows.reduce((sum, row) => sum + row.item.directCount, 0) }) })] }) }) }), _jsx("div", { className: css.timeline, children: _jsxs("div", { className: css.timelineContent, children: [loading && rows === undefined && error === undefined && _jsx("div", { className: css.emptySurface, children: _jsxs("p", { className: css.loadingState, children: [_jsx("span", { className: css.loadingMark, "aria-hidden": "true" }), t('loadingInbox')] }) }), !loading && rows === undefined && error !== undefined && _jsxs("div", { className: css.errorState, role: "alert", children: [_jsx("span", { children: error }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => { void refresh(); }, children: t('retry') })] }), rows !== undefined && (rows.length === 0
|
|
69
|
+
? _jsx("div", { className: css.emptySurface, children: _jsxs("div", { className: css.emptyState, children: [_jsx("strong", { children: t('inboxEmptyTitle') }), _jsx("span", { children: t('inboxEmptyHint') })] }) })
|
|
70
|
+
: _jsx("div", { className: inboxCss.list, children: rows.map(row => (_jsxs("button", { type: "button", className: inboxCss.row, onClick: () => { open(row); }, children: [_jsxs("span", { className: inboxCss.rowCrumb, children: [row.workspaceTitle, " / ", _jsxs("span", { className: inboxCss.rowChannel, children: ["#", row.item.channelName] }), row.item.taskNumber !== undefined && _jsx("span", { className: inboxCss.rowTask, children: t('taskLabel', { number: row.item.taskNumber }) })] }), _jsx("span", { className: inboxCss.rowPreview, children: row.item.previewText }), _jsx("time", { className: inboxCss.rowTime, dateTime: row.item.newestOccurredAt, title: formatAbsoluteTime(row.item.newestOccurredAt), children: formatInboxTime(row.item.newestOccurredAt, t) })] }, `${row.workspaceId} ${row.item.thread.threadRef}`))) })), rows !== undefined && error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error })] }) })] });
|
|
71
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamMemberEditor.d.ts","sourceRoot":"","sources":["../../../src/client/TeamMemberEditor.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,2BAA2B,EAAE,uBAAuB,EAAgC,MAAM,gCAAgC,CAAA;AACxI,OAAO,KAAK,EAAiD,gBAAgB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"TeamMemberEditor.d.ts","sourceRoot":"","sources":["../../../src/client/TeamMemberEditor.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,2BAA2B,EAAE,uBAAuB,EAAgC,MAAM,gCAAgC,CAAA;AACxI,OAAO,KAAK,EAAiD,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAajG;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;IAClF,QAAQ,CAAC,KAAK,EAAE,uBAAuB,GAAG,SAAS,CAAA;IACnD,QAAQ,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,uBAAuB,GAAG,SAAS,KAAK,IAAI,CAAA;IAC7E,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAA;IACnD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CAClC,+BA0GA;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;IAC/F,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAA;IAC5C,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAA;IACnD,QAAQ,CAAC,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CAClC,+BA4DA;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,uBAAuB,GAAG,SAAS,EAAE,KAAK,EAAE,uBAAuB,GAAG,SAAS,GAAG,OAAO,CAIxH"}
|