@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.
Files changed (91) hide show
  1. package/README.md +14 -2
  2. package/README.zh.md +10 -2
  3. package/package.json +32 -24
  4. package/packages/agent-team/lib/attachments.js +157 -0
  5. package/packages/agent-team/lib/index.js +280 -22
  6. package/packages/agent-team/lib/ledger.js +98 -22
  7. package/packages/agent-team/lib/recovery.js +87 -0
  8. package/packages/agent-team/lib/spec.js +27 -5
  9. package/packages/agent-team/lib/typert.host.js +313 -26
  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 +234 -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 +68 -6
  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 +94 -3
  23. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  24. package/packages/agent-team/preset/team-member/agent.cordis.yml +2 -2
  25. package/packages/client-agent-team/README.md +4 -3
  26. package/packages/client-agent-team/README.zh.md +4 -3
  27. package/packages/client-agent-team/lib/client.js +1791 -322
  28. package/packages/client-agent-team/lib/client.js.map +1 -1
  29. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +5 -2
  30. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  31. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +108 -59
  32. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +7 -1
  33. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  34. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +80 -19
  35. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +38 -19
  37. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
  38. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamComposer.js +61 -4
  40. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  42. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  43. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +12 -2
  44. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamMessage.js +152 -14
  46. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
  47. package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
  48. package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
  49. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +7 -0
  50. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  51. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +93 -16
  52. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  53. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  54. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +6 -5
  55. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
  56. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
  57. package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
  58. package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
  59. package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
  60. package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
  61. package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
  62. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  63. package/packages/client-agent-team/lib/types/client/index.js +50 -10
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts +34 -0
  65. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/locales.js +34 -0
  67. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
  68. package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
  70. package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
  71. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
  73. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +35 -0
  74. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -0
  75. package/packages/client-agent-team/lib/types/client/sidebar-drag.js +70 -0
  76. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +27 -0
  77. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -0
  78. package/packages/client-agent-team/lib/types/client/sidebar-order.js +170 -0
  79. package/packages/client-agent-team/lib/types/client/slots.d.ts +18 -2
  80. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +20 -0
  82. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -0
  83. package/packages/client-agent-team/lib/types/client/task-refs.js +58 -0
  84. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +15 -0
  85. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  86. package/packages/client-agent-team/lib/types/client/team-formatters.js +30 -2
  87. package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
  88. package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
  89. package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
  90. package/packages/tool-agent-team/lib/index.js +10 -6
  91. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
@@ -1,4 +1,4 @@
1
- import type { AgentTeamAddMemberRequest } from '@wowyuarm/dsh-agent-team/types';
1
+ import type { AgentTeamAddMemberRequest, AgentTeamClientMemberStatus } from '@wowyuarm/dsh-agent-team/types';
2
2
  import type { WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client';
3
3
  import type { TeamSidebarProps } from './slots.ts';
4
4
  interface TeamAgentsPanelProps {
@@ -8,13 +8,16 @@ interface TeamAgentsPanelProps {
8
8
  readonly loadChannels: TeamSidebarProps['loadChannels'];
9
9
  readonly addMember: TeamSidebarProps['addMember'];
10
10
  readonly updateMember: TeamSidebarProps['updateMember'];
11
+ readonly recoverMember: TeamSidebarProps['recoverMember'];
11
12
  readonly joinChannel: TeamSidebarProps['joinChannel'];
12
13
  readonly removeChannelMember: TeamSidebarProps['removeChannelMember'];
13
14
  readonly loadModels: TeamSidebarProps['loadModels'];
15
+ /** The Member Session currently embedded in the conversation seat, if any. */
16
+ readonly memberSessionId?: AgentTeamClientMemberStatus['member']['sessionId'];
14
17
  readonly openMemberSession: TeamSidebarProps['openMemberSession'];
15
18
  readonly onCreatingChange: (request: AgentTeamAddMemberRequest, creating: boolean) => void;
16
19
  readonly t: TeamSidebarProps['t'];
17
20
  }
18
- export declare function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, loadChannels, addMember, updateMember, joinChannel, removeChannelMember, loadModels, openMemberSession, onCreatingChange, t }: TeamAgentsPanelProps): import("react").JSX.Element;
21
+ export declare function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, loadChannels, addMember, updateMember, recoverMember, joinChannel, removeChannelMember, loadModels, memberSessionId, openMemberSession, onCreatingChange, t }: TeamAgentsPanelProps): import("react").JSX.Element;
19
22
  export {};
20
23
  //# sourceMappingURL=TeamAgentsPanel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TeamAgentsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamAgentsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EAM1B,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAGzE,OAAO,KAAK,EAA0B,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAQ1E,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,kBAAkB,CAAC,CAAA;IAC/D,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACjD,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,qBAAqB,CAAC,CAAA;IACrE,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAA;IACnD,QAAQ,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,mBAAmB,CAAC,CAAA;IACjE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,OAAO,EAAE,yBAAyB,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC1F,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CAClC;AAED,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,mBAAmB,EAAE,UAAU,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC,EAAE,EAAE,oBAAoB,+BA8JhO"}
