@wowyuarm/dsh-agent-team 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +14 -2
  2. package/README.zh.md +10 -2
  3. package/package.json +2 -2
  4. package/packages/agent-team/lib/attachments.js +157 -0
  5. package/packages/agent-team/lib/index.js +274 -5
  6. package/packages/agent-team/lib/ledger.js +65 -13
  7. package/packages/agent-team/lib/recovery.js +87 -0
  8. package/packages/agent-team/lib/spec.js +17 -1
  9. package/packages/agent-team/lib/typert.host.js +321 -21
  10. package/packages/agent-team/lib/typert.remote-client.d.ts +9 -1
  11. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  12. package/packages/agent-team/lib/typert.remote-client.js +249 -17
  13. package/packages/agent-team/lib/types/attachments.d.ts +69 -0
  14. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -0
  15. package/packages/agent-team/lib/types/index.d.ts +66 -1
  16. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  17. package/packages/agent-team/lib/types/ledger.d.ts +17 -1
  18. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  19. package/packages/agent-team/lib/types/recovery.d.ts +42 -0
  20. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -0
  21. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/types.d.ts +85 -2
  23. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  24. package/packages/client-agent-team/README.md +2 -2
  25. package/packages/client-agent-team/README.zh.md +2 -2
  26. package/packages/client-agent-team/lib/client.js +1176 -281
  27. package/packages/client-agent-team/lib/client.js.map +1 -1
  28. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +6 -2
  29. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  30. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +104 -58
  31. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +5 -1
  32. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  33. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +44 -19
  34. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  35. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +23 -18
  36. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
  37. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  38. package/packages/client-agent-team/lib/types/client/TeamComposer.js +40 -4
  39. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  40. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  42. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +7 -2
  43. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  44. package/packages/client-agent-team/lib/types/client/TeamMessage.js +34 -8
  45. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
  46. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
  47. package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
  48. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +3 -0
  49. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +18 -11
  51. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  52. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +6 -5
  54. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
  55. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
  56. package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
  57. package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
  58. package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
  59. package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
  60. package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
  61. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/index.js +50 -10
  63. package/packages/client-agent-team/lib/types/client/locales.d.ts +28 -0
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/locales.js +28 -0
  66. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
  67. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
  68. package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
  69. package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
  70. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  71. package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
  72. package/packages/client-agent-team/lib/types/client/slots.d.ts +18 -2
  73. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  74. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +2 -0
  75. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  76. package/packages/client-agent-team/lib/types/client/team-formatters.js +4 -0
  77. package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
  78. package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
  79. package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
  80. package/packages/tool-agent-team/lib/index.js +6 -2
  81. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -1,6 +1,6 @@
1
1
  import type { AgentTeamClientMemberStatus, AgentTeamMemberId } from '@wowyuarm/dsh-agent-team/types';
2
2
  import type { TeamConversationProps } from './slots.ts';
