@wowyuarm/dsh-agent-team 0.1.10 → 0.1.12
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 -4
- package/README.zh.md +17 -4
- package/package.json +53 -47
- package/packages/agent-team/README.md +6 -4
- package/packages/agent-team/README.zh.md +6 -4
- package/packages/agent-team/lib/context-management.js +40 -50
- package/packages/agent-team/lib/context-projection.js +15 -1
- package/packages/agent-team/lib/index.js +244 -154
- package/packages/agent-team/lib/invariant.js +38 -4
- package/packages/agent-team/lib/ledger.js +775 -383
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-runtime.js +47 -9
- package/packages/agent-team/lib/member-time-context.js +20 -5
- package/packages/agent-team/lib/mentions.js +56 -0
- package/packages/agent-team/lib/pressure-policy.js +33 -9
- package/packages/agent-team/lib/session-event-cursor.js +116 -0
- package/packages/agent-team/lib/session-remediation.js +38 -26
- package/packages/agent-team/lib/spec.js +47 -34
- package/packages/agent-team/lib/stored-session-reader.js +138 -0
- package/packages/agent-team/lib/typert.host.js +157 -41
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +129 -32
- package/packages/agent-team/lib/types/context-management.d.ts +7 -0
- 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/index.d.ts +77 -7
- 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 +251 -20
- 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-runtime.d.ts +2 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/mentions.d.ts +34 -0
- package/packages/agent-team/lib/types/mentions.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/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 +23 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -1
- 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/types/entities.d.ts +28 -20
- 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 +87 -4
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +49 -7
- 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 +2357 -1207
- 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 +27 -7
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +18 -0
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +22 -0
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +167 -35
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +23 -27
- 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 +26 -26
- 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 +9 -2
- package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamCountBadge.js +25 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +25 -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 +139 -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/TeamMemberRow.d.ts +43 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberRow.js +27 -0
- package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts +2 -2
- package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMembersAction.js +2 -2
- 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 +11 -4
- 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 +108 -30
- 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 +68 -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 +19 -9
- package/packages/client-agent-team/lib/types/client/locales.d.ts +40 -2
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +40 -2
- 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 +7 -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 +33 -2
- 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 +107 -25
- 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 +43 -1
- 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 +81 -6
- package/packages/tool-agent-team/README.md +2 -2
- package/packages/tool-agent-team/README.zh.md +2 -2
- 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 +44 -32
- 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
- package/packages/agent-team/lib/progress-nudge.js +0 -328
- package/packages/agent-team/lib/types/progress-nudge.d.ts +0 -136
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts +0 -3
- package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/TeamSettings.js +0 -5
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Button, IconArchiveOutline20, IconEditOutline16, IconPlusOutline16, Input, Modal, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
4
|
import { TeamPresenceDot } from "./TeamPresenceDot.js";
|
|
5
|
+
import { TeamMemberRow } from "./TeamMemberRow.js";
|
|
5
6
|
import { MultiMenuField } from "./multi-menu-field.js";
|
|
6
7
|
import { SortableRow, useSidebarRowDrag } from "./sidebar-drag.js";
|
|
7
8
|
import { moveSidebarItem, useSidebarOrder } from "./sidebar-order.js";
|
|
@@ -10,6 +11,7 @@ import { mintRequestId } from "./requests.js";
|
|
|
10
11
|
import { TeamRowMenu } from "./TeamRowMenu.js";
|
|
11
12
|
import { TeamSidebarSection } from "./TeamSidebarSection.js";
|
|
12
13
|
import { useChannelMembership } from "./team-membership.js";
|
|
14
|
+
import { useEditDialogSave } from "./team-dialog-save.js";
|
|
13
15
|
import createCss from './create.module.css';
|
|
14
16
|
import css from './sidebar.module.css';
|
|
15
17
|
export function TeamChannelsPanel(props) {
|
|
@@ -39,8 +41,12 @@ export function TeamChannelsPanel(props) {
|
|
|
39
41
|
};
|
|
40
42
|
const drag = useSidebarRowDrag({ refs: orderedChannelRefs, onCommit: applyMove });
|
|
41
43
|
const sectionOpen = useSidebarSectionOpen(workspaceId, 'channels');
|
|
44
|
+
// Only the first refresh owns the loading surface; later wakes (workspace
|
|
45
|
+
// catalog changes, member creation) refresh the rendered rows in place.
|
|
46
|
+
const loadedRef = useRef(false);
|
|
42
47
|
const refresh = useCallback(async () => {
|
|
43
|
-
|
|
48
|
+
if (!loadedRef.current)
|
|
49
|
+
setLoading(true);
|
|
44
50
|
const [channelResult, memberResult] = await Promise.all([
|
|
45
51
|
loadChannels({ workspaceId, limit: 1 }),
|
|
46
52
|
loadMembers({ workspaceId }),
|
|
@@ -53,6 +59,7 @@ export function TeamChannelsPanel(props) {
|
|
|
53
59
|
.map(status => status.member.memberId));
|
|
54
60
|
setSelected(current => new Set([...current].filter(memberId => selectable.has(memberId))));
|
|
55
61
|
setError(undefined);
|
|
62
|
+
loadedRef.current = true;
|
|
56
63
|
}
|
|
57
64
|
else if (!channelResult.ok) {
|
|
58
65
|
setError(channelResult.error.message);
|
|
@@ -146,7 +153,7 @@ export function TeamChannelsPanel(props) {
|
|
|
146
153
|
return next;
|
|
147
154
|
});
|
|
148
155
|
setPendingCreate(undefined);
|
|
149
|
-
}, triggerEmptyLabel: t('membersPickerEmpty'), formatCount: count => t('membersPickerCount', { count }) }), error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('channels'), open: sectionOpen, onToggle: open => { setSidebarSectionOpen(workspaceId, 'channels', open); }, actions: (_jsx(Tooltip, { label: t('addChannel'), delayMs: 500, children: _jsx("button", { ref: triggerRef, type: "button", className: css.iconButton, "aria-label": t('addChannel'), onClick: () => { setError(undefined); setFormOpen(true); }, children: _jsx(IconPlusOutline16, { size: 14 }) }) })), children: [loading && view === undefined && _jsx("p", { className: css.emptyState, children: t('loadingChannels') }), !loading &&
|
|
156
|
+
}, triggerEmptyLabel: t('membersPickerEmpty'), formatCount: count => t('membersPickerCount', { count }) }), error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('channels'), open: sectionOpen, onToggle: open => { setSidebarSectionOpen(workspaceId, 'channels', open); }, actions: (_jsx(Tooltip, { label: t('addChannel'), delayMs: 500, children: _jsx("button", { ref: triggerRef, type: "button", className: css.iconButton, "aria-label": t('addChannel'), onClick: () => { setError(undefined); setFormOpen(true); }, children: _jsx(IconPlusOutline16, { size: 14 }) }) })), children: [loading && view === undefined && _jsx("p", { className: css.emptyState, children: t('loadingChannels') }), !loading && error === undefined && view !== undefined && view.channels.length === 0 && _jsx("p", { className: css.emptyState, children: t('emptyChannels') }), _jsx("div", { className: css.channelList, children: orderedChannels.map(channel => {
|
|
150
157
|
const joined = membership.get(channel.channelRef) ?? new Set();
|
|
151
158
|
return (_jsx(SortableRow, { drag: drag, orderKey: channel.channelRef, children: _jsx(ChannelRow, { channel: channel, members: members, joinedIds: joined, selected: selectedChannelRef === channel.channelRef, updateChannel: updateChannel, archiveChannel: props.archiveChannel, joinChannel: props.joinChannel, removeChannelMember: props.removeChannelMember, onCommitted: () => { void refresh(); }, selectChannel: selectChannel, t: t }) }, channel.channelRef));
|
|
152
159
|
}) })] }), !formOpen && error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error })] }));
|
|
@@ -193,9 +200,11 @@ function ChannelRow({ channel, members, joinedIds, selected, updateChannel, arch
|
|
|
193
200
|
function ChannelEditorDialog({ channel, members, joinedIds, updateChannel, joinChannel, removeChannelMember, onCommitted, onClose, t }) {
|
|
194
201
|
const [name, setName] = useState(channel.name);
|
|
195
202
|
const [description, setDescription] = useState(channel.description);
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
const { saving, error, pendingRequest, save } = useEditDialogSave({
|
|
204
|
+
save: updateChannel,
|
|
205
|
+
onCommitted,
|
|
206
|
+
onClose,
|
|
207
|
+
});
|
|
199
208
|
const membership = useChannelMembership({ joinChannel, removeChannelMember }, change => change.memberId, async () => { await onCommitted(); });
|
|
200
209
|
const dirty = name.trim() !== channel.name || description.trim() !== channel.description;
|
|
201
210
|
const submit = async (event) => {
|
|
@@ -213,32 +222,19 @@ function ChannelEditorDialog({ channel, members, joinedIds, updateChannel, joinC
|
|
|
213
222
|
name: normalizedName,
|
|
214
223
|
description: normalizedDescription,
|
|
215
224
|
};
|
|
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
|
-
}
|
|
225
|
+
await save(request);
|
|
236
226
|
};
|
|
237
227
|
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
228
|
const joined = joinedIds.has(status.member.memberId);
|
|
239
229
|
const rowPending = membership.pending.has(status.member.memberId);
|
|
240
|
-
|
|
230
|
+
// Same membership law as the Channel page roster: joining needs an
|
|
231
|
+
// active Member, leaving only needs the fact.
|
|
232
|
+
const disabled = rowPending || (!joined && status.availability !== 'active');
|
|
241
233
|
const rowError = membership.errors.get(status.member.memberId);
|
|
242
|
-
return
|
|
234
|
+
return _jsx(TeamMemberRow, { status: status, action: {
|
|
235
|
+
label: rowPending ? t('membershipUpdating') : joined ? t('removeFromChannel') : t('addToChannel'),
|
|
236
|
+
disabled,
|
|
237
|
+
onSelect: () => { void membership.change({ workspaceId: channel.workspaceId, channelRef: channel.channelRef, memberId: status.member.memberId, joined }); },
|
|
238
|
+
}, ...(rowError === undefined ? {} : { error: rowError }), t: t }, status.member.memberId);
|
|
243
239
|
})] }), error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }));
|
|
244
240
|
}
|
|
@@ -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;AAqD/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,+BAiUA"}
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
+
import { allMentionMembers, containsAllMention, containsMention, mentionedMemberIds } from "./team-formatters.js";
|
|
5
6
|
import css from './composer.module.css';
|
|
6
7
|
import { formatByteSize } from "./attachment-preview.js";
|
|
7
8
|
function findMention(draft, caret) {
|
|
@@ -18,12 +19,6 @@ function findMention(draft, caret) {
|
|
|
18
19
|
return undefined;
|
|
19
20
|
return { start: at, end: caret, query: beforeCaret.slice(at + 1) };
|
|
20
21
|
}
|
|
21
|
-
function escapeRegExp(value) {
|
|
22
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
23
|
-
}
|
|
24
|
-
function containsMention(draft, handle) {
|
|
25
|
-
return new RegExp(`(?:^|[^\\p{L}\\p{N}_])@${escapeRegExp(handle)}(?=$|[^\\p{L}\\p{N}_])`, 'u').test(draft);
|
|
26
|
-
}
|
|
27
22
|
function mentionCandidates(members, query) {
|
|
28
23
|
const normalized = query.toLocaleLowerCase();
|
|
29
24
|
return members.filter(status => status.presence !== 'unavailable'
|
|
@@ -37,13 +32,6 @@ function rankMentionCandidates(candidates, followers) {
|
|
|
37
32
|
return candidates;
|
|
38
33
|
return [...candidates].sort((left, right) => Number(followers.has(right.member.memberId)) - Number(followers.has(left.member.memberId)));
|
|
39
34
|
}
|
|
40
|
-
/** Every member a manual handle pick could reach: the @all expansion snapshot. */
|
|
41
|
-
function allMentionMembers(members) {
|
|
42
|
-
return members.filter(status => status.presence !== 'unavailable' && status.member.state !== 'inactive' && status.member.state !== 'archived');
|
|
43
|
-
}
|
|
44
|
-
function containsAllMention(draft) {
|
|
45
|
-
return /(?:^|[^\p{L}\p{N}_])@all(?=$|[^\p{L}\p{N}_])/u.test(draft);
|
|
46
|
-
}
|
|
47
35
|
/** One object URL per draft file; revoked when the draft is removed. */
|
|
48
36
|
const draftPreviewUrls = new WeakMap();
|
|
49
37
|
function draftPreviewUrl(file) {
|
|
@@ -56,13 +44,17 @@ function draftPreviewUrl(file) {
|
|
|
56
44
|
}
|
|
57
45
|
return url;
|
|
58
46
|
}
|
|
59
|
-
export function TeamComposer({ members, followerMemberIds,
|
|
47
|
+
export function TeamComposer({ members, followerMemberIds, drafts, draftKey, pending, confirmation, error, onEdit, onSubmit, placeholder, pendingFiles, onFilesChange, asTask, onAsTaskChange, t }) {
|
|
60
48
|
const inputRef = useRef(null);
|
|
61
49
|
const fileInputRef = useRef(null);
|
|
62
50
|
const rootRef = useRef(null);
|
|
63
51
|
const menuRef = useRef(null);
|
|
64
52
|
const activeOptionRef = useRef(null);
|
|
65
53
|
const composingRef = useRef(false);
|
|
54
|
+
// The draft lives in the injected cache so a view switch or refresh does not
|
|
55
|
+
// cost the half-written message. Subscribing here — not in the page that
|
|
56
|
+
// hosts this composer — is what keeps a keystroke off the timeline.
|
|
57
|
+
const { draft, recipients } = useSyncExternalStore(drafts.subscribe, () => drafts.getSnapshot(draftKey));
|
|
66
58
|
const [mention, setMention] = useState();
|
|
67
59
|
const [highlight, setHighlight] = useState(0);
|
|
68
60
|
// @all is a composer-layer expansion of the same eligibility filter a
|
|
@@ -117,18 +109,20 @@ export function TeamComposer({ members, followerMemberIds, recipients, draft, pe
|
|
|
117
109
|
return;
|
|
118
110
|
inputRef.current?.focus({ preventScroll: true });
|
|
119
111
|
}, [confirmation, pending]);
|
|
112
|
+
// Handle lookup for both directions of the same judgement: whether a picked
|
|
113
|
+
// recipient is still spelled in the text, and which Members the text names.
|
|
114
|
+
const memberHandles = new Map(members.map(status => [status.member.memberId, status.member.handle]));
|
|
120
115
|
const pruneRecipients = (nextDraft) => {
|
|
121
116
|
// An @all marker stands for its expansion snapshot: the member handles it
|
|
122
117
|
// stands for are not in the text, so text-based pruning must stand down.
|
|
123
118
|
if (containsAllMention(nextDraft))
|
|
124
119
|
return;
|
|
125
|
-
const knownMembers = new Map(members.map(status => [status.member.memberId, status.member]));
|
|
126
120
|
const next = new Set([...recipients].filter(memberId => {
|
|
127
|
-
const
|
|
128
|
-
return
|
|
121
|
+
const handle = memberHandles.get(memberId);
|
|
122
|
+
return handle !== undefined && containsMention(nextDraft, handle);
|
|
129
123
|
}));
|
|
130
124
|
if (next.size !== recipients.size)
|
|
131
|
-
|
|
125
|
+
drafts.writeRecipients(draftKey, next);
|
|
132
126
|
};
|
|
133
127
|
// Restored drafts may carry recipients that no longer match the text (or
|
|
134
128
|
// unknown Members); converge on mount and on every state change so the
|
|
@@ -139,6 +133,10 @@ export function TeamComposer({ members, followerMemberIds, recipients, draft, pe
|
|
|
139
133
|
return;
|
|
140
134
|
pruneRecipients(draft);
|
|
141
135
|
}, [draft, recipients, members]);
|
|
136
|
+
// The notify row reports what the Host will resolve from this draft, not just
|
|
137
|
+
// what the mention menu picked: an authored `@Handle` delivers exactly like a
|
|
138
|
+
// pick, and `@all` stands for the menu's expansion.
|
|
139
|
+
const notifiedIds = [...new Set([...recipients, ...mentionedMemberIds(draft, members)])].sort();
|
|
142
140
|
const updateMention = (nextDraft, caret) => {
|
|
143
141
|
const match = findMention(nextDraft, caret);
|
|
144
142
|
setMention(match);
|
|
@@ -147,15 +145,17 @@ export function TeamComposer({ members, followerMemberIds, recipients, draft, pe
|
|
|
147
145
|
};
|
|
148
146
|
const onChange = (event) => {
|
|
149
147
|
const nextDraft = event.target.value;
|
|
150
|
-
|
|
148
|
+
drafts.writeDraft(draftKey, nextDraft);
|
|
149
|
+
onEdit?.();
|
|
151
150
|
pruneRecipients(nextDraft);
|
|
152
151
|
updateMention(nextDraft, event.target.selectionStart ?? nextDraft.length);
|
|
153
152
|
};
|
|
154
153
|
// Both pick kinds share the same commit path: swap the text in, add the
|
|
155
154
|
// recipients, close the menu, and restore the caret after focus.
|
|
156
155
|
const commitMention = (nextDraft, nextCaret, nextRecipients) => {
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
drafts.writeDraft(draftKey, nextDraft);
|
|
157
|
+
drafts.writeRecipients(draftKey, nextRecipients);
|
|
158
|
+
onEdit?.();
|
|
159
159
|
setMention(undefined);
|
|
160
160
|
setHighlight(0);
|
|
161
161
|
requestAnimationFrame(() => {
|
|
@@ -246,7 +246,7 @@ export function TeamComposer({ members, followerMemberIds, recipients, draft, pe
|
|
|
246
246
|
return option.kind === 'all'
|
|
247
247
|
? _jsxs("button", { id: optionId, type: "button", role: "option", "aria-selected": selected, className: css.mentionOption, ref: selected ? activeOptionRef : undefined, onMouseDown: event => { event.preventDefault(); }, onClick: () => { selectOption(option); }, children: [_jsx("span", { className: css.mentionAllDot, "aria-hidden": "true" }), _jsx("span", { className: css.mentionName, children: "@all" }), _jsx("span", { className: css.mentionDescription, children: t('mentionAll', { count: allCount }) })] }, "all")
|
|
248
248
|
: _jsxs("button", { id: optionId, type: "button", role: "option", "aria-selected": selected, className: css.mentionOption, ref: selected ? activeOptionRef : undefined, onMouseDown: event => { event.preventDefault(); }, onClick: () => { selectOption(option); }, children: [_jsx(TeamPresenceDot, { status: option.status, t: t }), _jsxs("span", { className: css.mentionName, children: ["@", option.status.member.handle] }), _jsx("span", { className: css.mentionDescription, children: option.status.member.description })] }, option.status.member.memberId);
|
|
249
|
-
}) }), _jsx("textarea", { ref: inputRef, "aria-label": t('messageDraft'), "aria-autocomplete": "list", "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen && activeOption !== undefined ? activeOption.kind === 'all' ? `${listId}-all` : `${listId}-${activeOption.status.member.memberId}` : undefined, "aria-expanded": menuOpen, value: draft, readOnly: pending, placeholder: placeholder ?? t('messagePlaceholder'), rows: 1, onChange: onChange, onPaste: onPaste, onKeyDown: onKeyDown, onSelect: event => { updateMention(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length); }, onCompositionStart: () => { composingRef.current = true; }, onCompositionEnd: () => { setTimeout(() => { composingRef.current = false; }, 10); } })] }),
|
|
249
|
+
}) }), _jsx("textarea", { ref: inputRef, "aria-label": t('messageDraft'), "aria-autocomplete": "list", "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen && activeOption !== undefined ? activeOption.kind === 'all' ? `${listId}-all` : `${listId}-${activeOption.status.member.memberId}` : undefined, "aria-expanded": menuOpen, value: draft, readOnly: pending, placeholder: placeholder ?? t('messagePlaceholder'), rows: 1, onChange: onChange, onPaste: onPaste, onKeyDown: onKeyDown, onSelect: event => { updateMention(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length); }, onCompositionStart: () => { composingRef.current = true; }, onCompositionEnd: () => { setTimeout(() => { composingRef.current = false; }, 10); } })] }), notifiedIds.length > 0 && _jsx("p", { className: css.notifyRow, "data-team-notify": true, children: t('composerNotify', { ids: notifiedIds.map(memberId => `@${memberHandles.get(memberId) ?? memberId}`).join(', ') }) }), onFilesChange !== undefined && pendingFiles !== undefined && pendingFiles.length > 0 && (_jsx("ul", { className: css.fileChips, "aria-label": t('attachFiles'), children: pendingFiles.map((file, index) => {
|
|
250
250
|
const previewUrl = draftPreviewUrl(file);
|
|
251
251
|
return _jsxs("li", { className: `${css.fileChip} ${previewUrl !== undefined ? css.imageChip : ''}`, children: [previewUrl !== undefined && _jsx("img", { src: previewUrl, alt: "", className: css.imageChipPreview }), _jsxs("span", { className: css.fileChipName, title: file.name, children: [file.name, _jsx("span", { className: css.fileChipSize, children: formatByteSize(file.size) })] }), _jsx("button", { type: "button", className: css.fileChipRemove, "aria-label": t('removeFile', { name: file.name }), disabled: pending, onClick: () => {
|
|
252
252
|
const url = draftPreviewUrls.get(file);
|
|
@@ -254,12 +254,12 @@ export function TeamComposer({ members, followerMemberIds, recipients, draft, pe
|
|
|
254
254
|
URL.revokeObjectURL(url);
|
|
255
255
|
onFilesChange(pendingFiles.filter((_, candidate) => candidate !== index));
|
|
256
256
|
}, children: "\u00D7" })] }, `${file.name}-${index}`);
|
|
257
|
-
}) })), _jsxs("div", { className: css.toolbar, children: [
|
|
257
|
+
}) })), _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
258
|
const chosen = [...event.target.files ?? []];
|
|
259
259
|
if (chosen.length > 0 && pendingFiles !== undefined)
|
|
260
260
|
onFilesChange([...pendingFiles, ...chosen]);
|
|
261
261
|
event.target.value = '';
|
|
262
|
-
} }), _jsx("button", { type: "button", className: css.attachButton, "aria-label": t('attachFiles'), title: t('
|
|
262
|
+
} }), _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
263
|
// Keep the composer focused when the send control is clicked.
|
|
264
264
|
event.preventDefault();
|
|
265
265
|
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,17 +1,24 @@
|
|
|
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
|
}
|
|
13
20
|
if (current !== undefined && navigationState.channelRef !== undefined) {
|
|
14
|
-
return _jsx(TeamChannelPage, { workspaceId: current.workspaceId, channelRef: navigationState.channelRef, drafts: drafts, putAttachment: putAttachment, getAttachment: getAttachment, loadChannels: loadChannels, subscribeChanges: subscribeChanges, loadMembers: loadMembers, sendMessage: sendMessage, joinChannel: joinChannel, removeChannelMember: removeChannelMember, selectThread: selectThread, selectChannel: selectChannel, backToChannels: backToChannels, resolveTaskRefs: resolveTaskRefs, t: t }, navigationState.channelRef);
|
|
21
|
+
return _jsx(TeamChannelPage, { workspaceId: current.workspaceId, channelRef: navigationState.channelRef, drafts: drafts, putAttachment: putAttachment, getAttachment: getAttachment, loadChannels: loadChannels, subscribeChanges: subscribeChanges, loadMembers: loadMembers, loadInbox: loadInbox, sendMessage: sendMessage, joinChannel: joinChannel, removeChannelMember: removeChannelMember, selectThread: selectThread, selectChannel: selectChannel, backToChannels: backToChannels, resolveTaskRefs: resolveTaskRefs, t: t }, navigationState.channelRef);
|
|
15
22
|
}
|
|
16
23
|
const welcome = current === undefined
|
|
17
24
|
? { eyebrow: t('teamMode'), title: t('team'), body: t('empty') }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one count capsule. The Human Inbox entry, the Channel feed's Thread
|
|
3
|
+
* entry, and the Inbox queue row all answer the same question — how much is
|
|
4
|
+
* waiting here — so they wear the same box rather than one hand-written copy
|
|
5
|
+
* of the same declarations per surface. Three copies is how the feed's digit
|
|
6
|
+
* ended up on a different line box from the other two.
|
|
7
|
+
*
|
|
8
|
+
* Zero is the absence of a count rather than a capsule reading zero, which is
|
|
9
|
+
* the rule every caller wants and therefore the component's own.
|
|
10
|
+
*/
|
|
11
|
+
export declare function TeamCountBadge({ count, tone, label, className }: {
|
|
12
|
+
readonly count: number;
|
|
13
|
+
/** `hairline` is unread that merely arrived; the solid fill is unread that names this reader. */
|
|
14
|
+
readonly tone?: 'solid' | 'hairline' | undefined;
|
|
15
|
+
/** The accessible name while the count is the only thing saying it; omit it when the surrounding control already carries the number. */
|
|
16
|
+
readonly label?: string | undefined;
|
|
17
|
+
/** Placement belongs to the surface hanging the capsule: the narrow rail pins it to the icon's corner. */
|
|
18
|
+
readonly className?: string | undefined;
|
|
19
|
+
}): import("react").JSX.Element | null;
|
|
20
|
+
//# sourceMappingURL=TeamCountBadge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TeamCountBadge.d.ts","sourceRoot":"","sources":["../../../src/client/TeamCountBadge.tsx"],"names":[],"mappings":"AAKA;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,IAAc,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iGAAiG;IACjG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;IAChD,wIAAwI;IACxI,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,0GAA0G;IAC1G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC,sCASA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import css from './countBadge.module.css';
|
|
3
|
+
/** Every count the Human reads is capped at this, so one wide number never widens the capsule. */
|
|
4
|
+
const COUNT_CAP = 99;
|
|
5
|
+
/**
|
|
6
|
+
* The one count capsule. The Human Inbox entry, the Channel feed's Thread
|
|
7
|
+
* entry, and the Inbox queue row all answer the same question — how much is
|
|
8
|
+
* waiting here — so they wear the same box rather than one hand-written copy
|
|
9
|
+
* of the same declarations per surface. Three copies is how the feed's digit
|
|
10
|
+
* ended up on a different line box from the other two.
|
|
11
|
+
*
|
|
12
|
+
* Zero is the absence of a count rather than a capsule reading zero, which is
|
|
13
|
+
* the rule every caller wants and therefore the component's own.
|
|
14
|
+
*/
|
|
15
|
+
export function TeamCountBadge({ count, tone = 'solid', label, className }) {
|
|
16
|
+
if (count <= 0)
|
|
17
|
+
return null;
|
|
18
|
+
const text = count > COUNT_CAP ? `${COUNT_CAP}+` : String(count);
|
|
19
|
+
const classes = `${css.badge}${tone === 'hairline' ? ` ${css.hairline}` : ''}${className === undefined ? '' : ` ${className}`}`;
|
|
20
|
+
// Tests and the browser journey read the capsule by this attribute rather
|
|
21
|
+
// than by a CSS-module hash, which changes whenever a rule is renamed.
|
|
22
|
+
return label === undefined
|
|
23
|
+
? _jsx("span", { className: classes, "data-team-count-badge": tone, "aria-hidden": "true", children: text })
|
|
24
|
+
: _jsx("span", { className: classes, "data-team-count-badge": tone, role: "img", "aria-label": label, title: label, children: text });
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 Inbox: one Inbox call per visible Workspace, rendering the Host's
|
|
13
|
+
* two slices — the unread queue (「需要我」, mentions counted inside it rather
|
|
14
|
+
* than alone) and the 「最近活跃」 tail of Threads the reader took part in.
|
|
15
|
+
* Both merge across every Workspace into one recency-ordered list, newest
|
|
16
|
+
* first, since a queue is read by recency rather than by Workspace.
|
|
17
|
+
* Opening the page never acknowledges anything — only a durable Thread read
|
|
18
|
+
* advances the watermark and consumes a mention marker, so rows and the badge
|
|
19
|
+
* drop after the Thread is opened through the existing auto-ack path. A Thread
|
|
20
|
+
* holding unread is only ever in the queue: the Host already excludes it from
|
|
21
|
+
* the tail, and this page never re-derives that judgement.
|
|
22
|
+
*/
|
|
23
|
+
export declare function TeamInboxPage({ useWorkspaces, loadInbox, subscribeChanges, selectWorkspace, selectThread, t }: TeamInboxPageProps): import("react").JSX.Element;
|
|
24
|
+
export {};
|
|
25
|
+
//# 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;AAavD,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;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,kBAAkB,+BA0GjI"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { TeamAvatarStack } from "./TeamAvatarStack.js";
|
|
6
|
+
import { TeamCountBadge } from "./TeamCountBadge.js";
|
|
7
|
+
import css from './conversation.module.css';
|
|
8
|
+
import inboxCss from './inbox.module.css';
|
|
9
|
+
/**
|
|
10
|
+
* How many 「最近活跃」 rows the merged page may show. The Host bounds each
|
|
11
|
+
* Workspace's own slice; this is the single bound across every Workspace on
|
|
12
|
+
* screen, so the section stays the same size no matter how many are open — and
|
|
13
|
+
* it stays a way back into work rather than a second queue, which is why it is
|
|
14
|
+
* shorter than the queue the reader is actually being asked to work through.
|
|
15
|
+
*/
|
|
16
|
+
const RECENT_ROWS_LIMIT = 5;
|
|
17
|
+
/**
|
|
18
|
+
* Queue order: newest unread first, one total order across Workspaces. Each
|
|
19
|
+
* Workspace slice arrives in the Host's own order (that order is the Host's
|
|
20
|
+
* truncation policy); the merged display order is decided here, where the
|
|
21
|
+
* merge happens, and the ledger sequence breaks ties so the queue never
|
|
22
|
+
* reshuffles two rows that share an instant.
|
|
23
|
+
*/
|
|
24
|
+
function compareInboxRows(left, right) {
|
|
25
|
+
const leftAt = Date.parse(left.item.newestOccurredAt);
|
|
26
|
+
const rightAt = Date.parse(right.item.newestOccurredAt);
|
|
27
|
+
const byTime = (Number.isNaN(rightAt) ? 0 : rightAt) - (Number.isNaN(leftAt) ? 0 : leftAt);
|
|
28
|
+
return byTime !== 0 ? byTime : right.item.newestSequence - left.item.newestSequence;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The Human Inbox: one Inbox call per visible Workspace, rendering the Host's
|
|
32
|
+
* two slices — the unread queue (「需要我」, mentions counted inside it rather
|
|
33
|
+
* than alone) and the 「最近活跃」 tail of Threads the reader took part in.
|
|
34
|
+
* Both merge across every Workspace into one recency-ordered list, newest
|
|
35
|
+
* first, since a queue is read by recency rather than by Workspace.
|
|
36
|
+
* Opening the page never acknowledges anything — only a durable Thread read
|
|
37
|
+
* advances the watermark and consumes a mention marker, so rows and the badge
|
|
38
|
+
* drop after the Thread is opened through the existing auto-ack path. A Thread
|
|
39
|
+
* holding unread is only ever in the queue: the Host already excludes it from
|
|
40
|
+
* the tail, and this page never re-derives that judgement.
|
|
41
|
+
*/
|
|
42
|
+
export function TeamInboxPage({ useWorkspaces, loadInbox, subscribeChanges, selectWorkspace, selectThread, t }) {
|
|
43
|
+
const workspaces = useWorkspaces(state => state.items);
|
|
44
|
+
const [rows, setRows] = useState();
|
|
45
|
+
const [recentRows, setRecentRows] = useState([]);
|
|
46
|
+
const [loading, setLoading] = useState(true);
|
|
47
|
+
const [error, setError] = useState();
|
|
48
|
+
// Only the first refresh owns the loading surface; later wakes refresh the
|
|
49
|
+
// rendered rows in place instead of flashing them back to skeleton.
|
|
50
|
+
const loadedRef = useRef(false);
|
|
51
|
+
const refresh = useCallback(async () => {
|
|
52
|
+
if (!loadedRef.current)
|
|
53
|
+
setLoading(true);
|
|
54
|
+
const results = await Promise.all(workspaces.map(async (workspace) => {
|
|
55
|
+
const result = await loadInbox({ workspaceId: workspace.workspaceId, limit: 100 });
|
|
56
|
+
return result.ok
|
|
57
|
+
? { ok: true, workspaceId: workspace.workspaceId, workspaceTitle: workspace.title, items: result.value.items, recent: result.value.recent }
|
|
58
|
+
: { ok: false, message: result.error.message };
|
|
59
|
+
}));
|
|
60
|
+
const failure = results.find(result => !result.ok);
|
|
61
|
+
const asRows = (items, workspaceId, workspaceTitle) => items.map(item => ({ workspaceId, workspaceTitle, item }));
|
|
62
|
+
setRows(results.flatMap(result => result.ok ? asRows(result.items, result.workspaceId, result.workspaceTitle) : []).sort(compareInboxRows));
|
|
63
|
+
// The tail is one global bound rather than one per Workspace: the reader was
|
|
64
|
+
// promised ten Threads to step back into, and every Workspace's slice is
|
|
65
|
+
// already capped on its own, so the merged list is trimmed here.
|
|
66
|
+
setRecentRows(results.flatMap(result => result.ok ? asRows(result.recent, result.workspaceId, result.workspaceTitle) : [])
|
|
67
|
+
.sort(compareInboxRows).slice(0, RECENT_ROWS_LIMIT));
|
|
68
|
+
setError(failure?.ok === false ? failure.message : undefined);
|
|
69
|
+
loadedRef.current = true;
|
|
70
|
+
setLoading(false);
|
|
71
|
+
}, [loadInbox, workspaces]);
|
|
72
|
+
useEffect(() => { void refresh(); }, [refresh]);
|
|
73
|
+
useEffect(() => subscribeChanges(undefined, update => {
|
|
74
|
+
if (update.type === 'failed') {
|
|
75
|
+
setError(update.message);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
void refresh();
|
|
79
|
+
}), [subscribeChanges, refresh]);
|
|
80
|
+
const open = (row) => {
|
|
81
|
+
selectWorkspace(row.workspaceId);
|
|
82
|
+
selectThread(row.item.thread.threadRef, row.item.channelRef, row.item.task?.taskRef, row.item.taskNumber);
|
|
83
|
+
};
|
|
84
|
+
// The page rides the shared conversation seat: the same header band, 880px
|
|
85
|
+
// reading column, responsive gutters, and scrollbar gutter as Channel and
|
|
86
|
+
// Thread, so switching surfaces does not shift the content column.
|
|
87
|
+
const totalUnread = rows?.reduce((sum, row) => sum + row.item.unreadCount, 0) ?? 0;
|
|
88
|
+
const totalMentions = rows?.reduce((sum, row) => sum + row.item.directCount, 0) ?? 0;
|
|
89
|
+
// A row names its Workspace only while the rows on screen span more than one.
|
|
90
|
+
// With a single Workspace in the list the segment is a constant printed down
|
|
91
|
+
// every row, which spends the row's most readable position on something that
|
|
92
|
+
// never varies; the moment a second Workspace reaches the list it comes back,
|
|
93
|
+
// because two rows then have to be tellable apart. This reads the rows rather
|
|
94
|
+
// than the open Workspaces: a Workspace holding nothing to show here is not a
|
|
95
|
+
// reason to keep printing the name of the one that is.
|
|
96
|
+
const shownWorkspaces = new Set();
|
|
97
|
+
for (const row of rows ?? [])
|
|
98
|
+
shownWorkspaces.add(row.workspaceTitle);
|
|
99
|
+
for (const row of recentRows)
|
|
100
|
+
shownWorkspaces.add(row.workspaceTitle);
|
|
101
|
+
const showWorkspace = shownWorkspaces.size > 1;
|
|
102
|
+
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('inboxTitle') }), rows !== undefined && rows.length > 0 && _jsxs("p", { className: inboxCss.headerMeta, children: [_jsx("span", { children: t('inboxHeaderThreads', { count: rows.length }) }), _jsx("span", { className: inboxCss.headerUnread, children: t('inboxHeaderUnread', { count: totalUnread }) }), totalMentions > 0 && _jsx("span", { children: t('inboxHeaderMentions', { count: totalMentions }) })] })] }) }) }), _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 && recentRows.length === 0
|
|
103
|
+
? _jsx("div", { className: css.emptySurface, children: _jsxs("div", { className: css.emptyState, children: [_jsx("strong", { children: t('inboxEmptyTitle') }), _jsx("span", { children: t('inboxEmptyHint') })] }) })
|
|
104
|
+
: _jsxs(_Fragment, { children: [rows.length > 0 && _jsxs("section", { className: inboxCss.section, children: [_jsxs("h2", { className: inboxCss.sectionTitle, children: [t('inboxSectionNeedsMe'), _jsx("span", { className: inboxCss.sectionCount, children: rows.length })] }), _jsx("div", { className: inboxCss.list, children: rows.map(row => _jsx(InboxQueueRow, { row: row, t: t, showWorkspace: showWorkspace, onOpen: () => { open(row); } }, `${row.workspaceId} ${row.item.thread.threadRef}`)) })] }), recentRows.length > 0 && _jsxs("section", { className: inboxCss.section, children: [_jsxs("h2", { className: inboxCss.sectionTitle, children: [t('inboxSectionRecent'), _jsx("span", { className: inboxCss.sectionCount, children: recentRows.length })] }), _jsx("div", { className: inboxCss.list, children: recentRows.map(row => _jsx(InboxQueueRow, { row: row, t: t, showWorkspace: showWorkspace, onOpen: () => { open(row); } }, `${row.workspaceId} ${row.item.thread.threadRef}`)) })] })] })), rows !== undefined && error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error })] }) })] });
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* One queue row, shaped like the shipped two-line result row: who is on this
|
|
108
|
+
* Thread leads in the gutter, the identity line answers which Thread this is,
|
|
109
|
+
* how much is waiting, and when it last moved, and the gist sits under it on the
|
|
110
|
+
* same column as evidence for that identity rather than as the row's subject.
|
|
111
|
+
*
|
|
112
|
+
* The gutter carries the one thing every row has — who is on the work, or who
|
|
113
|
+
* moved a Thread nobody has claimed — so a Thread that merely arrived and one
|
|
114
|
+
* that named the reader open on the same edge instead of the quieter one opening
|
|
115
|
+
* on a slot reserved for a count it does not hold. It is the grammar the Channel
|
|
116
|
+
* feed's Thread entry row already speaks, where the people on the work lead the
|
|
117
|
+
* row.
|
|
118
|
+
*
|
|
119
|
+
* Before the queue admitted every unread Thread, each row was a mention and the
|
|
120
|
+
* rows were interchangeable; now that named and ambient unread share one list,
|
|
121
|
+
* the row has to carry that difference itself. The count closes the identity
|
|
122
|
+
* line and only its ink changes — the shared capsule fill for a row that names
|
|
123
|
+
* the reader, a hairline for one that merely moved. The two numbers behind that
|
|
124
|
+
* ink (unread, mentions) reach assistive tech through the capsule's own name,
|
|
125
|
+
* because a second visible count beside the first would cost the row the one
|
|
126
|
+
* thing it needs to stay scannable.
|
|
127
|
+
*/
|
|
128
|
+
function InboxQueueRow({ row, t, showWorkspace, onOpen }) {
|
|
129
|
+
const { item } = row;
|
|
130
|
+
const actor = item.newestActor;
|
|
131
|
+
const owners = item.claimOwners;
|
|
132
|
+
const named = item.directCount > 0;
|
|
133
|
+
const countLabel = named
|
|
134
|
+
? t('inboxRowUnreadMentions', { count: item.unreadCount, mentions: item.directCount })
|
|
135
|
+
: t('inboxRowUnread', { count: item.unreadCount });
|
|
136
|
+
return _jsxs("button", { type: "button", className: inboxCss.row, "data-named": named || undefined, onClick: onOpen, children: [_jsx("span", { className: inboxCss.rowActor, children: owners.length > 0
|
|
137
|
+
? _jsx(TeamAvatarStack, { owners: owners, label: t('claimers', { names: owners.map(owner => `@${owner.name}`).join(', ') }) })
|
|
138
|
+
: _jsx(TeamAvatarStack, { owners: [actor], label: t('inboxRowActor', { name: `@${actor.name}` }) }) }), _jsxs("span", { className: inboxCss.rowLine, children: [_jsxs("span", { className: inboxCss.rowCrumb, children: [showWorkspace && _jsx("span", { className: inboxCss.rowWorkspace, children: row.workspaceTitle }), showWorkspace && ' / ', _jsxs("span", { className: inboxCss.rowChannel, children: ["#", item.channelName] }), ' ', item.taskNumber !== undefined && _jsx("span", { className: inboxCss.rowTask, children: t('taskLabel', { number: item.taskNumber }) })] }), _jsx(TeamCountBadge, { count: item.unreadCount, tone: named ? 'solid' : 'hairline', label: countLabel }), _jsx("time", { className: inboxCss.rowTime, dateTime: item.newestOccurredAt, title: formatAbsoluteTime(item.newestOccurredAt), children: formatInboxTime(item.newestOccurredAt, t) })] }), _jsx("span", { className: inboxCss.rowPreview, children: item.previewText })] });
|
|
139
|
+
}
|
|
@@ -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"}
|