@wowyuarm/dsh-agent-team 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/README.zh.md +10 -2
- package/package.json +32 -24
- package/packages/agent-team/lib/attachments.js +157 -0
- package/packages/agent-team/lib/index.js +280 -22
- package/packages/agent-team/lib/ledger.js +98 -22
- package/packages/agent-team/lib/recovery.js +87 -0
- package/packages/agent-team/lib/spec.js +27 -5
- package/packages/agent-team/lib/typert.host.js +313 -26
- package/packages/agent-team/lib/typert.remote-client.d.ts +9 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +234 -17
- package/packages/agent-team/lib/types/attachments.d.ts +69 -0
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +68 -6
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +17 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +42 -0
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +94 -3
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +2 -2
- package/packages/client-agent-team/README.md +4 -3
- package/packages/client-agent-team/README.zh.md +4 -3
- package/packages/client-agent-team/lib/client.js +1791 -322
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +5 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +108 -59
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +7 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +80 -19
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +38 -19
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +61 -4
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +12 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +152 -14
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +7 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +93 -16
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +6 -5
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +50 -10
- package/packages/client-agent-team/lib/types/client/locales.d.ts +34 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +34 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +35 -0
- package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/sidebar-drag.js +70 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +27 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/sidebar-order.js +170 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +18 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/task-refs.js +58 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +15 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +30 -2
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
- package/packages/tool-agent-team/lib/index.js +10 -6
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -1,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,
|
|
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 ||
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
|
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 [
|
|
146
|
-
const [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
204
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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;
|
|
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"}
|