3
- export declare function TeamComposer({ members, recipients, draft, pending, confirmation, error, onDraftChange, onRecipientsChange, onSubmit, t }: {
3
+ export declare function TeamComposer({ members, recipients, draft, pending, confirmation, error, onDraftChange, onRecipientsChange, onSubmit, pendingFiles, onFilesChange, t }: {
4
4
  readonly members: readonly AgentTeamClientMemberStatus[];
5
5
  readonly recipients: ReadonlySet<AgentTeamMemberId>;
6
6
  readonly draft: string;
@@ -10,6 +10,9 @@ export declare function TeamComposer({ members, recipients, draft, pending, conf
10
10
  readonly onDraftChange: (draft: string) => void;
11
11
  readonly onRecipientsChange: (recipients: ReadonlySet<AgentTeamMemberId>) => void;
12
12
  readonly onSubmit: () => void;
13
+ /** Upload-capable surfaces pass this to enable the "+" file picker; the reply path omits it. */
14
+ readonly pendingFiles?: readonly File[];
15
+ readonly onFilesChange?: (files: readonly File[]) => void;
13
16
  readonly t: TeamConversationProps['t'];
14
17
  }): import("react").JSX.Element;
15
18
  //# sourceMappingURL=TeamComposer.d.ts.map
@@ -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;AAoCvD,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE;IACzI,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAA;IACxD,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,QAAQ,CAAC,kBAAkB,EAAE,CAAC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;IAC7B,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC,+BAkKA"}
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;AAoCvD,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE;IACtK,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAA;IACxD,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,QAAQ,CAAC,kBAAkB,EAAE,CAAC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,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,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC,+BAyNA"}
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
- import { IconSendOutline16, useAnchoredMaxHeight, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { IconPlusOutline16, IconSendOutline16, useAnchoredMaxHeight, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { TeamPresenceDot } from "./TeamPresenceDot.js";
5
5
  import css from './composer.module.css';
6
6
  function findMention(draft, caret) {
@@ -29,8 +29,9 @@ function mentionCandidates(members, query) {
29
29
  && status.member.state !== 'inactive'
30
30
  && status.member.handle.toLocaleLowerCase().startsWith(normalized));
31
31
  }
32
- export function TeamComposer({ members, recipients, draft, pending, confirmation, error, onDraftChange, onRecipientsChange, onSubmit, t }) {
32
+ export function TeamComposer({ members, recipients, draft, pending, confirmation, error, onDraftChange, onRecipientsChange, onSubmit, pendingFiles, onFilesChange, t }) {
33
33
  const inputRef = useRef(null);
34
+ const fileInputRef = useRef(null);
34
35
  const rootRef = useRef(null);
35
36
  const menuRef = useRef(null);
36
37
  const composingRef = useRef(false);
@@ -57,6 +58,22 @@ export function TeamComposer({ members, recipients, draft, pending, confirmation
57
58
  }
58
59
  setHighlight(current => Math.min(current, candidates.length - 1));
59
60
  }, [mention, candidates.length]);
61
+ // Match the resident DSH composer without stealing a later user choice: Team
62
+ // data can load after navigation, so a dialog or another control may already
63
+ // own focus by the time this composer appears.
64
+ useEffect(() => {
65
+ const active = document.activeElement;
66
+ if (active !== document.body && active?.closest('[aria-current="page"]') === null)
67
+ return;
68
+ inputRef.current?.focus({ preventScroll: true });
69
+ }, []);
70
+ // Confirmation settles after a read-only submission span. Restore focus in
71
+ // case the browser moved it so the second Enter can confirm immediately.
72
+ useEffect(() => {
73
+ if (confirmation === undefined || pending)
74
+ return;
75
+ inputRef.current?.focus({ preventScroll: true });
76
+ }, [confirmation, pending]);
60
77
  const pruneRecipients = (nextDraft) => {
61
78
  const knownMembers = new Map(members.map(status => [status.member.memberId, status.member]));
62
79
  const next = new Set([...recipients].filter(memberId => {
@@ -66,6 +83,15 @@ export function TeamComposer({ members, recipients, draft, pending, confirmation
66
83
  if (next.size !== recipients.size)
67
84
  onRecipientsChange(next);
68
85
  };
86
+ // Restored drafts may carry recipients that no longer match the text (or
87
+ // unknown Members); converge on mount and on every state change so the
88
+ // cached entry never stays stale — the same rule user edits already apply.
89
+ // An unloaded roster must never judge recipients unknown.
90
+ useEffect(() => {
91
+ if (members.length === 0)
92
+ return;
93
+ pruneRecipients(draft);
94
+ }, [draft, recipients, members]);
69
95
  const updateMention = (nextDraft, caret) => {
70
96
  const match = findMention(nextDraft, caret);
71
97
  setMention(match);
@@ -133,6 +159,7 @@ export function TeamComposer({ members, recipients, draft, pending, confirmation
133
159
  onSubmit();
134
160
  };
135
161
  return _jsxs("form", { ref: rootRef, className: css.root, onSubmit: event => {
162
+ console.log('DBG composer submit pending=%s draft=%j', pending, draft);
136
163
  event.preventDefault();
137
164
  if (!pending && draft.trim() !== '')
138
165
  onSubmit();
@@ -140,5 +167,14 @@ export function TeamComposer({ members, recipients, draft, pending, confirmation
140
167
  const optionId = `${listId}-${status.member.memberId}`;
141
168
  const selected = index === highlight;
142
169
  return _jsxs("button", { id: optionId, type: "button", role: "option", "aria-selected": selected, className: css.mentionOption, onMouseDown: event => { event.preventDefault(); }, onClick: () => { selectMention(status); }, children: [_jsx(TeamPresenceDot, { status: status, t: t }), _jsxs("span", { className: css.mentionName, children: ["@", status.member.handle] }), _jsx("span", { className: css.mentionDescription, children: status.member.description })] }, status.member.memberId);
143
- }) }), _jsx("textarea", { ref: inputRef, "aria-label": t('messageDraft'), "aria-autocomplete": "list", "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen && activeCandidate !== undefined ? `${listId}-${activeCandidate.member.memberId}` : undefined, "aria-expanded": menuOpen, value: draft, disabled: pending, placeholder: t('messagePlaceholder'), rows: 1, onChange: onChange, 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); } })] }), recipients.size > 0 && _jsx("p", { className: css.notifyRow, "data-team-notify": true, children: t('composerNotify', { ids: [...recipients].sort().map(memberId => `@${members.find(candidate => candidate.member.memberId === memberId)?.member.handle ?? memberId}`).join(', ') }) }), _jsx("div", { className: css.toolbar, children: _jsx("button", { type: "submit", className: css.sendButton, "aria-label": pending ? t('sendingMessage') : t('sendMessage'), disabled: pending || draft.trim() === '', children: _jsx(IconSendOutline16, { size: 16 }) }) })] }), error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error })] });
170
+ }) }), _jsx("textarea", { ref: inputRef, "aria-label": t('messageDraft'), "aria-autocomplete": "list", "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen && activeCandidate !== undefined ? `${listId}-${activeCandidate.member.memberId}` : undefined, "aria-expanded": menuOpen, value: draft, readOnly: pending, placeholder: t('messagePlaceholder'), rows: 1, onChange: onChange, 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); } })] }), recipients.size > 0 && _jsx("p", { className: css.notifyRow, "data-team-notify": true, children: t('composerNotify', { ids: [...recipients].sort().map(memberId => `@${members.find(candidate => candidate.member.memberId === memberId)?.member.handle ?? memberId}`).join(', ') }) }), onFilesChange !== undefined && pendingFiles !== undefined && pendingFiles.length > 0 && (_jsx("ul", { className: css.fileChips, "aria-label": t('attachFiles'), children: pendingFiles.map((file, index) => _jsxs("li", { className: css.fileChip, children: [_jsx("span", { className: css.fileChipName, title: file.name, children: file.name }), _jsx("button", { type: "button", className: css.fileChipRemove, "aria-label": t('removeFile', { name: file.name }), disabled: pending, onClick: () => { onFilesChange(pendingFiles.filter((_, candidate) => candidate !== index)); }, children: "\u00D7" })] }, `${file.name}-${index}`)) })), _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 => {
171
+ const chosen = [...event.target.files ?? []];
172
+ if (chosen.length > 0 && pendingFiles !== undefined)
173
+ onFilesChange([...pendingFiles, ...chosen]);
174
+ event.target.value = '';
175
+ } }), _jsx("button", { type: "button", className: css.attachButton, "aria-label": t('attachFiles'), title: t('attachFiles'), disabled: pending, onClick: () => { fileInputRef.current?.click(); }, children: _jsx(IconPlusOutline16, { size: 14 }) })] })), _jsx("button", { type: "submit", className: css.sendButton, "aria-label": pending ? t('sendingMessage') : t('sendMessage'), disabled: pending || draft.trim() === '', onMouseDown: event => {
176
+ // Keep the composer focused when the send control is clicked.
177
+ event.preventDefault();
178
+ inputRef.current?.focus({ preventScroll: true });
179
+ }, children: _jsx(IconSendOutline16, { size: 16 }) })] })] }), error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error })] });
144
180
  }