1
+ {"version":3,"file":"TeamAgentsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamAgentsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,2BAA2B,EAM5B,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAGzE,OAAO,KAAK,EAAiD,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAWjG,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,kBAAkB,CAAC,CAAA;IAC/D,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAA;IACjD,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,qBAAqB,CAAC,CAAA;IACrE,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAA;IACnD,8EAA8E;IAC9E,QAAQ,CAAC,eAAe,CAAC,EAAE,2BAA2B,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,CAAA;IAC7E,QAAQ,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,mBAAmB,CAAC,CAAA;IACjE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,OAAO,EAAE,yBAAyB,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC1F,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CAClC;AAED,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,mBAAmB,EAAE,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC,EAAE,EAAE,oBAAoB,+BAiLhQ"}
@@ -1,13 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from 'react';
3
- import { Button, IconChevronDownOutline14, IconEditOutline16, IconPlusOutline16, Input, Menu, Modal, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Button, IconChevronDownOutline14, IconEditOutline16, IconPlayOutline16, IconPlusOutline16, Input, Menu, Modal, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { TeamMemberAvatar } from "./TeamMemberAvatar.js";
5
+ import { SortableRow, useSidebarRowDrag } from "./sidebar-drag.js";
6
+ import { moveSidebarItem, useSidebarOrder } from "./sidebar-order.js";
5
7
  import { TeamRowMenu } from "./TeamRowMenu.js";
6
8
  import { TeamSidebarSection } from "./TeamSidebarSection.js";
9
+ import { MultiMenuField } from "./multi-menu-field.js";
7
10
  import { useChannelMembership } from "./team-membership.js";
8
11
  import createCss from './create.module.css';
9
12
  import css from './sidebar.module.css';
10
- export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, loadChannels, addMember, updateMember, joinChannel, removeChannelMember, loadModels, openMemberSession, onCreatingChange, t }) {
13
+ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, loadChannels, addMember, updateMember, recoverMember, joinChannel, removeChannelMember, loadModels, memberSessionId, openMemberSession, onCreatingChange, t }) {
11
14
  const [members, setMembers] = useState([]);
12
15
  const [channels, setChannels] = useState([]);
13
16
  const [channelRefs, setChannelRefs] = useState([]);
@@ -16,9 +19,22 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, lo
16
19
  const [formOpen, setFormOpen] = useState(false);
17
20
  const [handle, setHandle] = useState('');
18
21
  const [description, setDescription] = useState('');
22
+ const [model, setModel] = useState(undefined);
19
23
  const [creating, setCreating] = useState(false);
20
24
  const [retryRequest, setRetryRequest] = useState();
21
25
  const triggerRef = useRef(null);
26
+ // Same presentation-preference ordering as the Channels list; the drag
27
+ // commits through one shared mutation.
28
+ const agentRefs = useMemo(() => members.map(status => status.member.memberId), [members]);
29
+ const orderedAgentRefs = useSidebarOrder(workspaceId, 'agents', agentRefs);
30
+ const orderedMembers = useMemo(() => {
31
+ const byId = new Map(members.map(status => [status.member.memberId, status]));
32
+ return orderedAgentRefs.map(memberId => byId.get(memberId)).filter(status => status !== undefined);
33
+ }, [orderedAgentRefs, members]);
34
+ const applyMove = (movedRef, targetRef, marker) => {
35
+ void moveSidebarItem(workspaceId, 'agents', orderedAgentRefs, movedRef, targetRef, marker);
36
+ };
37
+ const drag = useSidebarRowDrag({ refs: orderedAgentRefs, onCommit: applyMove });
22
38
  const refresh = useCallback(async () => {
23
39
  setLoading(true);
24
40
  const result = await loadMembers({ workspaceId });
@@ -68,6 +84,7 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, lo
68
84
  });
69
85
  setHandle('');
70
86
  setDescription('');
87
+ setModel(undefined);
71
88
  setChannelRefs([]);
72
89
  setFormOpen(false);
73
90
  if (result.value.status.presence === 'unavailable') {
@@ -95,12 +112,11 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, lo
95
112
  event.preventDefault();
96
113
  const normalizedHandle = handle.trim();
97
114
  const normalizedDescription = description.trim();
98
- if (normalizedHandle.length === 0 || normalizedDescription.length === 0 || creating)
99
- return;
100
- if (channelRefs.length === 0)
115
+ if (normalizedHandle.length === 0 || creating)
101
116
  return;
102
117
  const sameRequest = retryRequest !== undefined && retryRequest.workspaceId === workspaceId
103
118
  && retryRequest.handle === normalizedHandle && retryRequest.description === normalizedDescription
119
+ && sameModel(retryRequest.model, model)
104
120
  && retryRequest.channelRefs.length === channelRefs.length && retryRequest.channelRefs.every(ref => channelRefs.includes(ref));
105
121
  void provision(sameRequest ? retryRequest : {
106
122
  requestId: crypto.randomUUID(),
@@ -109,47 +125,55 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, lo
109
125
  description: normalizedDescription,
110
126
  presetId: 'team-member',
111
127
  channelRefs,
128
+ ...(model === undefined ? {} : { model }),
112
129
  });
113
130
  };
114
- return (_jsxs("div", { className: css.panel, children: [_jsx(Modal, { open: formOpen, onClose: closeForm, title: t('addAgent'), closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: creating, onClick: closeForm, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-agent-create-form", variant: "primary", disabled: creating || handle.trim().length === 0 || description.trim().length === 0 || channelRefs.length === 0, children: creating ? t('creatingAgent') : t('createAgent') })] }), children: _jsxs("form", { id: "team-agent-create-form", className: createCss.form, onSubmit: submit, children: [_jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('agentName') }), _jsx(Input, { className: createCss.input, value: handle, onChange: event => { setHandle(event.target.value); setRetryRequest(undefined); }, disabled: creating, autoFocus: true })] }), _jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('agentDescription') }), _jsx(Input, { className: createCss.input, value: description, onChange: event => { setDescription(event.target.value); setRetryRequest(undefined); }, disabled: creating })] }), _jsxs("fieldset", { className: createCss.field, children: [_jsx("legend", { children: t('initialChannels') }), channels.length === 0 ? _jsx("span", { children: t('noChannelsForAgent') }) : channels.map(channel => _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: channelRefs.includes(channel.channelRef), disabled: creating, onChange: event => {
115
- setChannelRefs(current => event.target.checked ? [...current, channel.channelRef] : current.filter(ref => ref !== channel.channelRef));
116
- setRetryRequest(undefined);
117
- } }), " ", channel.name] }, channel.channelRef))] }), formOpen && error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('agents'), actions: (_jsx(Tooltip, { label: t('addAgent'), delayMs: 500, children: _jsx("button", { ref: triggerRef, type: "button", className: css.iconButton, "aria-label": t('addAgent'), onClick: () => { setError(undefined); setFormOpen(true); }, children: _jsx(IconPlusOutline16, { size: 14 }) }) })), children: [loading && members.length === 0 && _jsx("p", { className: css.emptyState, children: t('loadingAgents') }), !loading && members.length === 0 && _jsx("p", { className: css.emptyState, children: t('emptyAgents') }), _jsx("div", { className: css.agentList, children: members.map(status => (_jsx(AgentRow, { status: status, channels: channels, loadChannels: loadChannels, updateMember: updateMember, joinChannel: joinChannel, removeChannelMember: removeChannelMember, loadModels: loadModels, openMemberSession: openMemberSession, onUpdated: () => { void refresh(); }, t: t }, status.member.memberId))) })] }), !formOpen && error !== undefined && (_jsxs("div", { className: css.retryError, role: "alert", children: [_jsx("span", { children: error }), retryRequest !== undefined && _jsx("button", { type: "button", className: css.textButton, disabled: creating, onClick: () => { setFormOpen(true); }, children: t('retry') })] }))] }));
131
+ return (_jsxs("div", { className: css.panel, children: [_jsx(Modal, { open: formOpen, onClose: closeForm, title: t('addAgent'), closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: creating, onClick: closeForm, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-agent-create-form", variant: "primary", disabled: creating || handle.trim().length === 0, children: creating ? t('creatingAgent') : t('createAgent') })] }), children: _jsxs("form", { id: "team-agent-create-form", className: createCss.form, onSubmit: submit, children: [_jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('agentName') }), _jsx(Input, { className: createCss.input, value: handle, onChange: event => { setHandle(event.target.value); setRetryRequest(undefined); }, disabled: creating, autoFocus: true })] }), _jsxs("label", { className: createCss.field, children: [_jsxs("span", { children: [t('agentDescription'), t('optionalSuffix')] }), _jsx(Input, { className: createCss.input, value: description, placeholder: t('agentDescriptionPlaceholder'), onChange: event => { setDescription(event.target.value); setRetryRequest(undefined); }, disabled: creating })] }), _jsx(ModelPickerField, { model: model, onModelChange: choice => { setModel(choice); setRetryRequest(undefined); }, loadModels: loadModels, disabled: creating, t: t }), _jsx(MultiMenuField, { label: `${t('initialChannels')}${t('optionalSuffix')}`, disabled: creating, options: channels.map(channel => ({ id: channel.channelRef, label: channel.name })), selected: channelRefs, onToggle: ref => {
132
+ setChannelRefs(current => current.includes(ref) ? current.filter(item => item !== ref) : [...current, ref]);
133
+ setRetryRequest(undefined);
134
+ }, emptyText: t('noChannelsForAgent'), triggerEmptyLabel: t('channelsPickerEmpty'), formatCount: count => t('channelsPickerCount', { count }) }), formOpen && error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('agents'), actions: (_jsx(Tooltip, { label: t('addAgent'), delayMs: 500, children: _jsx("button", { ref: triggerRef, type: "button", className: css.iconButton, "aria-label": t('addAgent'), onClick: () => { setError(undefined); setFormOpen(true); }, children: _jsx(IconPlusOutline16, { size: 14 }) }) })), children: [loading && members.length === 0 && _jsx("p", { className: css.emptyState, children: t('loadingAgents') }), !loading && members.length === 0 && _jsx("p", { className: css.emptyState, children: t('emptyAgents') }), _jsx("div", { className: css.agentList, children: orderedMembers.map(status => (_jsx(SortableRow, { drag: drag, orderKey: status.member.memberId, children: _jsx(AgentRow, { status: status, ...(memberSessionId === undefined ? {} : { current: status.member.sessionId === memberSessionId }), channels: channels, loadChannels: loadChannels, updateMember: updateMember, recoverMember: recoverMember, joinChannel: joinChannel, removeChannelMember: removeChannelMember, loadModels: loadModels, openMemberSession: openMemberSession, onUpdated: () => { void refresh(); }, t: t }) }, status.member.memberId))) })] }), !formOpen && error !== undefined && (_jsxs("div", { className: css.retryError, role: "alert", children: [_jsx("span", { children: error }), retryRequest !== undefined && _jsx("button", { type: "button", className: css.textButton, disabled: creating, onClick: () => { setFormOpen(true); }, children: t('retry') })] }))] }));
118
135
  }
