@wowyuarm/dsh-agent-team 0.1.0 → 0.1.2
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 +14 -2
- package/README.zh.md +10 -2
- package/package.json +32 -24
- package/packages/agent-team/lib/attachments.js +157 -0
- package/packages/agent-team/lib/index.js +280 -22
- package/packages/agent-team/lib/ledger.js +98 -22
- package/packages/agent-team/lib/recovery.js +87 -0
- package/packages/agent-team/lib/spec.js +27 -5
- package/packages/agent-team/lib/typert.host.js +313 -26
- package/packages/agent-team/lib/typert.remote-client.d.ts +9 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +234 -17
- package/packages/agent-team/lib/types/attachments.d.ts +69 -0
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +68 -6
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +17 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +42 -0
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +94 -3
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +2 -2
- package/packages/client-agent-team/README.md +4 -3
- package/packages/client-agent-team/README.zh.md +4 -3
- package/packages/client-agent-team/lib/client.js +1791 -322
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +5 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +108 -59
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +7 -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 +80 -19
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +38 -19
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +61 -4
- 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 +3 -3
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +12 -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 +152 -14
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +7 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +93 -16
- 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 +6 -5
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +50 -10
- package/packages/client-agent-team/lib/types/client/locales.d.ts +34 -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 +34 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +35 -0
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/sidebar-drag.js +70 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +27 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.js +170 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +18 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/task-refs.js +58 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +15 -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 +30 -2
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
- package/packages/tool-agent-team/lib/index.js +10 -6
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Button, DisclosureRow, IconChevronLeftOutline14, IconChecklistOutline14, Pill } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { Button, DisclosureRow, IconChevronLeftOutline14, IconChecklistOutline14, Modal, Pill } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
4
|
import { TeamComposer } from "./TeamComposer.js";
|
|
5
5
|
import { TeamPresenceDot } from "./TeamPresenceDot.js";
|
|
6
6
|
import { TeamMessage } from "./TeamMessage.js";
|
|
7
|
+
import { TeamRunDivider } from "./TeamRunDivider.js";
|
|
7
8
|
import { formatActivity, formatClaimState, formatTaskStatus, formatTaskTitle, mentionNamesOf } from "./team-formatters.js";
|
|
8
|
-
import { daySeparatorLabel, timelineDayKey } from "./team-separators.js";
|
|
9
|
+
import { daySeparatorLabel, isRunGap, timelineDayKey } from "./team-separators.js";
|
|
9
10
|
import { useTimelineScroll } from "./timeline-scroll.js";
|
|
11
|
+
import { rememberResolvedTaskRef } from "./task-refs.js";
|
|
12
|
+
import { bytesToBase64 } from "./attachment-preview.js";
|
|
10
13
|
import css from './conversation.module.css';
|
|
11
14
|
import threadCss from './thread.module.css';
|
|
12
15
|
function factKey(fact) {
|
|
@@ -29,7 +32,7 @@ function readMeta(facts) {
|
|
|
29
32
|
return new Map(facts.map(fact => [factKey(fact.fact), fact]));
|
|
30
33
|
}
|
|
31
34
|
export function TeamThreadPage(props) {
|
|
32
|
-
const { workspaceId, channelRef, taskRef, threadRef, taskNumber, backToWorkspace, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, reply, changeTask, t, } = props;
|
|
35
|
+
const { workspaceId, channelRef, taskRef, threadRef, taskNumber, backToWorkspace, selectChannel, selectThread, resolveTaskRefs, putAttachment, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, drafts, getAttachment, reply, changeTask, t, } = props;
|
|
33
36
|
const [projection, setProjection] = useState();
|
|
34
37
|
const [channelView, setChannelView] = useState();
|
|
35
38
|
const [members, setMembers] = useState([]);
|
|
@@ -40,9 +43,18 @@ export function TeamThreadPage(props) {
|
|
|
40
43
|
const [historyHasMore, setHistoryHasMore] = useState(false);
|
|
41
44
|
const [remainingUnreadCount, setRemainingUnreadCount] = useState(0);
|
|
42
45
|
const [newFactsCount, setNewFactsCount] = useState(0);
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
// The reply draft lives in the keyed draft cache: view switches unmount
|
|
47
|
+
// this page, and a refresh must not cost the half-written message either.
|
|
48
|
+
const draftKey = `thread:${threadRef}`;
|
|
49
|
+
const { draft, recipients } = useSyncExternalStore(drafts.subscribe, () => drafts.getSnapshot(draftKey));
|
|
45
50
|
const [claimsOpen, setClaimsOpen] = useState(false);
|
|
51
|
+
// Early acceptance: the Human may accept while Claims are still open; the
|
|
52
|
+
// confirm dialog lists exactly what will be completed with the Task.
|
|
53
|
+
const [confirmingAccept, setConfirmingAccept] = useState(false);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (confirmingAccept && projection?.task.resolution === 'accepted')
|
|
56
|
+
setConfirmingAccept(false);
|
|
57
|
+
}, [confirmingAccept, projection?.task.resolution]);
|
|
46
58
|
const [replyRequestId, setReplyRequestId] = useState();
|
|
47
59
|
const [confirmation, setConfirmation] = useState();
|
|
48
60
|
const [statusMessage, setStatusMessage] = useState();
|
|
@@ -291,11 +303,38 @@ export function TeamThreadPage(props) {
|
|
|
291
303
|
return status === undefined ? t('memberUnknown') : `@${status.member.handle}`;
|
|
292
304
|
};
|
|
293
305
|
const messageSender = (fact) => fact.kind === 'message' ? fact.message.sender : undefined;
|
|
306
|
+
const [pendingFiles, setPendingFiles] = useState([]);
|
|
307
|
+
// Branded-ref navigation for message bodies: channel refs hop to the
|
|
308
|
+
// Channel; the open Task's own refs are already on screen, and other Tasks
|
|
309
|
+
// are not resolvable from this surface, so they degrade to a no-op.
|
|
310
|
+
const openRef = (ref) => {
|
|
311
|
+
if (ref.startsWith('channel:') && ref !== channelRef)
|
|
312
|
+
selectChannel(ref);
|
|
313
|
+
if (ref.startsWith('task:') && ref !== taskRef) {
|
|
314
|
+
// Another Task cited here: resolve its home Channel and jump.
|
|
315
|
+
void resolveTaskRefs({ workspaceId, taskRefs: [ref] })
|
|
316
|
+
.then(result => {
|
|
317
|
+
if (!result.ok)
|
|
318
|
+
return;
|
|
319
|
+
const hit = result.value.resolved[0];
|
|
320
|
+
if (hit !== undefined)
|
|
321
|
+
selectThread(hit.taskRef, hit.threadRef, hit.channelRef, hit.taskNumber);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const lookupTaskRefs = (refs) => resolveTaskRefs({ workspaceId, taskRefs: refs })
|
|
326
|
+
.then(result => {
|
|
327
|
+
if (!result.ok)
|
|
328
|
+
return [];
|
|
329
|
+
for (const entry of result.value.resolved)
|
|
330
|
+
rememberResolvedTaskRef(entry);
|
|
331
|
+
return result.value.resolved;
|
|
332
|
+
});
|
|
294
333
|
const renderFact = (fact, grouped = false) => {
|
|
295
334
|
if (fact.kind === 'message') {
|
|
296
335
|
const sender = memberName(fact.message.sender);
|
|
297
336
|
const senderStatus = members.find(candidate => candidate.member.memberId === fact.message.sender);
|
|
298
|
-
return _jsx(TeamMessage, { senderName: sender, memberId: fact.message.sender, human: fact.message.sender === channelView?.humanMemberId, body: fact.message.body, occurredAt: fact.message.occurredAt, mentionNames: mentionNamesOf(fact.mentions, mentionHandlesMap), grouped: grouped, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }) }, factKey(fact));
|
|
337
|
+
return _jsx(TeamMessage, { senderName: sender, memberId: fact.message.sender, human: fact.message.sender === channelView?.humanMemberId, body: fact.message.body, attachments: fact.message.attachments, loadAttachment: getAttachment, t: t, occurredAt: fact.message.occurredAt, mentionNames: mentionNamesOf(fact.mentions, mentionHandlesMap), onOpenRef: openRef, onResolveTaskRefs: lookupTaskRefs, grouped: grouped, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }) }, factKey(fact));
|
|
299
338
|
}
|
|
300
339
|
return _jsxs("p", { className: threadCss.activityRow, children: [_jsx("span", { className: threadCss.activityMark, "aria-hidden": "true" }), _jsx("span", { className: threadCss.activityText, children: formatActivity(fact.activity, { t, actorName: memberName, claims: taskClaims }) })] }, factKey(fact));
|
|
301
340
|
};
|
|
@@ -305,8 +344,13 @@ export function TeamThreadPage(props) {
|
|
|
305
344
|
let run = [];
|
|
306
345
|
let lastDay;
|
|
307
346
|
const flushRun = () => {
|
|
308
|
-
if (run.length > 0)
|
|
309
|
-
nodes.push(_jsx("div", { className: css.messageRun, children: run.map((entry, entryIndex) =>
|
|
347
|
+
if (run.length > 0) {
|
|
348
|
+
nodes.push(_jsx("div", { className: css.messageRun, children: run.map((entry, entryIndex) => {
|
|
349
|
+
const previous = entryIndex > 0 ? run[entryIndex - 1] : undefined;
|
|
350
|
+
const turnGap = entry.kind === 'message' && isRunGap(previous?.kind === 'message' ? previous.message.occurredAt : undefined, entry.message.occurredAt);
|
|
351
|
+
return _jsxs(Fragment, { children: [turnGap && _jsx(TeamRunDivider, { occurredAt: entry.message.occurredAt }), renderFact(entry, entryIndex > 0)] }, factKey(entry));
|
|
352
|
+
}) }, `run-${factKey(run[0])}`));
|
|
353
|
+
}
|
|
310
354
|
run = [];
|
|
311
355
|
};
|
|
312
356
|
facts.forEach((fact, index) => {
|
|
@@ -392,7 +436,24 @@ export function TeamThreadPage(props) {
|
|
|
392
436
|
setPending(true);
|
|
393
437
|
setError(undefined);
|
|
394
438
|
try {
|
|
395
|
-
|
|
439
|
+
// Upload chosen files first; any failure aborts the reply with the
|
|
440
|
+
// existing error surface and keeps the chips for a retry.
|
|
441
|
+
const attachmentIds = [];
|
|
442
|
+
for (const file of pendingFiles) {
|
|
443
|
+
const uploaded = await putAttachment({
|
|
444
|
+
requestId: crypto.randomUUID(),
|
|
445
|
+
workspaceId,
|
|
446
|
+
name: file.name,
|
|
447
|
+
mediaType: file.type === '' ? undefined : file.type,
|
|
448
|
+
bytesBase64: bytesToBase64(new Uint8Array(await file.arrayBuffer())),
|
|
449
|
+
});
|
|
450
|
+
if (!uploaded.ok) {
|
|
451
|
+
setError(uploaded.error.message);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
attachmentIds.push(uploaded.value.attachmentId);
|
|
455
|
+
}
|
|
456
|
+
const result = await reply({ requestId: id, workspaceId, taskRef: task.taskRef, body: draft.trim(), baseRevision: thread.revision, recipients: [...recipients].sort(), ...(attachmentIds.length === 0 ? {} : { attachments: attachmentIds }), ...(confirmation === undefined ? {} : { confirmationToken: confirmation }) });
|
|
396
457
|
if (!result.ok) {
|
|
397
458
|
setError(result.error.message);
|
|
398
459
|
return;
|
|
@@ -405,8 +466,8 @@ export function TeamThreadPage(props) {
|
|
|
405
466
|
return merged;
|
|
406
467
|
});
|
|
407
468
|
setProjection(current => current === undefined ? current : { ...current, task: committed.task, thread: committed.thread });
|
|
408
|
-
|
|
409
|
-
|
|
469
|
+
drafts.clear(draftKey);
|
|
470
|
+
setPendingFiles([]);
|
|
410
471
|
setReplyRequestId(undefined);
|
|
411
472
|
setConfirmation(undefined);
|
|
412
473
|
setStatusMessage(undefined);
|
|
@@ -453,12 +514,28 @@ export function TeamThreadPage(props) {
|
|
|
453
514
|
// Threads are always entered through a Channel page, so a Channel origin
|
|
454
515
|
// returns to its timeline; a Thread restored without one returns further.
|
|
455
516
|
const backLabel = channelRef === undefined ? t('backToWorkspace') : t('backToChannel');
|
|
456
|
-
return _jsxs("main", { className: css.surface, "data-team-thread": threadRef, children: [_jsxs("div", { className: css.surfaceHeader, children: [_jsx("div", { className: css.backRow, children: _jsx(Button, { size: "sm", icon: _jsx(IconChevronLeftOutline14, {}), onClick: backToWorkspace, children: backLabel }) }), _jsxs("header", { className: css.headerRow, children: [_jsxs("div", { className: css.headerCopy, children: [_jsxs("div", { className: threadCss.titleLine, children: [_jsx("h1", { children: task === undefined ? t('taskPending') : t('taskLabel', { number: taskNumber ?? '…' }) }), task !== undefined && _jsx(Pill, { children: formatTaskStatus(task.status, t) })] }), taskTitle !== undefined && taskTitle !== '' && _jsx("p", { className: threadCss.taskTitle, children: taskTitle })] }), task !== undefined && thread !== undefined && task.resolution !== 'closed' && _jsxs("div", { className: css.headerActions, children: [
|
|
517
|
+
return _jsxs("main", { className: css.surface, "data-team-thread": threadRef, children: [_jsxs("div", { className: css.surfaceHeader, children: [_jsx("div", { className: css.backRow, children: _jsx(Button, { size: "sm", icon: _jsx(IconChevronLeftOutline14, {}), onClick: backToWorkspace, children: backLabel }) }), _jsxs("header", { className: css.headerRow, children: [_jsxs("div", { className: css.headerCopy, children: [_jsxs("div", { className: threadCss.titleLine, children: [_jsx("h1", { children: task === undefined ? t('taskPending') : t('taskLabel', { number: taskNumber ?? '…' }) }), task !== undefined && _jsx(Pill, { children: formatTaskStatus(task.status, t) })] }), taskTitle !== undefined && taskTitle !== '' && _jsx("p", { className: threadCss.taskTitle, children: taskTitle })] }), task !== undefined && thread !== undefined && task.resolution !== 'closed' && _jsxs("div", { className: css.headerActions, children: [(() => {
|
|
518
|
+
const activeClaims = projection?.claims.filter(claim => claim.taskRef === task.taskRef && claim.state === 'active') ?? [];
|
|
519
|
+
const earlyAccept = task.resolution === 'open' && task.status === 'in_progress' && activeClaims.length > 0;
|
|
520
|
+
if (!(task.status === 'in_review' || earlyAccept) || task.resolution !== 'open')
|
|
521
|
+
return null;
|
|
522
|
+
return _jsx(Button, { size: "sm", variant: "primary", disabled: pending, onClick: () => { if (earlyAccept) {
|
|
523
|
+
setConfirmingAccept(true);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
void mutateTask('accept');
|
|
527
|
+
} }, children: t('acceptTask') });
|
|
528
|
+
})(), task.resolution === 'open'
|
|
457
529
|
? _jsx(Button, { size: "sm", variant: "outline", disabled: pending, onClick: () => { void mutateTask('close'); }, children: t('closeTask') })
|
|
458
|
-
: _jsx(Button, { size: "sm", variant: "primary", disabled: pending, onClick: () => { void mutateTask('reopen'); }, children: t('reopenTask') })] })] }), risks.length > 0 && _jsxs("section", { className: threadCss.riskSection, "aria-label": t('runtimeRisk'), children: [_jsx("h2", { children: t('runtimeRisk') }), risks.map(({ claim, status }) => _jsxs("p", { className: threadCss.riskRow, children: [_jsx(TeamPresenceDot, { status: status, t: t }), _jsxs("span", { children: [t('runtimeRiskDetail', { member: status.member.handle, diagnostic: status.diagnostic ?? t('statusError') }), " \u00B7 ", claim.direction] })] }, claim.claimRef))] }), task !== undefined && thread !== undefined &&
|
|
530
|
+
: _jsx(Button, { size: "sm", variant: "primary", disabled: pending, onClick: () => { void mutateTask('reopen'); }, children: t('reopenTask') })] })] }), risks.length > 0 && _jsxs("section", { className: threadCss.riskSection, "aria-label": t('runtimeRisk'), children: [_jsx("h2", { children: t('runtimeRisk') }), risks.map(({ claim, status }) => _jsxs("p", { className: threadCss.riskRow, children: [_jsx(TeamPresenceDot, { status: status, t: t }), _jsxs("span", { children: [t('runtimeRiskDetail', { member: status.member.handle, diagnostic: status.diagnostic ?? t('statusError') }), " \u00B7 ", claim.direction] })] }, claim.claimRef))] }), task !== undefined && thread !== undefined && (() => {
|
|
531
|
+
// Recomputed here so the confirm list never shows stale rows.
|
|
532
|
+
const activeClaims = projection?.claims.filter(claim => claim.taskRef === task.taskRef && claim.state === 'active') ?? [];
|
|
533
|
+
return _jsxs(Modal, { open: confirmingAccept, onClose: () => { if (!pending)
|
|
534
|
+
setConfirmingAccept(false); }, title: t('acceptEarlyTitle'), closeLabel: t('cancel'), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: pending, onClick: () => { setConfirmingAccept(false); }, children: t('cancel') }), _jsx(Button, { variant: "primary", disabled: pending, onClick: () => { void mutateTask('accept'); }, children: pending ? t('acceptingTask') : t('acceptTask') })] }), children: [_jsx("p", { className: css.confirmBody, children: t('acceptEarlyBody', { count: activeClaims.length }) }), _jsx("ul", { className: css.confirmList, children: activeClaims.map(claim => (_jsxs("li", { children: [memberName(claim.owner), " \u00B7 ", claim.direction] }, claim.claimRef))) })] });
|
|
535
|
+
})(), task !== undefined && thread !== undefined && _jsx("section", { className: threadCss.workSection, "aria-label": t('claims'), children: _jsx(DisclosureRow, { expandOnRowClick: true, expandable: true, open: claimsOpen, onToggle: () => { setClaimsOpen(current => !current); }, icon: _jsx(IconChecklistOutline14, { size: 14 }), title: `${t('claims')} · ${taskClaims.length}`, children: _jsxs("div", { className: threadCss.claimList, children: [taskClaims.length === 0 && _jsx("p", { className: threadCss.emptyClaims, children: t('noClaims') }), taskClaims.map(claim => {
|
|
459
536
|
const ownerStatus = members.find(status => status.member.memberId === claim.owner);
|
|
460
537
|
return _jsxs("article", { className: threadCss.claimRow, children: [ownerStatus === undefined ? _jsx("span", {}) : _jsx(TeamPresenceDot, { status: ownerStatus, t: t }), _jsx("strong", { className: threadCss.claimOwner, children: memberName(claim.owner) }), _jsx("span", { className: threadCss.claimDirection, children: claim.direction }), _jsx("small", { className: threadCss.claimState, children: formatClaimState(claim.state, t) })] }, claim.claimRef);
|
|
461
538
|
})] }) }) })] }), _jsx("section", { ref: timeline.ref, onScroll: timeline.onScroll, className: css.timeline, "aria-label": t('timelineLabel'), children: _jsxs("div", { className: css.timelineContent, children: [loading && projection === undefined && error === undefined && _jsx("div", { className: css.emptySurface, children: _jsxs("p", { className: css.loadingState, children: [_jsx("span", { className: css.loadingMark, "aria-hidden": "true" }), t('loadingThread')] }) }), projection === undefined && error !== undefined && _jsxs("div", { className: css.errorState, role: "alert", children: [_jsx("span", { children: error }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => { void readCurrent(); }, children: t('retry') })] }), olderFacts.length > 0 && _jsxs("section", { className: threadCss.historySection, "aria-label": t('olderHistory'), children: [_jsx("h2", { children: t('olderHistory') }), renderFactBlocks(olderFacts, undefined)] }), historyHasMore && _jsx("div", { className: css.timelineAction, children: _jsx(Button, { size: "sm", onClick: () => { void loadOlder(); }, children: t('loadOlder') }) }), currentFactsWithAnchor.length > 0 && _jsx("section", { className: threadCss.publicSection, children: renderFactBlocks(currentFactsWithAnchor, unreadBoundary) }), newFactsCount > 0 && _jsxs("div", { className: threadCss.newUpdates, role: "status", children: [_jsx("span", { children: t('readNewUpdates', { count: newFactsCount }) }), _jsx(Button, { size: "sm", variant: "outline", disabled: loading, onClick: () => { timeline.jumpToLatest(); void readCurrent(true); }, children: t('markRead') })] }), remainingUnreadCount > 0 && _jsxs("div", { className: css.timelineAction, role: "status", children: [_jsx("span", { children: t('remainingUnread', { count: remainingUnreadCount }) }), _jsx(Button, { size: "sm", onClick: () => { timeline.jumpToBoundary(); void readCurrent(true); }, disabled: loading, children: t('continueReading') })] })] }) }), projection !== undefined && task !== undefined && thread !== undefined ? (task.resolution === 'closed'
|
|
462
539
|
? _jsxs("div", { className: threadCss.closedBar, "data-team-closed": true, children: [error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error }), _jsxs("div", { className: threadCss.closedNotice, children: [_jsx("span", { children: t('taskClosedNotice') }), _jsx(Button, { size: "sm", variant: "outline", disabled: pending, onClick: () => { void mutateTask('reopen'); }, children: t('reopenTask') })] })] })
|
|
463
|
-
: _jsx(TeamComposer, { members: channelMembers, recipients: recipients, draft: draft, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onDraftChange: next => {
|
|
540
|
+
: _jsx(TeamComposer, { members: channelMembers, recipients: recipients, draft: draft, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onDraftChange: next => { drafts.writeDraft(draftKey, next); setConfirmation(undefined); setReplyRequestId(undefined); setStatusMessage(undefined); }, onRecipientsChange: next => { drafts.writeRecipients(draftKey, next); setConfirmation(undefined); setReplyRequestId(undefined); setStatusMessage(undefined); }, onSubmit: () => { void sendReply(); }, pendingFiles: pendingFiles, onFilesChange: setPendingFiles, t: t }, draftKey)) : _jsx("div", {})] });
|
|
464
541
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { TeamSidebarProps } from './slots.ts';
|
|
2
|
-
export declare function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWorkspace, selectChannel, t, useWorkspaces, loadMembers, subscribeChanges, addMember, loadChannels, createChannel, updateChannel, updateMember, joinChannel, removeChannelMember, loadModels, openMemberSession }: TeamSidebarProps): import("react").JSX.Element;
|
|
2
|
+
export declare function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWorkspace, selectChannel, t, useWorkspaces, loadMembers, subscribeChanges, addMember, loadChannels, createChannel, updateChannel, updateMember, recoverMember, joinChannel, removeChannelMember, loadModels, openMemberSession }: TeamSidebarProps): import("react").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=TeamWorkspaceBrowser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamWorkspaceBrowser.d.ts","sourceRoot":"","sources":["../../../src/client/TeamWorkspaceBrowser.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAQlD,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,mBAAmB,EAAE,UAAU,EAAE,iBAAiB,EAAE,EAAE,gBAAgB,+
|
|
1
|
+
{"version":3,"file":"TeamWorkspaceBrowser.d.ts","sourceRoot":"","sources":["../../../src/client/TeamWorkspaceBrowser.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAQlD,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,mBAAmB,EAAE,UAAU,EAAE,iBAAiB,EAAE,EAAE,gBAAgB,+BA4D/T"}
|
|
@@ -5,16 +5,17 @@ import { TeamWorkspaceRow } from "./TeamWorkspaceRow.js";
|
|
|
5
5
|
import { TeamAgentsPanel } from "./TeamAgentsPanel.js";
|
|
6
6
|
import { TeamChannelsPanel } from "./TeamChannelsPanel.js";
|
|
7
7
|
import css from './sidebar.module.css';
|
|
8
|
-
export function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWorkspace, selectChannel, t, useWorkspaces, loadMembers, subscribeChanges, addMember, loadChannels, createChannel, updateChannel, updateMember, joinChannel, removeChannelMember, loadModels, openMemberSession }) {
|
|
8
|
+
export function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWorkspace, selectChannel, t, useWorkspaces, loadMembers, subscribeChanges, addMember, loadChannels, createChannel, updateChannel, updateMember, recoverMember, joinChannel, removeChannelMember, loadModels, openMemberSession }) {
|
|
9
9
|
const navigationState = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot, navigation.getSnapshot);
|
|
10
10
|
const workspaces = useWorkspaces(state => state.items);
|
|
11
11
|
const selected = navigationState.workspaceId;
|
|
12
12
|
const selectedExists = selected !== undefined && workspaces.some(workspace => workspace.workspaceId === selected);
|
|
13
13
|
const selectedId = selectedExists ? selected : workspaces[0]?.workspaceId;
|
|
14
14
|
// Exactly one sidebar row carries aria-current='page': the open Channel while
|
|
15
|
-
// one is set,
|
|
16
|
-
//
|
|
17
|
-
|
|
15
|
+
// one is set, the selected Agent card while a Member Session view is open,
|
|
16
|
+
// otherwise the browsed Workspace's overview. The selected row keeps its
|
|
17
|
+
// quiet folder tint (data-selected) in every case.
|
|
18
|
+
const overviewIsCurrent = navigationState.channelRef === undefined && navigationState.memberSessionId === undefined;
|
|
18
19
|
const [creatingAgents, setCreatingAgents] = useState([]);
|
|
19
20
|
// Rail icons request expansion and name the section to reveal once wide.
|
|
20
21
|
const [pendingSection, setPendingSection] = useState();
|
|
@@ -35,5 +36,5 @@ export function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWo
|
|
|
35
36
|
if (!wide) {
|
|
36
37
|
return _jsxs("nav", { className: css.railWorkspace, "aria-label": t('workspaceSections'), children: [_jsx(Tooltip, { label: t('channels'), side: "right", children: _jsx("button", { type: "button", className: css.railButton, "aria-label": t('channels'), onClick: () => { setPendingSection('channels'); expandSidebar(); }, children: _jsx(IconListPenOutline16, { size: 16 }) }) }), _jsx(Tooltip, { label: t('agents'), side: "right", children: _jsx("button", { type: "button", className: css.railButton, "aria-label": t('agents'), onClick: () => { setPendingSection('agents'); expandSidebar(); }, children: _jsx(IconAgentPresetOutline16, { size: 16 }) }) })] });
|
|
37
38
|
}
|
|
38
|
-
return _jsxs("section", { className: css.workspaceBrowser, "aria-label": t('workspaces'), children: [_jsx("div", { className: css.workspaceHeader, children: t('workspaces') }), _jsxs("div", { className: css.workspaceList, children: [workspaces.map(workspace => _jsx(TeamWorkspaceRow, { workspaceId: workspace.workspaceId, title: workspace.title, path: workspace.path, selected: workspace.workspaceId === selectedId, current: workspace.workspaceId === selectedId && overviewIsCurrent, onSelect: selectWorkspace }, workspace.workspaceId)), workspaces.length === 0 && _jsx("p", { className: css.emptyState, children: t('empty') })] }), selectedId !== undefined && _jsxs("div", { className: css.workspaceSection, children: [_jsx("div", { ref: channelsRef, children: _jsx(TeamChannelsPanel, { workspaceId: selectedId, loadMembers: loadMembers, loadChannels: loadChannels, subscribeChanges: subscribeChanges, createChannel: createChannel, updateChannel: updateChannel, joinChannel: joinChannel, removeChannelMember: removeChannelMember, creatingAgents: creatingAgents.filter(request => request.workspaceId === selectedId), ...(navigationState.channelRef === undefined ? {} : { selectedChannelRef: navigationState.channelRef }), selectChannel: selectChannel, t: t }, selectedId) }), _jsx("div", { ref: agentsRef, children: _jsx(TeamAgentsPanel, { workspaceId: selectedId, loadMembers: loadMembers, subscribeChanges: subscribeChanges, loadChannels: loadChannels, addMember: addMember, updateMember: updateMember, joinChannel: joinChannel, removeChannelMember: removeChannelMember, loadModels: loadModels, openMemberSession: openMemberSession, onCreatingChange: (request, creating) => { setCreatingAgents(current => creating ? [...current.filter(item => item.requestId !== request.requestId), request] : current.filter(item => item.requestId !== request.requestId)); }, t: t }, selectedId) })] })] });
|
|
39
|
+
return _jsxs("section", { className: css.workspaceBrowser, "aria-label": t('workspaces'), children: [_jsx("div", { className: css.workspaceHeader, children: t('workspaces') }), _jsxs("div", { className: css.workspaceList, children: [workspaces.map(workspace => _jsx(TeamWorkspaceRow, { workspaceId: workspace.workspaceId, title: workspace.title, path: workspace.path, selected: workspace.workspaceId === selectedId, current: workspace.workspaceId === selectedId && overviewIsCurrent, onSelect: selectWorkspace }, workspace.workspaceId)), workspaces.length === 0 && _jsx("p", { className: css.emptyState, children: t('empty') })] }), selectedId !== undefined && _jsxs("div", { className: css.workspaceSection, children: [_jsx("div", { ref: channelsRef, children: _jsx(TeamChannelsPanel, { workspaceId: selectedId, loadMembers: loadMembers, loadChannels: loadChannels, subscribeChanges: subscribeChanges, createChannel: createChannel, updateChannel: updateChannel, joinChannel: joinChannel, removeChannelMember: removeChannelMember, creatingAgents: creatingAgents.filter(request => request.workspaceId === selectedId), ...(navigationState.memberSessionId !== undefined || navigationState.channelRef === undefined ? {} : { selectedChannelRef: navigationState.channelRef }), selectChannel: selectChannel, t: t }, selectedId) }), _jsx("div", { ref: agentsRef, children: _jsx(TeamAgentsPanel, { workspaceId: selectedId, loadMembers: loadMembers, subscribeChanges: subscribeChanges, loadChannels: loadChannels, addMember: addMember, updateMember: updateMember, recoverMember: recoverMember, joinChannel: joinChannel, removeChannelMember: removeChannelMember, loadModels: loadModels, ...(navigationState.memberSessionId === undefined ? {} : { memberSessionId: navigationState.memberSessionId }), openMemberSession: openMemberSession, onCreatingChange: (request, creating) => { setCreatingAgents(current => creating ? [...current.filter(item => item.requestId !== request.requestId), request] : current.filter(item => item.requestId !== request.requestId)); }, t: t }, selectedId) })] })] });
|
|
39
40
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentTeamAttachmentId, AgentTeamGetAttachmentRequest, AgentTeamGetAttachmentResult } from '@wowyuarm/dsh-agent-team/types';
|
|
2
|
+
/** Base64 one file payload in chunks so large uploads stay off the call-stack limit. */
|
|
3
|
+
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
4
|
+
/** Mirrors the slot's Remote result union without importing the slots module. */
|
|
5
|
+
type GetAttachment = (request: AgentTeamGetAttachmentRequest) => Promise<{
|
|
6
|
+
ok: true;
|
|
7
|
+
value: AgentTeamGetAttachmentResult;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
error: {
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
export declare function cachedAttachmentDataUrl(attachmentId: AgentTeamAttachmentId): string | null | undefined;
|
|
15
|
+
export declare function loadAttachmentDataUrl(getAttachment: GetAttachment, attachment: {
|
|
16
|
+
attachmentId: AgentTeamAttachmentId;
|
|
17
|
+
mediaType: string;
|
|
18
|
+
}): Promise<string | null>;
|
|
19
|
+
/** Human-readable byte size for attachment chips. */
|
|
20
|
+
export declare function formatByteSize(byteSize: number): string;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=attachment-preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-preview.d.ts","sourceRoot":"","sources":["../../../src/client/attachment-preview.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,6BAA6B,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAA;AAExI,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAOvD;AAED,iFAAiF;AACjF,KAAK,aAAa,GAAG,CAAC,OAAO,EAAE,6BAA6B,KAAK,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,4BAA4B,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CAAA;AASvK,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAEtG;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE;IAAE,YAAY,EAAE,qBAAqB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOxK;AAED,qDAAqD;AACrD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvD"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Base64 one file payload in chunks so large uploads stay off the call-stack limit. */
|
|
2
|
+
export function bytesToBase64(bytes) {
|
|
3
|
+
let binary = '';
|
|
4
|
+
const chunk = 0x8000;
|
|
5
|
+
for (let start = 0; start < bytes.length; start += chunk) {
|
|
6
|
+
binary += String.fromCharCode(...bytes.subarray(start, start + chunk));
|
|
7
|
+
}
|
|
8
|
+
return btoa(binary);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Thumbnail data URLs live in one session-wide cache: message lists re-render
|
|
12
|
+
* often, and each miss costs a Host round-trip plus a base64 decode. Failed
|
|
13
|
+
* loads (bytes already GC'd) are cached as `null` so the chip fallback is stable.
|
|
14
|
+
*/
|
|
15
|
+
const dataUrlCache = new Map();
|
|
16
|
+
export function cachedAttachmentDataUrl(attachmentId) {
|
|
17
|
+
return dataUrlCache.get(attachmentId);
|
|
18
|
+
}
|
|
19
|
+
export async function loadAttachmentDataUrl(getAttachment, attachment) {
|
|
20
|
+
const cached = dataUrlCache.get(attachment.attachmentId);
|
|
21
|
+
if (cached !== undefined)
|
|
22
|
+
return cached;
|
|
23
|
+
const result = await getAttachment({ attachmentId: attachment.attachmentId });
|
|
24
|
+
const url = result.ok && attachment.mediaType.startsWith('image/') ? `data:${attachment.mediaType};base64,${result.value.bytesBase64}` : null;
|
|
25
|
+
dataUrlCache.set(attachment.attachmentId, url);
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
/** Human-readable byte size for attachment chips. */
|
|
29
|
+
export function formatByteSize(byteSize) {
|
|
30
|
+
if (byteSize < 1024)
|
|
31
|
+
return `${byteSize} B`;
|
|
32
|
+
if (byteSize < 1024 * 1024)
|
|
33
|
+
return `${(byteSize / 1024).toFixed(0)} KB`;
|
|
34
|
+
return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AgentTeamChannelRef, AgentTeamMemberId, AgentTeamThreadRef } from '@wowyuarm/dsh-agent-team/types';
|
|
2
|
+
/**
|
|
3
|
+
* Keyed draft cache for the Channel/Thread composers. Drafts are local UI
|
|
4
|
+
* convenience state — never Host-authoritative facts — so localStorage is the
|
|
5
|
+
* right home: switching views unmounts the pages, and a refresh should not
|
|
6
|
+
* cost the Human their half-written message.
|
|
7
|
+
*/
|
|
8
|
+
export type TeamDraftKey = `channel:${AgentTeamChannelRef}` | `thread:${AgentTeamThreadRef}`;
|
|
9
|
+
/** One cached composer state; `recipients` mirrors the structured mention set. */
|
|
10
|
+
export interface TeamDraftState {
|
|
11
|
+
readonly draft: string;
|
|
12
|
+
readonly recipients: ReadonlySet<AgentTeamMemberId>;
|
|
13
|
+
}
|
|
14
|
+
declare const STORAGE_KEY = "dsh.agent-team.drafts.v1";
|
|
15
|
+
/**
|
|
16
|
+
* Root-scoped draft cache, one instance per Client context (created in
|
|
17
|
+
* `applyUi` and injected like the navigation service — never a module
|
|
18
|
+
* singleton). Slot lifetimes subscribe per key.
|
|
19
|
+
*/
|
|
20
|
+
export declare class TeamDraftStore {
|
|
21
|
+
private entries;
|
|
22
|
+
private readonly snapshots;
|
|
23
|
+
private readonly listeners;
|
|
24
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
25
|
+
/** Release listeners when the owning Client context disposes. */
|
|
26
|
+
dispose(): void;
|
|
27
|
+
/** Stable per-key snapshot identity for useSyncExternalStore. */
|
|
28
|
+
readonly getSnapshot: (key: TeamDraftKey) => TeamDraftState;
|
|
29
|
+
writeDraft(key: TeamDraftKey, draft: string): void;
|
|
30
|
+
writeRecipients(key: TeamDraftKey, recipients: Iterable<AgentTeamMemberId>): void;
|
|
31
|
+
/** Drop one key entirely — the success path after a committed send. */
|
|
32
|
+
clear(key: TeamDraftKey): void;
|
|
33
|
+
/** Test seam: forget everything, including what localStorage still holds. */
|
|
34
|
+
reset(): void;
|
|
35
|
+
/** Re-read persisted content — what a fresh page load starts from. */
|
|
36
|
+
reload(): void;
|
|
37
|
+
private write;
|
|
38
|
+
private evict;
|
|
39
|
+
}
|
|
40
|
+
export { STORAGE_KEY as TEAM_DRAFTS_STORAGE_KEY };
|
|
41
|
+
//# sourceMappingURL=drafts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drafts.d.ts","sourceRoot":"","sources":["../../../src/client/drafts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AAEhH;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,mBAAmB,EAAE,GAAG,UAAU,kBAAkB,EAAE,CAAA;AAE5F,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;CACpD;AAQD,QAAA,MAAM,WAAW,6BAA6B,CAAA;AAuC9C;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IACpE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,QAAQ,CAAC,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAGxD;IAED,iEAAiE;IACjE,OAAO,IAAI,IAAI;IAIf,iEAAiE;IACjE,QAAQ,CAAC,WAAW,GAAI,KAAK,YAAY,KAAG,cAAc,CAQzD;IAED,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIlD,eAAe,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,CAAC,iBAAiB,CAAC,GAAG,IAAI;IAIjF,uEAAuE;IACvE,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI;IAO9B,6EAA6E;IAC7E,KAAK,IAAI,IAAI;IAOb,sEAAsE;IACtE,MAAM,IAAI,IAAI;IAMd,OAAO,CAAC,KAAK;IAwBb,OAAO,CAAC,KAAK;CAed;AAED,OAAO,EAAE,WAAW,IAAI,uBAAuB,EAAE,CAAA"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const STORAGE_KEY = 'dsh.agent-team.drafts.v1';
|
|
2
|
+
/** Retention ceiling; the oldest savedAt entries fall out first. */
|
|
3
|
+
const LIMIT = 50;
|
|
4
|
+
const EMPTY = Object.freeze({ draft: '', recipients: new Set() });
|
|
5
|
+
function isTeamDraftKey(value) {
|
|
6
|
+
return value.startsWith('channel:') || value.startsWith('thread:');
|
|
7
|
+
}
|
|
8
|
+
function readStored() {
|
|
9
|
+
if (typeof localStorage === 'undefined')
|
|
10
|
+
return new Map();
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '');
|
|
13
|
+
const restored = new Map();
|
|
14
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
15
|
+
if (!isTeamDraftKey(key) || typeof value !== 'object' || value === null)
|
|
16
|
+
continue;
|
|
17
|
+
const candidate = value;
|
|
18
|
+
if (typeof candidate.draft !== 'string' || !Array.isArray(candidate.recipientIds) || typeof candidate.savedAt !== 'number')
|
|
19
|
+
continue;
|
|
20
|
+
if (!candidate.recipientIds.every(id => typeof id === 'string'))
|
|
21
|
+
continue;
|
|
22
|
+
restored.set(key, { draft: candidate.draft, recipientIds: candidate.recipientIds, savedAt: candidate.savedAt });
|
|
23
|
+
}
|
|
24
|
+
return restored;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Local persistence is a convenience; malformed content just starts empty.
|
|
28
|
+
return new Map();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function persistStored(entries) {
|
|
32
|
+
if (typeof localStorage === 'undefined')
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
const payload = Object.fromEntries([...entries].map(([key, entry]) => [key, entry]));
|
|
36
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Private mode and quota failures do not block composing.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Root-scoped draft cache, one instance per Client context (created in
|
|
44
|
+
* `applyUi` and injected like the navigation service — never a module
|
|
45
|
+
* singleton). Slot lifetimes subscribe per key.
|
|
46
|
+
*/
|
|
47
|
+
export class TeamDraftStore {
|
|
48
|
+
entries = readStored();
|
|
49
|
+
snapshots = new Map();
|
|
50
|
+
listeners = new Set();
|
|
51
|
+
subscribe = (listener) => {
|
|
52
|
+
this.listeners.add(listener);
|
|
53
|
+
return () => { this.listeners.delete(listener); };
|
|
54
|
+
};
|
|
55
|
+
/** Release listeners when the owning Client context disposes. */
|
|
56
|
+
dispose() {
|
|
57
|
+
this.listeners.clear();
|
|
58
|
+
}
|
|
59
|
+
/** Stable per-key snapshot identity for useSyncExternalStore. */
|
|
60
|
+
getSnapshot = (key) => {
|
|
61
|
+
const cached = this.snapshots.get(key);
|
|
62
|
+
if (cached !== undefined)
|
|
63
|
+
return cached;
|
|
64
|
+
const entry = this.entries.get(key);
|
|
65
|
+
if (entry === undefined)
|
|
66
|
+
return EMPTY;
|
|
67
|
+
const snapshot = Object.freeze({ draft: entry.draft, recipients: new Set(entry.recipientIds) });
|
|
68
|
+
this.snapshots.set(key, snapshot);
|
|
69
|
+
return snapshot;
|
|
70
|
+
};
|
|
71
|
+
writeDraft(key, draft) {
|
|
72
|
+
this.write(key, { draft });
|
|
73
|
+
}
|
|
74
|
+
writeRecipients(key, recipients) {
|
|
75
|
+
this.write(key, { recipientIds: [...recipients] });
|
|
76
|
+
}
|
|
77
|
+
/** Drop one key entirely — the success path after a committed send. */
|
|
78
|
+
clear(key) {
|
|
79
|
+
if (!this.entries.delete(key))
|
|
80
|
+
return;
|
|
81
|
+
this.snapshots.delete(key);
|
|
82
|
+
persistStored(this.entries);
|
|
83
|
+
for (const listener of this.listeners)
|
|
84
|
+
listener();
|
|
85
|
+
}
|
|
86
|
+
/** Test seam: forget everything, including what localStorage still holds. */
|
|
87
|
+
reset() {
|
|
88
|
+
this.entries = new Map();
|
|
89
|
+
this.snapshots.clear();
|
|
90
|
+
if (typeof localStorage !== 'undefined')
|
|
91
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
92
|
+
for (const listener of this.listeners)
|
|
93
|
+
listener();
|
|
94
|
+
}
|
|
95
|
+
/** Re-read persisted content — what a fresh page load starts from. */
|
|
96
|
+
reload() {
|
|
97
|
+
this.entries = readStored();
|
|
98
|
+
this.snapshots.clear();
|
|
99
|
+
for (const listener of this.listeners)
|
|
100
|
+
listener();
|
|
101
|
+
}
|
|
102
|
+
write(key, patch) {
|
|
103
|
+
const previous = this.entries.get(key);
|
|
104
|
+
const next = {
|
|
105
|
+
draft: patch.draft ?? previous?.draft ?? '',
|
|
106
|
+
recipientIds: patch.recipientIds ?? previous?.recipientIds ?? [],
|
|
107
|
+
savedAt: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
// Identical content must not bump savedAt or notify — convergence effects
|
|
110
|
+
// re-run freely and would otherwise loop through persistence.
|
|
111
|
+
if (previous !== undefined && previous.draft === next.draft
|
|
112
|
+
&& previous.recipientIds.length === next.recipientIds.length
|
|
113
|
+
&& previous.recipientIds.every((id, index) => id === next.recipientIds[index]))
|
|
114
|
+
return;
|
|
115
|
+
// An emptied composer leaves nothing worth restoring behind.
|
|
116
|
+
if (next.draft === '' && next.recipientIds.length === 0) {
|
|
117
|
+
this.clear(key);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.entries.set(key, next);
|
|
121
|
+
this.evict();
|
|
122
|
+
this.snapshots.delete(key);
|
|
123
|
+
persistStored(this.entries);
|
|
124
|
+
for (const listener of this.listeners)
|
|
125
|
+
listener();
|
|
126
|
+
}
|
|
127
|
+
evict() {
|
|
128
|
+
while (this.entries.size > LIMIT) {
|
|
129
|
+
let oldestKey;
|
|
130
|
+
let oldestSavedAt = Number.POSITIVE_INFINITY;
|
|
131
|
+
for (const [key, entry] of this.entries) {
|
|
132
|
+
if (entry.savedAt < oldestSavedAt) {
|
|
133
|
+
oldestSavedAt = entry.savedAt;
|
|
134
|
+
oldestKey = key;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (oldestKey === undefined)
|
|
138
|
+
return;
|
|
139
|
+
this.entries.delete(oldestKey);
|
|
140
|
+
this.snapshots.delete(oldestKey);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export { STORAGE_KEY as TEAM_DRAFTS_STORAGE_KEY };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
2
2
|
import { TeamNavigation } from './navigation.ts';
|
|
3
|
+
import { TeamDraftStore } from './drafts.ts';
|
|
3
4
|
import { type TeamKey } from './locales.ts';
|
|
4
5
|
export type { TeamMode, TeamNavigationActions, TeamNavigationSnapshot } from './navigation.ts';
|
|
5
6
|
export type { TeamKey } from './locales.ts';
|
|
@@ -13,6 +14,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
13
14
|
declare module '@deepseek-ai/cordis' {
|
|
14
15
|
interface Context {
|
|
15
16
|
teamNavigation: TeamNavigation;
|
|
17
|
+
teamDrafts: TeamDraftStore;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
export declare function apply(ctx: ClientContext): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,wCAAwC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,wCAAwC,CAAA;AA8BtF,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAK5C,OAAO,EAAU,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAEnD,YAAY,EAAE,QAAQ,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAC9F,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAIhD,eAAO,MAAM,MAAM,UAElB,CAAA;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,IAAI,EAAE,OAAO,CAAA;KACd;CACF;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,cAAc,EAAE,cAAc,CAAA;QAC9B,UAAU,EAAE,cAAc,CAAA;KAC3B;CACF;AAoJD,wBAAsB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
|