@@ -1,3 +1,3 @@
1
1
  import type { TeamConversationProps } from './slots.ts';
2
- export declare function TeamConversation({ t, useWorkspaces, navigation, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, selectThread, backToWorkspace, backToChannels }: TeamConversationProps): import("react").JSX.Element;
2
+ export declare function TeamConversation({ t, useWorkspaces, navigation, drafts, putAttachment, getAttachment, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, selectThread, backToWorkspace, backToChannels }: 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;AAKvD,wBAAgB,gBAAgB,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,EAAE,qBAAqB,+BAoBpR"}
1
+ {"version":3,"file":"TeamConversation.d.ts","sourceRoot":"","sources":["../../../src/client/TeamConversation.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAKvD,wBAAgB,gBAAgB,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,EAAE,qBAAqB,+BAoB1T"}
@@ -3,15 +3,15 @@ import { useSyncExternalStore } from 'react';
3
3
  import { TeamChannelPage } from "./TeamChannelPage.js";
4
4
  import { TeamThreadPage } from "./TeamThreadPage.js";
5
5
  import css from './conversation.module.css';
6
- export function TeamConversation({ t, useWorkspaces, navigation, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, selectThread, backToWorkspace, backToChannels }) {
6
+ export function TeamConversation({ t, useWorkspaces, navigation, drafts, putAttachment, getAttachment, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, selectThread, backToWorkspace, backToChannels }) {
7
7
  const navigationState = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot, navigation.getSnapshot);
8
8
  const workspaces = useWorkspaces(state => state.items);
9
9
  const current = workspaces.find(workspace => workspace.workspaceId === navigationState.workspaceId);
10
10
  if (current !== undefined && navigationState.taskRef !== undefined && navigationState.threadRef !== undefined) {
11
- return _jsx(TeamThreadPage, { workspaceId: current.workspaceId, taskRef: navigationState.taskRef, threadRef: navigationState.threadRef, backToWorkspace: backToWorkspace, ...(navigationState.channelRef === undefined ? {} : { channelRef: navigationState.channelRef }), ...(navigationState.taskNumber === undefined ? {} : { taskNumber: navigationState.taskNumber }), readThread: readThread, loadChannels: loadChannels, loadThreadHistory: loadThreadHistory, subscribeChanges: subscribeChanges, loadMembers: loadMembers, reply: reply, changeTask: changeTask, t: t }, navigationState.threadRef);
11
+ return _jsx(TeamThreadPage, { workspaceId: current.workspaceId, taskRef: navigationState.taskRef, threadRef: navigationState.threadRef, backToWorkspace: backToWorkspace, ...(navigationState.channelRef === undefined ? {} : { channelRef: navigationState.channelRef }), ...(navigationState.taskNumber === undefined ? {} : { taskNumber: navigationState.taskNumber }), drafts: drafts, getAttachment: getAttachment, readThread: readThread, loadChannels: loadChannels, loadThreadHistory: loadThreadHistory, subscribeChanges: subscribeChanges, loadMembers: loadMembers, reply: reply, changeTask: changeTask, t: t }, navigationState.threadRef);
12
12
  }
13
13
  if (current !== undefined && navigationState.channelRef !== undefined) {
14
- return _jsx(TeamChannelPage, { workspaceId: current.workspaceId, channelRef: navigationState.channelRef, loadChannels: loadChannels, subscribeChanges: subscribeChanges, loadMembers: loadMembers, sendMessage: sendMessage, joinChannel: joinChannel, removeChannelMember: removeChannelMember, selectThread: selectThread, backToChannels: backToChannels, t: t }, navigationState.channelRef);
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, backToChannels: backToChannels, t: t }, navigationState.channelRef);
15
15
  }
16
16
  const welcome = current === undefined
17
17
  ? { eyebrow: t('teamMode'), title: t('team'), body: t('empty') }
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { AgentTeamMemberId } from '@wowyuarm/dsh-agent-team/types';
2
+ import type { AgentTeamMemberId, AgentTeamMessageAttachment } from '@wowyuarm/dsh-agent-team/types';
3
+ import type { TeamConversationProps } from './slots.ts';
3
4
  export interface TeamMessageProps {
4
5
  readonly senderName: string;
5
6
  readonly memberId: AgentTeamMemberId;
@@ -12,8 +13,12 @@ export interface TeamMessageProps {
12
13
  readonly senderTitle?: string;
13
14
  /** Continuation of one same-sender run: suppress repeated identity chrome. */
14
15
  readonly grouped?: boolean;
16
+ readonly attachments?: readonly AgentTeamMessageAttachment[] | undefined;
17
+ /** Cache readback for thumbnails; absent on surfaces without the remotes. */
18
+ readonly loadAttachment?: TeamConversationProps['getAttachment'] | undefined;
19
+ readonly t?: TeamConversationProps['t'] | undefined;
15
20
  readonly children?: ReactNode;
16
21
  }
17
22
  /** One chat message row with identity chrome and sender-appropriate rendering. */
18
- export declare function TeamMessage({ senderName, memberId, human, body, occurredAt, mentionNames, senderTitle, grouped, children }: TeamMessageProps): import("react").JSX.Element;
23
+ export declare function TeamMessage({ senderName, memberId, human, body, occurredAt, mentionNames, senderTitle, grouped, attachments, loadAttachment, t, children }: TeamMessageProps): import("react").JSX.Element;
19
24
  //# sourceMappingURL=TeamMessage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TeamMessage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAIvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;IACpC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAC9B;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BAsC5I"}
1
+ {"version":3,"file":"TeamMessage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEzF,OAAO,KAAK,EAAgC,iBAAiB,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAA;AACjI,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAKvD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;IACpC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,0BAA0B,EAAE,GAAG,SAAS,CAAA;IACxE,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,CAAC,EAAE,qBAAqB,CAAC,eAAe,CAAC,GAAG,SAAS,CAAA;IAC5E,QAAQ,CAAC,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,GAAG,SAAS,CAAA;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAC9B;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BA8C5K"}
@@ -1,17 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment } from 'react';
3
- import { MarkdownText, MessageText } from '@deepseek-ai/dsh-client-ui-primitives';
4
- import { formatMessageTime, isPlainTextBody, memberHue, splitMentionNames } from "./team-formatters.js";
2
+ import { Fragment, useEffect, useState } from 'react';
3
+ import { MarkdownText, MessageText, Modal } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import { cachedAttachmentDataUrl, formatByteSize, loadAttachmentDataUrl } from "./attachment-preview.js";
5
+ import { formatMessageTime, isPlainTextBody, memberHue, splitMentionNames, stripAttachmentLines } from "./team-formatters.js";
5
6
  import css from './conversation.module.css';
6
7
  /** One chat message row with identity chrome and sender-appropriate rendering. */
7
- export function TeamMessage({ senderName, memberId, human, body, occurredAt, mentionNames, senderTitle, grouped, children }) {
8
+ export function TeamMessage({ senderName, memberId, human, body, occurredAt, mentionNames, senderTitle, grouped, attachments, loadAttachment, t, children }) {
8
9
  const avatarStyle = human ? undefined : { '--team-avatar-hue': memberHue(memberId) };
9
10
  // Literal bodies carry the chips inline — Human input always, and plain-
10
11
  // prose Agent bodies where literal rendering loses nothing. Rich Markdown
11
12
  // Agent bodies fall back to the trailing chip row: the Markdown primitive
12
13
  // renders block-level documents, so chips cannot interleave mid-paragraph.
13
- const inline = (human || isPlainTextBody(body)) && mentionNames !== undefined && mentionNames.length > 0
14
- ? splitMentionNames(body, mentionNames)
14
+ // The stored body carries machine-facing `[attachment] <path>` prompt lines;
15
+ // humans see the attachment strip rendered from the message metadata instead.
16
+ const displayBody = stripAttachmentLines(body) === '' ? body : stripAttachmentLines(body);
17
+ const inline = (human || isPlainTextBody(displayBody)) && mentionNames !== undefined && mentionNames.length > 0
18
+ ? splitMentionNames(displayBody, mentionNames)
15
19
  : undefined;
16
20
  const fallbackNames = inline === undefined ? (mentionNames ?? []) : inline.unmatched;
17
21
  return (_jsxs("article", { className: css.messageRow, "data-human": human || undefined, "data-grouped": grouped || undefined, children: [_jsx("div", { className: css.messageIdentity, style: avatarStyle, "aria-hidden": "true", children: senderName.replace('@', '').slice(0, 1).toUpperCase() }), _jsxs("div", { className: css.messageBody, children: [!grouped && (_jsxs("div", { className: css.nameRow, children: [_jsx("strong", { ...(senderTitle === undefined ? {} : { title: senderTitle }), children: senderName }), occurredAt !== undefined && _jsx("span", { className: css.messageTime, children: formatMessageTime(occurredAt) })] })), inline !== undefined
@@ -19,6 +23,28 @@ export function TeamMessage({ senderName, memberId, human, body, occurredAt, men
19
23
  ? _jsx("span", { className: css.mention, children: segment.text }, index)
20
24
  : _jsx(Fragment, { children: segment.text }, index)) })
21
25
  : human
22
- ? _jsx("div", { className: css.messageText, children: _jsx(MessageText, { text: body }) })
23
- : _jsx("div", { className: css.messageMarkdown, children: _jsx(MarkdownText, { text: body }) }), fallbackNames.length > 0 && (_jsx("div", { className: css.mentionsRow, children: fallbackNames.map(name => _jsxs("span", { className: css.mention, children: ["@", name] }, name)) })), children] })] }));
26
+ ? _jsx("div", { className: css.messageText, children: _jsx(MessageText, { text: displayBody }) })
27
+ : _jsx("div", { className: css.messageMarkdown, children: _jsx(MarkdownText, { text: displayBody }) }), fallbackNames.length > 0 && (_jsx("div", { className: css.mentionsRow, children: fallbackNames.map(name => _jsxs("span", { className: css.mention, children: ["@", name] }, name)) })), attachments !== undefined && attachments.length > 0 && _jsx(TeamAttachmentStrip, { attachments: attachments, ...(loadAttachment === undefined ? {} : { loadAttachment }), ...(t === undefined ? {} : { t }) }), children] })] }));
28
+ }
29
+ /** One message's attachment strip: image thumbnails with a large view, or name chips when bytes are gone. */
30
+ function TeamAttachmentStrip({ attachments, loadAttachment, t }) {
31
+ const [zoomed, setZoomed] = useState();
32
+ return _jsxs("div", { className: css.attachmentStrip, children: [attachments.map(attachment => _jsx(TeamAttachment, { attachment: attachment, ...(loadAttachment === undefined ? {} : { loadAttachment }), ...(t === undefined ? {} : { t }), onZoom: setZoomed }, attachment.attachmentId)), zoomed !== undefined && _jsx(Modal, { open: true, ...(css.attachmentModal === undefined ? {} : { className: css.attachmentModal }), title: zoomed.name, onClose: () => { setZoomed(undefined); }, children: _jsx("img", { className: css.attachmentZoom, src: cachedAttachmentDataUrl(zoomed.attachmentId) ?? undefined, alt: zoomed.name }) })] });
33
+ }
34
+ function TeamAttachment({ attachment, loadAttachment, t, onZoom }) {
35
+ const wantsPreview = loadAttachment !== undefined && attachment.mediaType.startsWith('image/');
36
+ const [dataUrl, setDataUrl] = useState(wantsPreview ? cachedAttachmentDataUrl(attachment.attachmentId) : null);
37
+ useEffect(() => {
38
+ if (!wantsPreview || dataUrl !== undefined)
39
+ return;
40
+ let mounted = true;
41
+ void loadAttachmentDataUrl(loadAttachment, attachment).then(url => { if (mounted)
42
+ setDataUrl(url); });
43
+ return () => { mounted = false; };
44
+ }, [wantsPreview, dataUrl, loadAttachment, attachment]);
45
+ const expired = t?.('attachmentExpired') ?? 'File no longer cached';
46
+ if (wantsPreview && dataUrl !== null) {
47
+ return _jsx("button", { type: "button", className: css.attachmentThumb, "aria-label": t?.('viewImage', { name: attachment.name }) ?? attachment.name, title: attachment.name, onClick: () => { onZoom(attachment); }, children: _jsx("img", { src: dataUrl, alt: attachment.name }) });
48
+ }
49
+ return _jsxs("span", { className: css.attachmentChip, title: wantsPreview ? expired : `${attachment.name} · ${formatByteSize(attachment.byteSize)}`, children: [_jsx("span", { className: css.attachmentChipName, children: attachment.name }), _jsx("span", { className: css.attachmentChipSize, children: wantsPreview ? expired : formatByteSize(attachment.byteSize) })] });
24
50
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Explicit boundary between two same-sender Messages of one run separated by
3
+ * a real waiting gap: the hairline restores the block boundary that grouping
4
+ * removed, and the label below it restores the instant that the suppressed
5
+ * identity chrome would have shown.
6
+ */
7
+ export declare function TeamRunDivider({ occurredAt }: {
8
+ readonly occurredAt: string;
9
+ }): import("react").JSX.Element;
10
+ //# sourceMappingURL=TeamRunDivider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TeamRunDivider.d.ts","sourceRoot":"","sources":["../../../src/client/TeamRunDivider.tsx"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,+BAM7E"}
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { formatMessageTime } from "./team-formatters.js";
3
+ import css from './conversation.module.css';
4
+ /**
5
+ * Explicit boundary between two same-sender Messages of one run separated by
6
+ * a real waiting gap: the hairline restores the block boundary that grouping
7
+ * removed, and the label below it restores the instant that the suppressed
8
+ * identity chrome would have shown.
9
+ */
10
+ export function TeamRunDivider({ occurredAt }) {
11
+ return (_jsx("div", { className: css.runDivider, role: "separator", children: _jsx("time", { dateTime: occurredAt, children: formatMessageTime(occurredAt) }) }));
12
+ }
@@ -1,6 +1,7 @@
1
1
  import type { AgentTeamChannelRef, AgentTeamTaskRef, AgentTeamThreadRef } from '@wowyuarm/dsh-agent-team/types';
2
2
  import type { WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client';
3
3
  import type { TeamConversationProps } from './slots.ts';
4
+ import type { TeamDraftStore } from './drafts.ts';
4
5
  interface TeamThreadPageProps {
5
6
  readonly workspaceId: WorkspaceId;
6
7
  readonly channelRef?: AgentTeamChannelRef;
@@ -13,6 +14,8 @@ interface TeamThreadPageProps {
13
14
  readonly loadThreadHistory: TeamConversationProps['loadThreadHistory'];
14
15
  readonly subscribeChanges: TeamConversationProps['subscribeChanges'];
15
16
  readonly loadMembers: TeamConversationProps['loadMembers'];
17
+ readonly drafts: TeamDraftStore;
18
+ readonly getAttachment: TeamConversationProps['getAttachment'];
16
19
  readonly reply: TeamConversationProps['reply'];
17
20
  readonly changeTask: TeamConversationProps['changeTask'];
18
21
  readonly t: TeamConversationProps['t'];
@@ -1 +1 @@
1
- {"version":3,"file":"TeamThreadPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamThreadPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,mBAAmB,EAInB,gBAAgB,EAGhB,kBAAkB,EAEnB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAEzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAUvD,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,mBAAmB,CAAA;IACzC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAA;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAA;IACxD,QAAQ,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAA;IACtE,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9C,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAA;IACxD,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AA8BD,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,+BA8dxD"}
1
+ {"version":3,"file":"TeamThreadPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamThreadPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,mBAAmB,EAInB,gBAAgB,EAGhB,kBAAkB,EAEnB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAEzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACvD,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,aAAa,CAAA;AAW/D,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,mBAAmB,CAAA;IACzC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAA;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAA;IACxD,QAAQ,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAA;IACtE,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9C,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAA;IACxD,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AA8BD,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,+BAgfxD"}
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { Fragment, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import { Button, DisclosureRow, IconChevronLeftOutline14, IconChecklistOutline14, 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";
10
11
  import css from './conversation.module.css';
11
12
  import threadCss from './thread.module.css';
@@ -29,7 +30,7 @@ function readMeta(facts) {
29
30
  return new Map(facts.map(fact => [factKey(fact.fact), fact]));
30
31
  }
31
32
  export function TeamThreadPage(props) {
32
- const { workspaceId, channelRef, taskRef, threadRef, taskNumber, backToWorkspace, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, reply, changeTask, t, } = props;
33
+ const { workspaceId, channelRef, taskRef, threadRef, taskNumber, backToWorkspace, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, drafts, getAttachment, reply, changeTask, t, } = props;
33
34
  const [projection, setProjection] = useState();
34
35
  const [channelView, setChannelView] = useState();
35
36
  const [members, setMembers] = useState([]);
@@ -40,8 +41,10 @@ export function TeamThreadPage(props) {
40
41
  const [historyHasMore, setHistoryHasMore] = useState(false);
41
42
  const [remainingUnreadCount, setRemainingUnreadCount] = useState(0);
42
43
  const [newFactsCount, setNewFactsCount] = useState(0);
43
- const [draft, setDraft] = useState('');
44
- const [recipients, setRecipients] = useState(new Set());
44
+ // The reply draft lives in the keyed draft cache: view switches unmount
45
+ // this page, and a refresh must not cost the half-written message either.
46
+ const draftKey = `thread:${threadRef}`;
47
+ const { draft, recipients } = useSyncExternalStore(drafts.subscribe, () => drafts.getSnapshot(draftKey));
45
48
  const [claimsOpen, setClaimsOpen] = useState(false);
46
49
  const [replyRequestId, setReplyRequestId] = useState();
47
50
  const [confirmation, setConfirmation] = useState();
@@ -295,7 +298,7 @@ export function TeamThreadPage(props) {
295
298
  if (fact.kind === 'message') {
296
299
  const sender = memberName(fact.message.sender);
297
300
  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));
301
+ 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), grouped: grouped, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }) }, factKey(fact));
299
302
  }
300
303
  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
304
  };
@@ -305,8 +308,13 @@ export function TeamThreadPage(props) {
305
308
  let run = [];
306
309
  let lastDay;
307
310
  const flushRun = () => {
308
- if (run.length > 0)
309
- nodes.push(_jsx("div", { className: css.messageRun, children: run.map((entry, entryIndex) => renderFact(entry, entryIndex > 0)) }, `run-${factKey(run[0])}`));
311
+ if (run.length > 0) {
312
+ nodes.push(_jsx("div", { className: css.messageRun, children: run.map((entry, entryIndex) => {
313
+ const previous = entryIndex > 0 ? run[entryIndex - 1] : undefined;
314
+ const turnGap = entry.kind === 'message' && isRunGap(previous?.kind === 'message' ? previous.message.occurredAt : undefined, entry.message.occurredAt);
315
+ return _jsxs(Fragment, { children: [turnGap && _jsx(TeamRunDivider, { occurredAt: entry.message.occurredAt }), renderFact(entry, entryIndex > 0)] }, factKey(entry));
316
+ }) }, `run-${factKey(run[0])}`));
317
+ }
310
318
  run = [];
311
319
  };
312
320
  facts.forEach((fact, index) => {
@@ -405,8 +413,7 @@ export function TeamThreadPage(props) {
405
413
  return merged;
406
414
  });
407
415
  setProjection(current => current === undefined ? current : { ...current, task: committed.task, thread: committed.thread });
408
- setDraft('');
409
- setRecipients(new Set());
416
+ drafts.clear(draftKey);
410
417
  setReplyRequestId(undefined);
411
418
  setConfirmation(undefined);
412
419
  setStatusMessage(undefined);
@@ -460,5 +467,5 @@ export function TeamThreadPage(props) {
460
467
  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
468
  })] }) }) })] }), _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
469
  ? _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 => { setDraft(next); setConfirmation(undefined); setReplyRequestId(undefined); setStatusMessage(undefined); }, onRecipientsChange: next => { setRecipients(next); setConfirmation(undefined); setReplyRequestId(undefined); setStatusMessage(undefined); }, onSubmit: () => { void sendReply(); }, t: t })) : _jsx("div", {})] });
470
+ : _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(); }, t: t }, draftKey)) : _jsx("div", {})] });
464
471
  }
@@ -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, restartMember, 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,+BA2DhT"}
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,aAAa,EAAE,WAAW,EAAE,mBAAmB,EAAE,UAAU,EAAE,iBAAiB,EAAE,EAAE,gBAAgB,+BA4D9U"}
@@ -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, restartMember, 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, otherwise the browsed Workspace's overview. The selected row
16
- // keeps its quiet folder tint (data-selected) in both cases.
17
- const overviewIsCurrent = navigationState.channelRef === undefined;
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, restartMember: restartMember, 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"}