119
136
  /**
120
137
  * One sidebar Agent row: the select button opens the Member's own Session
121
138
  * conversation page, the avatar carries identity plus the presence badge, and
122
139
  * the row menu opens the editor.
123
140
  */
124
- function AgentRow({ status, channels, loadChannels, updateMember, joinChannel, removeChannelMember, loadModels, openMemberSession, onUpdated, t }) {
141
+ function AgentRow({ status, current, channels, loadChannels, updateMember, recoverMember, joinChannel, removeChannelMember, loadModels, openMemberSession, onUpdated, t }) {
125
142
  const [menuOpen, setMenuOpen] = useState(false);
126
143
  const [editing, setEditing] = useState(false);
127
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.agentRow, "data-menu-open": menuOpen || undefined, children: [_jsxs("button", { type: "button", className: css.agentSelect, "aria-label": t('openAgentSession', { name: status.member.handle }), onClick: () => { openMemberSession(status.member.sessionId); }, children: [_jsx(TeamMemberAvatar, { status: status, t: t }), _jsxs("span", { className: css.agentCopy, children: [_jsx("strong", { children: status.member.handle }), _jsx("small", { children: status.member.description })] })] }), _jsx("span", { className: css.rowMenu, children: _jsx(TeamRowMenu, { label: t('actionsAgent', { name: status.member.handle }), items: [{ id: 'edit', label: t('editAgent'), icon: _jsx(IconEditOutline16, {}) }], onSelect: () => { setEditing(true); }, onOpenChange: setMenuOpen }) })] }), editing && (_jsx(AgentEditorDialog, { status: status, channels: channels, loadChannels: loadChannels, updateMember: updateMember, joinChannel: joinChannel, removeChannelMember: removeChannelMember, loadModels: loadModels, onCommitted: onUpdated, onClose: () => { setEditing(false); }, t: t }))] }));
144
+ const resume = async () => {
145
+ await recoverMember({
146
+ requestId: crypto.randomUUID(),
147
+ workspaceId: status.member.workspaceId,
148
+ memberId: status.member.memberId,
149
+ });
150
+ await onUpdated();
151
+ };
152
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.agentRow, "data-menu-open": menuOpen || undefined, children: [_jsxs("button", { type: "button", className: css.agentSelect, "aria-label": t('openAgentSession', { name: status.member.handle }), "aria-current": current ? 'page' : undefined, disabled: status.availability !== 'active', onClick: () => { openMemberSession(status.member.sessionId); }, children: [_jsx(TeamMemberAvatar, { status: status, t: t }), _jsxs("span", { className: css.agentCopy, children: [_jsx("strong", { children: status.member.handle }), _jsx("small", { children: status.member.description })] })] }), _jsx("span", { className: css.rowMenu, children: _jsx(TeamRowMenu, { label: t('actionsAgent', { name: status.member.handle }), items: [
153
+ { id: 'edit', label: t('editAgent'), icon: _jsx(IconEditOutline16, {}) },
154
+ ...(status.presence === 'error' ? [{ id: 'resume', label: t('resumeAgent'), icon: _jsx(IconPlayOutline16, {}) }] : []),
155
+ ], onSelect: (id) => {
156
+ if (id === 'resume')
157
+ void resume();
158
+ else
159
+ setEditing(true);
160
+ }, onOpenChange: setMenuOpen }) })] }), editing && (_jsx(AgentEditorDialog, { status: status, channels: channels, loadChannels: loadChannels, updateMember: updateMember, joinChannel: joinChannel, removeChannelMember: removeChannelMember, loadModels: loadModels, onCommitted: onUpdated, onClose: () => { setEditing(false); }, t: t }))] }));
128
161
  }
129
162
  /** Model option key inside one editor; opaque and resolved against the loaded groups. */
130
163
  function modelKey(provider, model) {
131
164
  return `${provider}\u0000${model}`;
132
165
  }
133
166
  /**
134
- * Agent editor: handle, description, and per-Member model selection commit
135
- * through one durable update; Channel membership below keeps its own
136
- * immediate add/remove flow.
167
+ * Shared provider/model dropdown for the create and edit forms. The option
168
+ * list rides the shared Menu primitive (one leading "follow Host default"
169
+ * row, then non-selectable provider headings) with a capped, internally
170
+ * scrolling card so growing model catalogs cannot stretch the dialog.
137
171
  */
138
- function AgentEditorDialog({ status, channels, loadChannels, updateMember, joinChannel, removeChannelMember, loadModels, onCommitted, onClose, t }) {
139
- const memberId = status.member.memberId;
140
- const [handle, setHandle] = useState(status.member.handle);
141
- const [description, setDescription] = useState(status.member.description);
142
- const [model, setModel] = useState(status.member.model === undefined ? undefined : [status.member.model.provider, status.member.model.model]);
172
+ function ModelPickerField({ model, onModelChange, loadModels, disabled, t }) {
143
173
  const [groups, setGroups] = useState();
144
174
  const [modelsError, setModelsError] = useState();
145
- const [modelOpen, setModelOpen] = useState(false);
146
- const [saving, setSaving] = useState(false);
147
- const [error, setError] = useState();
148
- const pendingRequest = useRef();
149
- // Membership comes from a fresh projection at open time so stale checkbox
150
- // state never flips a committed fact backwards.
151
- const [memberships, setMemberships] = useState();
152
- const [loadError, setLoadError] = useState();
175
+ const [open, setModelOpen] = useState(false);
176
+ const [effortOpen, setEffortOpen] = useState(false);
153
177
  useEffect(() => {
154
178
  let mounted = true;
155
179
  void loadModels().then(result => {
@@ -165,6 +189,56 @@ function AgentEditorDialog({ status, channels, loadChannels, updateMember, joinC
165
189
  });
166
190
  return () => { mounted = false; };
167
191
  }, [loadModels]);
192
+ const items = [{ id: '', label: t('modelFollowDefault') }];
193
+ const byKey = new Map();
194
+ for (const group of groups ?? []) {
195
+ items.push({ type: 'label', id: `model-group:${group.id}`, text: group.name });
196
+ for (const entry of group.models) {
197
+ const key = modelKey(group.id, entry.id);
198
+ byKey.set(key, { provider: group.id, id: entry.id, name: entry.name, efforts: entry.reasoning?.efforts ?? [] });
199
+ items.push({ id: key, label: entry.name });
200
+ }
201
+ }
202
+ const selectedModelKey = model === undefined ? '' : modelKey(model.provider, model.model);
203
+ const triggerLabel = model === undefined
204
+ ? t('modelFollowDefault')
205
+ : byKey.get(selectedModelKey)?.name ?? `${model.provider} / ${model.model}`;
206
+ // The effort sub-row only makes sense for a pinned model with adapter-exposed
207
+ // efforts; following the Host default inherits the operator's whole selection.
208
+ const efforts = model === undefined ? [] : byKey.get(selectedModelKey)?.efforts ?? [];
209
+ const effortItems = [{ id: '', label: t('effortFollowDefault') }, ...efforts.map(effort => ({ id: effort.id, label: effort.name }))];
210
+ const selectedEffort = model?.reasoningEffort ?? '';
211
+ const effortTriggerLabel = model === undefined || selectedEffort === ''
212
+ ? t('effortFollowDefault')
213
+ : efforts.find(effort => effort.id === selectedEffort)?.name ?? selectedEffort;
214
+ return _jsxs("div", { className: createCss.field, children: [_jsx("span", { children: t('memberModel') }), groups === undefined && modelsError === undefined && _jsx("small", { className: css.editHint, children: t('modelsLoading') }), modelsError !== undefined && _jsx("small", { className: css.editHint, children: t('modelsLoadFailed', { message: modelsError }) }), groups !== undefined && (_jsx(Menu, { open: open, portal: true, className: createCss.menuCap, items: items, selectedId: selectedModelKey, onSelect: key => {
215
+ setModelOpen(false);
216
+ const choice = byKey.get(key);
217
+ onModelChange(choice === undefined ? undefined : { provider: choice.provider, model: choice.id });
218
+ }, onClose: () => { setModelOpen(false); }, anchor: _jsxs("button", { type: "button", className: createCss.selectTrigger, "aria-label": t('memberModel'), "aria-haspopup": "listbox", "aria-expanded": open, disabled: disabled, onClick: () => { setModelOpen(value => !value); }, children: [_jsx("span", { className: createCss.selectValue, children: triggerLabel }), _jsx("span", { className: `${createCss.chevron} ${open ? createCss.chevronOpen : ''}`, "aria-hidden": true, children: _jsx(IconChevronDownOutline14, {}) })] }) })), model !== undefined && efforts.length > 0 && (_jsx(Menu, { open: effortOpen, portal: true, className: createCss.menuCap, items: effortItems, selectedId: selectedEffort, onSelect: key => {
219
+ setEffortOpen(false);
220
+ onModelChange(key === ''
221
+ ? { provider: model.provider, model: model.model }
222
+ : { provider: model.provider, model: model.model, reasoningEffort: key });
223
+ }, onClose: () => { setEffortOpen(false); }, anchor: _jsxs("button", { type: "button", className: createCss.selectTrigger, "aria-label": t('reasoningEffort'), "aria-haspopup": "listbox", "aria-expanded": effortOpen, disabled: disabled, onClick: () => { setEffortOpen(value => !value); }, children: [_jsx("span", { className: createCss.selectValue, children: `${t('reasoningEffort')} · ${effortTriggerLabel}` }), _jsx("span", { className: `${createCss.chevron} ${effortOpen ? createCss.chevronOpen : ''}`, "aria-hidden": true, children: _jsx(IconChevronDownOutline14, {}) })] }) }))] });
224
+ }
225
+ /**
226
+ * Agent editor: handle, description, and per-Member model selection commit
227
+ * through one durable update; Channel membership below keeps its own
228
+ * immediate add/remove flow.
229
+ */
230
+ function AgentEditorDialog({ status, channels, loadChannels, updateMember, joinChannel, removeChannelMember, loadModels, onCommitted, onClose, t }) {
231
+ const memberId = status.member.memberId;
232
+ const [handle, setHandle] = useState(status.member.handle);
233
+ const [description, setDescription] = useState(status.member.description);
234
+ const [model, setModel] = useState(status.member.model);
235
+ const [saving, setSaving] = useState(false);
236
+ const [error, setError] = useState();
237
+ const pendingRequest = useRef();
238
+ // Membership comes from a fresh projection at open time so stale checkbox
239
+ // state never flips a committed fact backwards.
240
+ const [memberships, setMemberships] = useState();
241
+ const [loadError, setLoadError] = useState();
168
242
  useEffect(() => {
169
243
  let mounted = true;
170
244
  void loadChannels({ workspaceId: status.member.workspaceId, topLevelOnly: true, includeActivities: false, limit: 1 }).then(result => {
@@ -190,43 +264,23 @@ function AgentEditorDialog({ status, channels, loadChannels, updateMember, joinC
190
264
  });
191
265
  // A successful join appends the durable fact; removal filters it out above.
192
266
  const joinedChannels = new Set((memberships ?? []).filter(item => item.memberId === memberId).map(item => item.channelRef));
193
- // The provider-grouped option list rides the shared Menu primitive: one
194
- // leading "follow Host default" row, then a non-selectable heading per
195
- // provider group. Keys resolve back through the map, never by parsing.
196
- const modelChoices = (() => {
197
- const items = [{ id: '', label: t('modelFollowDefault') }];
198
- const byKey = new Map();
199
- for (const group of groups ?? []) {
200
- items.push({ type: 'label', id: `model-group:${group.id}`, text: group.name });
201
- for (const entry of group.models) {
202
- const key = modelKey(group.id, entry.id);
203
- byKey.set(key, { provider: group.id, id: entry.id, name: entry.name });
204
- items.push({ id: key, label: entry.name });
205
- }
206
- }
207
- return { items, byKey };
208
- })();
209
- const selectedModelKey = model === undefined ? '' : modelKey(model[0], model[1]);
210
- const modelTriggerLabel = model === undefined
211
- ? t('modelFollowDefault')
212
- : modelChoices.byKey.get(selectedModelKey)?.name ?? `${model[0]} / ${model[1]}`;
213
267
  const dirty = handle.trim() !== status.member.handle || description.trim() !== status.member.description
214
- || !sameModel(model, status.member.model === undefined ? undefined : [status.member.model.provider, status.member.model.model]);
268
+ || !sameModel(model, status.member.model);
215
269
  const submit = async (event) => {
216
270
  event.preventDefault();
217
271
  const normalizedHandle = handle.trim();
218
272
  const normalizedDescription = description.trim();
219
- if (saving || !dirty || normalizedHandle.length === 0 || normalizedDescription.length === 0)
273
+ if (saving || !dirty || normalizedHandle.length === 0)
220
274
  return;
221
275
  const payload = {
222
276
  memberId,
223
277
  handle: normalizedHandle,
224
278
  description: normalizedDescription,
225
- ...(model === undefined ? {} : { model: { provider: model[0], model: model[1] } }),
279
+ ...(model === undefined ? {} : { model }),
226
280
  };
227
281
  const samePending = pendingRequest.current !== undefined && pendingRequest.current.memberId === payload.memberId
228
282
  && pendingRequest.current.handle === payload.handle && pendingRequest.current.description === payload.description
229
- && sameModel(pendingRequest.current.model === undefined ? undefined : [pendingRequest.current.model.provider, pendingRequest.current.model.model], model);
283
+ && sameModel(pendingRequest.current.model, model);
230
284
  const request = samePending ? pendingRequest.current : {
231
285
  requestId: crypto.randomUUID(),
232
286
  ...payload,
@@ -252,12 +306,7 @@ function AgentEditorDialog({ status, channels, loadChannels, updateMember, joinC
252
306
  setSaving(false);
253
307
  }
254
308
  };
255
- return (_jsx(Modal, { open: true, onClose: onClose, title: t('editAgent'), description: `@${status.member.handle}`, closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-agent-edit-form", variant: "primary", disabled: saving || !dirty || handle.trim().length === 0 || description.trim().length === 0, children: saving ? t('editSaving') : t('editSave') })] }), children: _jsxs("form", { id: "team-agent-edit-form", className: createCss.form, onSubmit: event => { void submit(event); }, children: [_jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('agentName') }), _jsx(Input, { className: createCss.input, value: handle, onChange: event => { setHandle(event.target.value); pendingRequest.current = undefined; }, disabled: saving, autoFocus: true })] }), _jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('agentDescription') }), _jsx(Input, { className: createCss.input, value: description, onChange: event => { setDescription(event.target.value); pendingRequest.current = undefined; }, disabled: saving })] }), _jsxs("div", { className: createCss.field, children: [_jsx("span", { children: t('memberModel') }), groups === undefined && modelsError === undefined && _jsx("small", { className: css.editHint, children: t('modelsLoading') }), modelsError !== undefined && _jsx("small", { className: css.editHint, children: t('modelsLoadFailed', { message: modelsError }) }), groups !== undefined && (_jsx(Menu, { open: modelOpen, portal: true, items: modelChoices.items, selectedId: selectedModelKey, onSelect: (key) => {
256
- setModelOpen(false);
257
- pendingRequest.current = undefined;
258
- const choice = modelChoices.byKey.get(key);
259
- setModel(choice === undefined ? undefined : [choice.provider, choice.id]);
260
- }, onClose: () => { setModelOpen(false); }, anchor: _jsxs("button", { type: "button", className: createCss.selectTrigger, "aria-label": t('memberModel'), "aria-haspopup": "listbox", "aria-expanded": modelOpen, disabled: saving, onClick: () => { setModelOpen(value => !value); }, children: [_jsx("span", { className: createCss.selectValue, children: modelTriggerLabel }), _jsx("span", { className: `${createCss.chevron} ${modelOpen ? createCss.chevronOpen : ''}`, "aria-hidden": true, children: _jsx(IconChevronDownOutline14, {}) })] }) }))] }), _jsxs("fieldset", { className: createCss.memberPicker, disabled: saving, children: [_jsx("legend", { children: t('channelMembersSection') }), channels.map(channel => {
309
+ return (_jsx(Modal, { open: true, onClose: onClose, title: t('editAgent'), description: `@${status.member.handle}`, closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-agent-edit-form", variant: "primary", disabled: saving || !dirty || handle.trim().length === 0, children: saving ? t('editSaving') : t('editSave') })] }), children: _jsxs("form", { id: "team-agent-edit-form", className: createCss.form, onSubmit: event => { void submit(event); }, children: [_jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('agentName') }), _jsx(Input, { className: createCss.input, value: handle, onChange: event => { setHandle(event.target.value); pendingRequest.current = undefined; }, disabled: saving, autoFocus: true })] }), _jsxs("label", { className: createCss.field, children: [_jsxs("span", { children: [t('agentDescription'), t('optionalSuffix')] }), _jsx(Input, { className: createCss.input, value: description, placeholder: t('agentDescriptionPlaceholder'), onChange: event => { setDescription(event.target.value); pendingRequest.current = undefined; }, disabled: saving })] }), _jsx(ModelPickerField, { model: model, onModelChange: choice => { pendingRequest.current = undefined; setModel(choice); }, loadModels: loadModels, disabled: saving, t: t }), _jsxs("fieldset", { className: createCss.memberPicker, disabled: saving, children: [_jsx("legend", { children: t('channelMembersSection') }), channels.map(channel => {
261
310
  const joined = joinedChannels.has(channel.channelRef);
262
311
  const rowPending = membership.pending.has(channel.channelRef);
263
312
  const disabled = rowPending || (!joined && status.presence === 'unavailable');
@@ -270,5 +319,5 @@ function sameModel(left, right) {
270
319
  return true;
271
320
  if (left === undefined || right === undefined)
272
321
  return false;
273
- return left[0] === right[0] && left[1] === right[1];
322
+ return left.provider === right.provider && left.model === right.model && left.reasoningEffort === right.reasoningEffort;
274
323
  }
@@ -1,19 +1,25 @@
1
1
  import type { AgentTeamChannelRef } 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 TeamChannelPageProps {
5
6
  readonly workspaceId: WorkspaceId;
6
7
  readonly channelRef: AgentTeamChannelRef;
7
8
  readonly loadChannels: TeamConversationProps['loadChannels'];
8
9
  readonly subscribeChanges: TeamConversationProps['subscribeChanges'];
9
10
  readonly loadMembers: TeamConversationProps['loadMembers'];
11
+ readonly drafts: TeamDraftStore;
12
+ readonly getAttachment: TeamConversationProps['getAttachment'];
13
+ readonly putAttachment: TeamConversationProps['putAttachment'];
10
14
  readonly sendMessage: TeamConversationProps['sendMessage'];
11
15
  readonly joinChannel: TeamConversationProps['joinChannel'];
12
16
  readonly removeChannelMember: TeamConversationProps['removeChannelMember'];
13
17
  readonly selectThread: TeamConversationProps['selectThread'];
18
+ readonly selectChannel: TeamConversationProps['selectChannel'];
19
+ readonly resolveTaskRefs: TeamConversationProps['resolveTaskRefs'];
14
20
  readonly backToChannels: TeamConversationProps['backToChannels'];
15
21
  readonly t: TeamConversationProps['t'];
16
22
  }
17
- export declare function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, selectThread, backToChannels, t }: TeamChannelPageProps): import("react").JSX.Element;
23
+ export declare function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscribeChanges, loadMembers, drafts, putAttachment, getAttachment, sendMessage, joinChannel, removeChannelMember, selectThread, selectChannel, backToChannels, resolveTaskRefs, t }: TeamChannelPageProps): import("react").JSX.Element;
18
24
  export {};
19
25
  //# sourceMappingURL=TeamChannelPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TeamChannelPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA+B,mBAAmB,EAAoF,MAAM,gCAAgC,CAAA;AACxL,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAEzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAYvD,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAA;IACxC,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,qBAAqB,CAAC,CAAA;IAC1E,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,cAAc,EAAE,qBAAqB,CAAC,gBAAgB,CAAC,CAAA;IAChE,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AAmBD,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE,oBAAoB,+BA+P7M"}
1
+ {"version":3,"file":"TeamChannelPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA+B,mBAAmB,EAE7D,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAEzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAGvD,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,aAAa,CAAA;AAc/D,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAA;IACxC,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,qBAAqB,CAAC,CAAA;IAC1E,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,cAAc,EAAE,qBAAqB,CAAC,gBAAgB,CAAC,CAAA;IAChE,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AAmBD,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC,EAAE,EAAE,oBAAoB,+BAgUnR"}
@@ -1,13 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { Fragment, useEffect, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import { Button, IconChevronLeftOutline14, IconChevronRightOutline14, Modal, StateDot } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import { bytesToBase64 } from "./attachment-preview.js";
4
5
  import { TeamComposer } from "./TeamComposer.js";
5
6
  import { TeamPresenceDot } from "./TeamPresenceDot.js";
6
7
  import { TeamMessage } from "./TeamMessage.js";
8
+ import { TeamRunDivider } from "./TeamRunDivider.js";
7
9
  import { formatTaskStatus, taskStatusDot, mentionNamesOf } from "./team-formatters.js";
8
10
  import { useChannelMembership } from "./team-membership.js";
9
11
  import { useTimelineScroll } from "./timeline-scroll.js";
10
- import { chunkRunsWithDays } from "./team-separators.js";
12
+ import { rememberResolvedTaskRef } from "./task-refs.js";
13
+ import { chunkRunsWithDays, isRunGap } from "./team-separators.js";
11
14
  import channelCss from './channel.module.css';
12
15
  import css from './conversation.module.css';
13
16
  import threadCss from './thread.module.css';
@@ -27,22 +30,60 @@ function mergeChannelView(current, fresh) {
27
30
  hasMore: fresh.hasMore || current.cursor < fresh.cursor,
28
31
  };
29
32
  }
30
- export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, selectThread, backToChannels, t }) {
33
+ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscribeChanges, loadMembers, drafts, putAttachment, getAttachment, sendMessage, joinChannel, removeChannelMember, selectThread, selectChannel, backToChannels, resolveTaskRefs, t }) {
31
34
  const [view, setView] = useState();
32
35
  const [members, setMembers] = useState([]);
33
- const [draft, setDraft] = useState('');
34
36
  const [error, setError] = useState();
37
+ const [pendingFiles, setPendingFiles] = useState([]);
35
38
  const [statusMessage, setStatusMessage] = useState();
36
39
  const [loading, setLoading] = useState(true);
37
40
  const [pending, setPending] = useState(false);
38
41
  const [loadingOlder, setLoadingOlder] = useState(false);
39
- const [recipients, setRecipients] = useState(new Set());
40
42
  const [managingMembers, setManagingMembers] = useState(false);
43
+ // The composer draft lives in the keyed draft cache: view switches unmount
44
+ // this page, and a refresh must not cost the half-written message either.
45
+ const draftKey = `channel:${channelRef}`;
46
+ const { draft, recipients } = useSyncExternalStore(drafts.subscribe, () => drafts.getSnapshot(draftKey));
41
47
  const manageTriggerRef = useRef(null);
42
48
  const memberListRef = useRef(null);
43
49
  const mountedRef = useRef(false);
44
50
  const refreshSequenceRef = useRef(0);
45
51
  const channelLastItem = view?.items[view.items.length - 1];
52
+ // Branded-ref navigation for message bodies: channel refs hop directly,
53
+ // task refs resolve against this Channel's loaded timeline and degrade to a
54
+ // no-op when the target is not reachable from here.
55
+ const openRef = (ref) => {
56
+ if (ref.startsWith('channel:')) {
57
+ if (ref !== channelRef)
58
+ selectChannel(ref);
59
+ return;
60
+ }
61
+ if (ref.startsWith('task:')) {
62
+ const match = view?.items.find(item => item.task.taskRef === ref);
63
+ if (match !== undefined) {
64
+ selectThread(match.task.taskRef, match.thread.threadRef, channelRef, match.taskNumber);
65
+ return;
66
+ }
67
+ // Not in the loaded timeline: resolve through the Host and jump to the
68
+ // Task's home Channel.
69
+ void resolveTaskRefs({ workspaceId, taskRefs: [ref] })
70
+ .then(result => {
71
+ if (!result.ok)
72
+ return;
73
+ const hit = result.value.resolved[0];
74
+ if (hit !== undefined)
75
+ selectThread(hit.taskRef, hit.threadRef, hit.channelRef, hit.taskNumber);
76
+ });
77
+ }
78
+ };
79
+ const lookupTaskRefs = (taskRefs) => resolveTaskRefs({ workspaceId, taskRefs })
80
+ .then(result => {
81
+ if (!result.ok)
82
+ return [];
83
+ for (const entry of result.value.resolved)
84
+ rememberResolvedTaskRef(entry);
85
+ return result.value.resolved;
86
+ });
46
87
  const timeline = useTimelineScroll(`${view?.items.length ?? 0}:${channelLastItem?.message.messageRef ?? ''}`);
47
88
  const channel = view?.channels.find(item => item.channelRef === channelRef);
48
89
  const channelMemberIds = new Set(view?.members.filter(item => item.channelRef === channelRef).map(item => item.memberId) ?? []);
@@ -112,7 +153,6 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
112
153
  setView(undefined);
113
154
  setError(undefined);
114
155
  setLoading(true);
115
- setRecipients(new Set());
116
156
  setManagingMembers(false);
117
157
  void refresh();
118
158
  const disposers = [
@@ -179,19 +219,39 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
179
219
  // definitive outcomes (committed or rejected) start the next send fresh.
180
220
  const pendingSendId = useRef();
181
221
  const send = async () => {
222
+ console.log('DBG send entry pending=%s draft=%j files=%d', pending, draft, pendingFiles.length);
182
223
  if (pending || draft.trim() === '')
183
224
  return;
184
225
  const recipientIds = [...recipients].sort();
185
226
  const requestId = pendingSendId.current ?? crypto.randomUUID();
186
227
  pendingSendId.current = requestId;
187
- const request = {
188
- requestId, workspaceId,
189
- channelRef, body: draft.trim(), recipients: recipientIds,
190
- };
191
228
  setPending(true);
192
229
  setError(undefined);
193
230
  setStatusMessage(undefined);
194
231
  try {
232
+ // Upload chosen files first; any failure aborts the send with the
233
+ // existing error surface and keeps the chips for a retry.
234
+ const attachmentIds = [];
235
+ for (const file of pendingFiles) {
236
+ const uploaded = await putAttachment({
237
+ requestId: crypto.randomUUID(),
238
+ workspaceId,
239
+ name: file.name,
240
+ mediaType: file.type === '' ? undefined : file.type,
241
+ bytesBase64: bytesToBase64(new Uint8Array(await file.arrayBuffer())),
242
+ });
243
+ if (!uploaded.ok) {
244
+ pendingSendId.current = undefined;
245
+ setError(uploaded.error.message);
246
+ return;
247
+ }
248
+ attachmentIds.push(uploaded.value.attachmentId);
249
+ }
250
+ const request = {
251
+ requestId, workspaceId,
252
+ channelRef, body: draft.trim(), recipients: recipientIds,
253
+ ...(attachmentIds.length === 0 ? {} : { attachments: attachmentIds }),
254
+ };
195
255
  const result = await sendMessage(request);
196
256
  if (!result.ok) {
197
257
  pendingSendId.current = undefined;
@@ -200,8 +260,8 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
200
260
  else if (result.value.kind === 'committed') {
201
261
  pendingSendId.current = undefined;
202
262
  await refresh();
203
- setDraft('');
204
- setRecipients(new Set());
263
+ drafts.clear(draftKey);
264
+ setPendingFiles([]);
205
265
  setStatusMessage(undefined);
206
266
  }
207
267
  else if (result.value.kind === 'confirmation_required') {
@@ -233,11 +293,12 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
233
293
  const senderStatus = members.find(member => member.member.memberId === item.message.sender);
234
294
  const human = item.message.sender === view.humanMemberId;
235
295
  const sender = human ? t('human') : senderStatus?.member.handle ?? item.message.sender;
236
- return _jsx(TeamMessage, { senderName: sender, memberId: item.message.sender, human: human, body: item.message.body, occurredAt: item.message.occurredAt, mentionNames: mentionNamesOf(item.mentions, mentionHandlesMap), grouped: index > 0, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }), children: item.message.topLevel && _jsxs("button", { type: "button", className: channelCss.taskFooter, "aria-label": t('openTask', { number: item.taskNumber }), onClick: () => { selectThread(item.task.taskRef, item.thread.threadRef, channelRef, item.taskNumber); }, children: [_jsx("span", { className: channelCss.taskDot, children: (() => {
237
- const dot = taskStatusDot(item.task.status);
238
- return dot === 'ongoing' || dot === 'warning' || dot === 'done'
239
- ? _jsx(StateDot, { size: 8, state: dot })
240
- : _jsx("span", { className: channelCss.taskDotQuiet, "data-variant": dot });
241
- })() }), _jsx("span", { className: channelCss.taskNumber, children: t('taskLabel', { number: item.taskNumber }) }), _jsx("span", { className: channelCss.taskStatus, children: formatTaskStatus(item.task.status, t) }), _jsx("span", { className: channelCss.taskCount, children: t('taskMessageCount', { count: item.messageCount }) }), _jsx("span", { className: channelCss.taskArrow, "aria-hidden": "true", children: _jsx(IconChevronRightOutline14, { size: 12 }) })] }) }, item.message.messageRef);
242
- }) }, `run-${block.items[0].message.messageRef}`))] }) }), channel !== undefined ? _jsx(TeamComposer, { members: channelMembers, recipients: recipients, draft: draft, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onDraftChange: next => { setDraft(next); setStatusMessage(undefined); }, onRecipientsChange: next => { setRecipients(next); setStatusMessage(undefined); }, onSubmit: () => { void send(); }, t: t }) : _jsx("div", {})] });
296
+ return _jsxs(Fragment, { children: [isRunGap(index > 0 ? block.items[index - 1].message.occurredAt : undefined, item.message.occurredAt)
297
+ && _jsx(TeamRunDivider, { occurredAt: item.message.occurredAt }), _jsx(TeamMessage, { senderName: sender, memberId: item.message.sender, human: human, body: item.message.body, attachments: item.message.attachments, loadAttachment: getAttachment, t: t, occurredAt: item.message.occurredAt, mentionNames: mentionNamesOf(item.mentions, mentionHandlesMap), onOpenRef: openRef, onResolveTaskRefs: lookupTaskRefs, grouped: index > 0, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }), children: item.message.topLevel && _jsxs("button", { type: "button", className: channelCss.taskFooter, "aria-label": t('openTask', { number: item.taskNumber }), onClick: () => { selectThread(item.task.taskRef, item.thread.threadRef, channelRef, item.taskNumber); }, children: [_jsx("span", { className: channelCss.taskDot, children: (() => {
298
+ const dot = taskStatusDot(item.task.status);
299
+ return dot === 'ongoing' || dot === 'warning' || dot === 'done'
300
+ ? _jsx(StateDot, { size: 8, state: dot })
301
+ : _jsx("span", { className: channelCss.taskDotQuiet, "data-variant": dot });
302
+ })() }), _jsx("span", { className: channelCss.taskNumber, children: t('taskLabel', { number: item.taskNumber }) }), _jsx("span", { className: channelCss.taskStatus, children: formatTaskStatus(item.task.status, t) }), _jsx("span", { className: channelCss.taskCount, children: t('taskMessageCount', { count: item.messageCount }) }), _jsx("span", { className: channelCss.taskArrow, "aria-hidden": "true", children: _jsx(IconChevronRightOutline14, { size: 12 }) })] }) })] }, item.message.messageRef);
303
+ }) }, `run-${block.items[0].message.messageRef}`))] }) }), channel !== undefined ? _jsx(TeamComposer, { members: channelMembers, recipients: recipients, draft: draft, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onDraftChange: next => { drafts.writeDraft(draftKey, next); setStatusMessage(undefined); }, onRecipientsChange: next => { drafts.writeRecipients(draftKey, next); setStatusMessage(undefined); }, onSubmit: () => { void send(); }, pendingFiles: pendingFiles, onFilesChange: setPendingFiles, t: t }, draftKey) : _jsx("div", {})] });
243
304
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TeamChannelsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EAEzB,mBAAmB,EAOpB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAEzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAQlD,UAAU,sBAAsB;IAC9B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,wGAAwG;IACxG,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,kBAAkB,CAAC,CAAA;IAC/D,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,qBAAqB,CAAC,CAAA;IACrE,QAAQ,CAAC,cAAc,EAAE,SAAS,yBAAyB,EAAE,CAAA;IAC7D,QAAQ,CAAC,kBAAkB,CAAC,EAAE,mBAAmB,CAAA;IACjD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CAClC;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,+BAuL9D"}
1
+ {"version":3,"file":"TeamChannelsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EAEzB,mBAAmB,EAOpB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAEzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAWlD,UAAU,sBAAsB;IAC9B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC,cAAc,CAAC,CAAA;IACvD,wGAAwG;IACxG,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,kBAAkB,CAAC,CAAA;IAC/D,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IACrD,QAAQ,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,qBAAqB,CAAC,CAAA;IACrE,QAAQ,CAAC,cAAc,EAAE,SAAS,yBAAyB,EAAE,CAAA;IAC7D,QAAQ,CAAC,kBAAkB,CAAC,EAAE,mBAAmB,CAAA;IACjD,QAAQ,CAAC,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;CAClC;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,+BA2L9D"}