@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
|
@@ -2,6 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Button, IconEditOutline16, IconPlusOutline16, Input, Modal, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
4
|
import { TeamPresenceDot } from "./TeamPresenceDot.js";
|
|
5
|
+
import { MultiMenuField } from "./multi-menu-field.js";
|
|
6
|
+
import { SortableRow, useSidebarRowDrag } from "./sidebar-drag.js";
|
|
7
|
+
import { moveSidebarItem, useSidebarOrder } from "./sidebar-order.js";
|
|
5
8
|
import { TeamRowMenu } from "./TeamRowMenu.js";
|
|
6
9
|
import { TeamSidebarSection } from "./TeamSidebarSection.js";
|
|
7
10
|
import { useChannelMembership } from "./team-membership.js";
|
|
@@ -21,6 +24,18 @@ export function TeamChannelsPanel(props) {
|
|
|
21
24
|
const [pendingCreate, setPendingCreate] = useState();
|
|
22
25
|
const previousCreatingKey = useRef(creatingAgents.map(request => request.requestId).join(','));
|
|
23
26
|
const triggerRef = useRef(null);
|
|
27
|
+
// Row order is this browser's presentation preference; the drag commits
|
|
28
|
+
// through the single shared mutation below.
|
|
29
|
+
const channelRefs = useMemo(() => view?.channels.map(channel => channel.channelRef) ?? [], [view]);
|
|
30
|
+
const orderedChannelRefs = useSidebarOrder(workspaceId, 'channels', channelRefs);
|
|
31
|
+
const orderedChannels = useMemo(() => {
|
|
32
|
+
const byRef = new Map((view?.channels ?? []).map(channel => [channel.channelRef, channel]));
|
|
33
|
+
return orderedChannelRefs.map(channelRef => byRef.get(channelRef)).filter(channel => channel !== undefined);
|
|
34
|
+
}, [orderedChannelRefs, view]);
|
|
35
|
+
const applyMove = (movedRef, targetRef, marker) => {
|
|
36
|
+
void moveSidebarItem(workspaceId, 'channels', orderedChannelRefs, movedRef, targetRef, marker);
|
|
37
|
+
};
|
|
38
|
+
const drag = useSidebarRowDrag({ refs: orderedChannelRefs, onCommit: applyMove });
|
|
24
39
|
const refresh = useCallback(async () => {
|
|
25
40
|
setLoading(true);
|
|
26
41
|
const [channelResult, memberResult] = await Promise.all([
|
|
@@ -76,7 +91,7 @@ export function TeamChannelsPanel(props) {
|
|
|
76
91
|
};
|
|
77
92
|
const submit = async (event) => {
|
|
78
93
|
event.preventDefault();
|
|
79
|
-
if (mutating || name.trim() === ''
|
|
94
|
+
if (mutating || name.trim() === '')
|
|
80
95
|
return;
|
|
81
96
|
const payload = { workspaceId, name: name.trim(), description: description.trim(), memberIds: [...selected] };
|
|
82
97
|
const samePending = pendingCreate !== undefined && pendingCreate.workspaceId === payload.workspaceId
|
|
@@ -110,23 +125,27 @@ export function TeamChannelsPanel(props) {
|
|
|
110
125
|
setMutating(false);
|
|
111
126
|
}
|
|
112
127
|
};
|
|
113
|
-
return (_jsxs("div", { className: css.panel, children: [_jsx(Modal, { open: formOpen, onClose: closeForm, title: t('addChannel'), closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: mutating, onClick: closeForm, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-channel-create-form", variant: "primary", disabled: mutating || name.trim() === ''
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
return (_jsxs("div", { className: css.panel, children: [_jsx(Modal, { open: formOpen, onClose: closeForm, title: t('addChannel'), closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: mutating, onClick: closeForm, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-channel-create-form", variant: "primary", disabled: mutating || name.trim() === '', children: mutating ? t('creatingChannel') : t('createChannel') })] }), children: _jsxs("form", { id: "team-channel-create-form", className: createCss.form, onSubmit: event => { void submit(event); }, children: [_jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('channelName') }), _jsx(Input, { className: createCss.input, value: name, disabled: mutating, autoFocus: true, onChange: event => { setName(event.target.value); setPendingCreate(undefined); } })] }), _jsxs("label", { className: createCss.field, children: [_jsxs("span", { children: [t('channelDescription'), t('optionalSuffix')] }), _jsx(Input, { className: createCss.input, value: description, placeholder: t('agentDescriptionPlaceholder'), disabled: mutating, onChange: event => { setDescription(event.target.value); setPendingCreate(undefined); } })] }), _jsx(MultiMenuField, { label: t('initialMembers'), disabled: mutating, options: [
|
|
129
|
+
...creatingAgents.map(request => ({ id: request.requestId, label: request.handle, disabled: true, hint: t('memberCreatingReason') })),
|
|
130
|
+
...members.map(status => ({
|
|
131
|
+
id: status.member.memberId,
|
|
132
|
+
label: status.member.handle,
|
|
133
|
+
...(status.presence === 'unavailable' ? { disabled: true, hint: t('memberUnavailableReason') } : {}),
|
|
134
|
+
icon: _jsx(TeamPresenceDot, { status: status, t: t }),
|
|
135
|
+
})),
|
|
136
|
+
], selected: [...selected], onToggle: id => {
|
|
137
|
+
setSelected(current => {
|
|
138
|
+
const next = new Set(current);
|
|
139
|
+
if (next.has(id))
|
|
140
|
+
next.delete(id);
|
|
141
|
+
else
|
|
142
|
+
next.add(id);
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
setPendingCreate(undefined);
|
|
146
|
+
}, triggerEmptyLabel: t('membersPickerEmpty'), formatCount: count => t('membersPickerCount', { count }) }), error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('channels'), actions: (_jsx(Tooltip, { label: t('addChannel'), delayMs: 500, children: _jsx("button", { ref: triggerRef, type: "button", className: css.iconButton, "aria-label": t('addChannel'), onClick: () => { setError(undefined); setFormOpen(true); }, children: _jsx(IconPlusOutline16, { size: 14 }) }) })), children: [loading && view === undefined && _jsx("p", { className: css.emptyState, children: t('loadingChannels') }), !loading && (view?.channels.length ?? 0) === 0 && _jsx("p", { className: css.emptyState, children: t('emptyChannels') }), _jsx("div", { className: css.channelList, children: orderedChannels.map(channel => {
|
|
128
147
|
const joined = membership.get(channel.channelRef) ?? new Set();
|
|
129
|
-
return (_jsx(ChannelRow, { channel: channel, members: members, joinedIds: joined, selected: selectedChannelRef === channel.channelRef, updateChannel: updateChannel, joinChannel: props.joinChannel, removeChannelMember: props.removeChannelMember, onCommitted: () => { void refresh(); }, selectChannel: selectChannel, t: t }, channel.channelRef));
|
|
148
|
+
return (_jsx(SortableRow, { drag: drag, orderKey: channel.channelRef, children: _jsx(ChannelRow, { channel: channel, members: members, joinedIds: joined, selected: selectedChannelRef === channel.channelRef, updateChannel: updateChannel, joinChannel: props.joinChannel, removeChannelMember: props.removeChannelMember, onCommitted: () => { void refresh(); }, selectChannel: selectChannel, t: t }) }, channel.channelRef));
|
|
130
149
|
}) })] }), !formOpen && error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error })] }));
|
|
131
150
|
}
|
|
132
151
|
/**
|
|
@@ -154,7 +173,7 @@ function ChannelEditorDialog({ channel, members, joinedIds, updateChannel, joinC
|
|
|
154
173
|
event.preventDefault();
|
|
155
174
|
const normalizedName = name.trim();
|
|
156
175
|
const normalizedDescription = description.trim();
|
|
157
|
-
if (saving || !dirty || normalizedName === ''
|
|
176
|
+
if (saving || !dirty || normalizedName === '')
|
|
158
177
|
return;
|
|
159
178
|
const samePending = pendingRequest.current !== undefined && pendingRequest.current.channelRef === channel.channelRef
|
|
160
179
|
&& pendingRequest.current.name === normalizedName && pendingRequest.current.description === normalizedDescription;
|
|
@@ -186,7 +205,7 @@ function ChannelEditorDialog({ channel, members, joinedIds, updateChannel, joinC
|
|
|
186
205
|
setSaving(false);
|
|
187
206
|
}
|
|
188
207
|
};
|
|
189
|
-
return (_jsx(Modal, { open: true, onClose: onClose, title: t('editChannel'), description: `# ${channel.name}`, closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-channel-edit-form", variant: "primary", disabled: saving || !dirty || name.trim() === ''
|
|
208
|
+
return (_jsx(Modal, { open: true, onClose: onClose, title: t('editChannel'), description: `# ${channel.name}`, closeLabel: t('close'), contentClassName: createCss.dialogContent, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx(Button, { type: "submit", form: "team-channel-edit-form", variant: "primary", disabled: saving || !dirty || name.trim() === '', children: saving ? t('editSaving') : t('editSave') })] }), children: _jsxs("form", { id: "team-channel-edit-form", className: createCss.form, onSubmit: event => { void submit(event); }, children: [_jsxs("label", { className: createCss.field, children: [_jsx("span", { children: t('channelName') }), _jsx(Input, { className: createCss.input, value: name, onChange: event => { setName(event.target.value); pendingRequest.current = undefined; }, disabled: saving, autoFocus: true })] }), _jsxs("label", { className: createCss.field, children: [_jsxs("span", { children: [t('channelDescription'), t('optionalSuffix')] }), _jsx(Input, { className: createCss.input, value: description, placeholder: t('agentDescriptionPlaceholder'), onChange: event => { setDescription(event.target.value); pendingRequest.current = undefined; }, disabled: saving })] }), _jsxs("fieldset", { className: createCss.memberPicker, disabled: saving, children: [_jsx("legend", { children: t('channelMembersSection') }), members.map(status => {
|
|
190
209
|
const joined = joinedIds.has(status.member.memberId);
|
|
191
210
|
const rowPending = membership.pending.has(status.member.memberId);
|
|
192
211
|
const disabled = rowPending || (!joined && status.presence === 'unavailable');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentTeamClientMemberStatus, AgentTeamMemberId } from '@wowyuarm/dsh-agent-team/types';
|
|
2
2
|
import type { TeamConversationProps } from './slots.ts';
|
|
3
|
-
export declare function TeamComposer({ members, recipients, draft, pending, confirmation, error, onDraftChange, onRecipientsChange, onSubmit, t }: {
|
|
3
|
+
export declare function TeamComposer({ members, recipients, draft, pending, confirmation, error, onDraftChange, onRecipientsChange, onSubmit, pendingFiles, onFilesChange, t }: {
|
|
4
4
|
readonly members: readonly AgentTeamClientMemberStatus[];
|
|
5
5
|
readonly recipients: ReadonlySet<AgentTeamMemberId>;
|
|
6
6
|
readonly draft: string;
|
|
@@ -10,6 +10,9 @@ export declare function TeamComposer({ members, recipients, draft, pending, conf
|
|
|
10
10
|
readonly onDraftChange: (draft: string) => void;
|
|
11
11
|
readonly onRecipientsChange: (recipients: ReadonlySet<AgentTeamMemberId>) => void;
|
|
12
12
|
readonly onSubmit: () => void;
|
|
13
|
+
/** Upload-capable surfaces pass this to enable the "+" file picker; the reply path omits it. */
|
|
14
|
+
readonly pendingFiles?: readonly File[];
|
|
15
|
+
readonly onFilesChange?: (files: readonly File[]) => void;
|
|
13
16
|
readonly t: TeamConversationProps['t'];
|
|
14
17
|
}): import("react").JSX.Element;
|
|
15
18
|
//# sourceMappingURL=TeamComposer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamComposer.d.ts","sourceRoot":"","sources":["../../../src/client/TeamComposer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAEpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"TeamComposer.d.ts","sourceRoot":"","sources":["../../../src/client/TeamComposer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAEpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAkDvD,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC,EAAE,EAAE;IACtK,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAA;IACxD,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/C,QAAQ,CAAC,kBAAkB,EAAE,CAAC,UAAU,EAAE,WAAW,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,IAAI,EAAE,CAAA;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,KAAK,IAAI,CAAA;IACzD,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC,+BAiOA"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
-
import { IconSendOutline16, useAnchoredMaxHeight, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
3
|
+
import { IconPlusOutline16, IconSendOutline16, useAnchoredMaxHeight, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
4
|
import { TeamPresenceDot } from "./TeamPresenceDot.js";
|
|
5
5
|
import css from './composer.module.css';
|
|
6
|
+
import { formatByteSize } from "./attachment-preview.js";
|
|
6
7
|
function findMention(draft, caret) {
|
|
7
8
|
const beforeCaret = draft.slice(0, caret);
|
|
8
9
|
if (beforeCaret.length === 0 || /\s/u.test(beforeCaret.at(-1) ?? ''))
|
|
@@ -29,8 +30,21 @@ function mentionCandidates(members, query) {
|
|
|
29
30
|
&& status.member.state !== 'inactive'
|
|
30
31
|
&& status.member.handle.toLocaleLowerCase().startsWith(normalized));
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
/** One object URL per draft file; revoked when the draft is removed. */
|
|
34
|
+
const draftPreviewUrls = new WeakMap();
|
|
35
|
+
function draftPreviewUrl(file) {
|
|
36
|
+
if (!file.type.startsWith('image/'))
|
|
37
|
+
return undefined;
|
|
38
|
+
let url = draftPreviewUrls.get(file);
|
|
39
|
+
if (url === undefined) {
|
|
40
|
+
url = URL.createObjectURL(file);
|
|
41
|
+
draftPreviewUrls.set(file, url);
|
|
42
|
+
}
|
|
43
|
+
return url;
|
|
44
|
+
}
|
|
45
|
+
export function TeamComposer({ members, recipients, draft, pending, confirmation, error, onDraftChange, onRecipientsChange, onSubmit, pendingFiles, onFilesChange, t }) {
|
|
33
46
|
const inputRef = useRef(null);
|
|
47
|
+
const fileInputRef = useRef(null);
|
|
34
48
|
const rootRef = useRef(null);
|
|
35
49
|
const menuRef = useRef(null);
|
|
36
50
|
const composingRef = useRef(false);
|
|
@@ -57,6 +71,22 @@ export function TeamComposer({ members, recipients, draft, pending, confirmation
|
|
|
57
71
|
}
|
|
58
72
|
setHighlight(current => Math.min(current, candidates.length - 1));
|
|
59
73
|
}, [mention, candidates.length]);
|
|
74
|
+
// Match the resident DSH composer without stealing a later user choice: Team
|
|
75
|
+
// data can load after navigation, so a dialog or another control may already
|
|
76
|
+
// own focus by the time this composer appears.
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const active = document.activeElement;
|
|
79
|
+
if (active !== document.body && active?.closest('[aria-current="page"]') === null)
|
|
80
|
+
return;
|
|
81
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
82
|
+
}, []);
|
|
83
|
+
// Confirmation settles after a read-only submission span. Restore focus in
|
|
84
|
+
// case the browser moved it so the second Enter can confirm immediately.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (confirmation === undefined || pending)
|
|
87
|
+
return;
|
|
88
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
89
|
+
}, [confirmation, pending]);
|
|
60
90
|
const pruneRecipients = (nextDraft) => {
|
|
61
91
|
const knownMembers = new Map(members.map(status => [status.member.memberId, status.member]));
|
|
62
92
|
const next = new Set([...recipients].filter(memberId => {
|
|
@@ -66,6 +96,15 @@ export function TeamComposer({ members, recipients, draft, pending, confirmation
|
|
|
66
96
|
if (next.size !== recipients.size)
|
|
67
97
|
onRecipientsChange(next);
|
|
68
98
|
};
|
|
99
|
+
// Restored drafts may carry recipients that no longer match the text (or
|
|
100
|
+
// unknown Members); converge on mount and on every state change so the
|
|
101
|
+
// cached entry never stays stale — the same rule user edits already apply.
|
|
102
|
+
// An unloaded roster must never judge recipients unknown.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (members.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
pruneRecipients(draft);
|
|
107
|
+
}, [draft, recipients, members]);
|
|
69
108
|
const updateMention = (nextDraft, caret) => {
|
|
70
109
|
const match = findMention(nextDraft, caret);
|
|
71
110
|
setMention(match);
|
|
@@ -133,6 +172,7 @@ export function TeamComposer({ members, recipients, draft, pending, confirmation
|
|
|
133
172
|
onSubmit();
|
|
134
173
|
};
|
|
135
174
|
return _jsxs("form", { ref: rootRef, className: css.root, onSubmit: event => {
|
|
175
|
+
console.log('DBG composer submit pending=%s draft=%j', pending, draft);
|
|
136
176
|
event.preventDefault();
|
|
137
177
|
if (!pending && draft.trim() !== '')
|
|
138
178
|
onSubmit();
|
|
@@ -140,5 +180,22 @@ export function TeamComposer({ members, recipients, draft, pending, confirmation
|
|
|
140
180
|
const optionId = `${listId}-${status.member.memberId}`;
|
|
141
181
|
const selected = index === highlight;
|
|
142
182
|
return _jsxs("button", { id: optionId, type: "button", role: "option", "aria-selected": selected, className: css.mentionOption, onMouseDown: event => { event.preventDefault(); }, onClick: () => { selectMention(status); }, children: [_jsx(TeamPresenceDot, { status: status, t: t }), _jsxs("span", { className: css.mentionName, children: ["@", status.member.handle] }), _jsx("span", { className: css.mentionDescription, children: status.member.description })] }, status.member.memberId);
|
|
143
|
-
}) }), _jsx("textarea", { ref: inputRef, "aria-label": t('messageDraft'), "aria-autocomplete": "list", "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen && activeCandidate !== undefined ? `${listId}-${activeCandidate.member.memberId}` : undefined, "aria-expanded": menuOpen, value: draft,
|
|
183
|
+
}) }), _jsx("textarea", { ref: inputRef, "aria-label": t('messageDraft'), "aria-autocomplete": "list", "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen && activeCandidate !== undefined ? `${listId}-${activeCandidate.member.memberId}` : undefined, "aria-expanded": menuOpen, value: draft, readOnly: pending, placeholder: t('messagePlaceholder'), rows: 1, onChange: onChange, onKeyDown: onKeyDown, onSelect: event => { updateMention(event.currentTarget.value, event.currentTarget.selectionStart ?? event.currentTarget.value.length); }, onCompositionStart: () => { composingRef.current = true; }, onCompositionEnd: () => { setTimeout(() => { composingRef.current = false; }, 10); } })] }), recipients.size > 0 && _jsx("p", { className: css.notifyRow, "data-team-notify": true, children: t('composerNotify', { ids: [...recipients].sort().map(memberId => `@${members.find(candidate => candidate.member.memberId === memberId)?.member.handle ?? memberId}`).join(', ') }) }), onFilesChange !== undefined && pendingFiles !== undefined && pendingFiles.length > 0 && (_jsx("ul", { className: css.fileChips, "aria-label": t('attachFiles'), children: pendingFiles.map((file, index) => {
|
|
184
|
+
const previewUrl = draftPreviewUrl(file);
|
|
185
|
+
return _jsxs("li", { className: `${css.fileChip} ${previewUrl !== undefined ? css.imageChip : ''}`, children: [previewUrl !== undefined && _jsx("img", { src: previewUrl, alt: "", className: css.imageChipPreview }), _jsxs("span", { className: css.fileChipName, title: file.name, children: [file.name, _jsx("span", { className: css.fileChipSize, children: formatByteSize(file.size) })] }), _jsx("button", { type: "button", className: css.fileChipRemove, "aria-label": t('removeFile', { name: file.name }), disabled: pending, onClick: () => {
|
|
186
|
+
const url = draftPreviewUrls.get(file);
|
|
187
|
+
if (url !== undefined)
|
|
188
|
+
URL.revokeObjectURL(url);
|
|
189
|
+
onFilesChange(pendingFiles.filter((_, candidate) => candidate !== index));
|
|
190
|
+
}, children: "\u00D7" })] }, `${file.name}-${index}`);
|
|
191
|
+
}) })), _jsxs("div", { className: css.toolbar, children: [onFilesChange !== undefined && (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: css.fileInput, "aria-hidden": "true", tabIndex: -1, onChange: event => {
|
|
192
|
+
const chosen = [...event.target.files ?? []];
|
|
193
|
+
if (chosen.length > 0 && pendingFiles !== undefined)
|
|
194
|
+
onFilesChange([...pendingFiles, ...chosen]);
|
|
195
|
+
event.target.value = '';
|
|
196
|
+
} }), _jsx("button", { type: "button", className: css.attachButton, "aria-label": t('attachFiles'), title: t('attachFiles'), disabled: pending, onClick: () => { fileInputRef.current?.click(); }, children: _jsx(IconPlusOutline16, { size: 14 }) })] })), _jsx("button", { type: "submit", className: css.sendButton, "aria-label": pending ? t('sendingMessage') : t('sendMessage'), disabled: pending || draft.trim() === '', onMouseDown: event => {
|
|
197
|
+
// Keep the composer focused when the send control is clicked.
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
200
|
+
}, children: _jsx(IconSendOutline16, { size: 16 }) })] })] }), error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error })] });
|
|
144
201
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { TeamConversationProps } from './slots.ts';
|
|
2
|
-
export declare function TeamConversation({ t, useWorkspaces, navigation, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, selectThread, backToWorkspace, backToChannels }: TeamConversationProps): import("react").JSX.Element;
|
|
2
|
+
export declare function TeamConversation({ t, useWorkspaces, navigation, drafts, putAttachment, getAttachment, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, selectThread, selectChannel, backToWorkspace, backToChannels, resolveTaskRefs }: TeamConversationProps): import("react").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=TeamConversation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamConversation.d.ts","sourceRoot":"","sources":["../../../src/client/TeamConversation.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAKvD,wBAAgB,gBAAgB,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,EAAE,qBAAqB,+
|
|
1
|
+
{"version":3,"file":"TeamConversation.d.ts","sourceRoot":"","sources":["../../../src/client/TeamConversation.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAKvD,wBAAgB,gBAAgB,CAAC,EAAE,CAAC,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,eAAe,EAAE,EAAE,qBAAqB,+BAoB1V"}
|
|
@@ -3,15 +3,15 @@ import { useSyncExternalStore } from 'react';
|
|
|
3
3
|
import { TeamChannelPage } from "./TeamChannelPage.js";
|
|
4
4
|
import { TeamThreadPage } from "./TeamThreadPage.js";
|
|
5
5
|
import css from './conversation.module.css';
|
|
6
|
-
export function TeamConversation({ t, useWorkspaces, navigation, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, selectThread, backToWorkspace, backToChannels }) {
|
|
6
|
+
export function TeamConversation({ t, useWorkspaces, navigation, drafts, putAttachment, getAttachment, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, sendMessage, joinChannel, removeChannelMember, reply, changeTask, selectThread, selectChannel, backToWorkspace, backToChannels, resolveTaskRefs }) {
|
|
7
7
|
const navigationState = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot, navigation.getSnapshot);
|
|
8
8
|
const workspaces = useWorkspaces(state => state.items);
|
|
9
9
|
const current = workspaces.find(workspace => workspace.workspaceId === navigationState.workspaceId);
|
|
10
10
|
if (current !== undefined && navigationState.taskRef !== undefined && navigationState.threadRef !== undefined) {
|
|
11
|
-
return _jsx(TeamThreadPage, { workspaceId: current.workspaceId, taskRef: navigationState.taskRef, threadRef: navigationState.threadRef, backToWorkspace: backToWorkspace, ...(navigationState.channelRef === undefined ? {} : { channelRef: navigationState.channelRef }), ...(navigationState.taskNumber === undefined ? {} : { taskNumber: navigationState.taskNumber }), readThread: readThread, loadChannels: loadChannels, loadThreadHistory: loadThreadHistory, subscribeChanges: subscribeChanges, loadMembers: loadMembers, reply: reply, changeTask: changeTask, t: t }, navigationState.threadRef);
|
|
11
|
+
return _jsx(TeamThreadPage, { workspaceId: current.workspaceId, putAttachment: putAttachment, taskRef: navigationState.taskRef, threadRef: navigationState.threadRef, backToWorkspace: backToWorkspace, selectChannel: selectChannel, selectThread: selectThread, resolveTaskRefs: resolveTaskRefs, ...(navigationState.channelRef === undefined ? {} : { channelRef: navigationState.channelRef }), ...(navigationState.taskNumber === undefined ? {} : { taskNumber: navigationState.taskNumber }), drafts: drafts, getAttachment: getAttachment, readThread: readThread, loadChannels: loadChannels, loadThreadHistory: loadThreadHistory, subscribeChanges: subscribeChanges, loadMembers: loadMembers, reply: reply, changeTask: changeTask, t: t }, navigationState.threadRef);
|
|
12
12
|
}
|
|
13
13
|
if (current !== undefined && navigationState.channelRef !== undefined) {
|
|
14
|
-
return _jsx(TeamChannelPage, { workspaceId: current.workspaceId, channelRef: navigationState.channelRef, loadChannels: loadChannels, subscribeChanges: subscribeChanges, loadMembers: loadMembers, sendMessage: sendMessage, joinChannel: joinChannel, removeChannelMember: removeChannelMember, selectThread: selectThread, backToChannels: backToChannels, t: t }, navigationState.channelRef);
|
|
14
|
+
return _jsx(TeamChannelPage, { workspaceId: current.workspaceId, channelRef: navigationState.channelRef, drafts: drafts, putAttachment: putAttachment, getAttachment: getAttachment, loadChannels: loadChannels, subscribeChanges: subscribeChanges, loadMembers: loadMembers, sendMessage: sendMessage, joinChannel: joinChannel, removeChannelMember: removeChannelMember, selectThread: selectThread, selectChannel: selectChannel, backToChannels: backToChannels, resolveTaskRefs: resolveTaskRefs, t: t }, navigationState.channelRef);
|
|
15
15
|
}
|
|
16
16
|
const welcome = current === undefined
|
|
17
17
|
? { eyebrow: t('teamMode'), title: t('team'), body: t('empty') }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import type { AgentTeamMemberId } from '@wowyuarm/dsh-agent-team/types';
|
|
2
|
+
import type { AgentTeamMemberId, AgentTeamMessageAttachment, AgentTeamTaskRef } from '@wowyuarm/dsh-agent-team/types';
|
|
3
|
+
import type { TeamConversationProps } from './slots.ts';
|
|
4
|
+
import { type ResolvedTaskRef } from './task-refs.ts';
|
|
3
5
|
export interface TeamMessageProps {
|
|
4
6
|
readonly senderName: string;
|
|
5
7
|
readonly memberId: AgentTeamMemberId;
|
|
@@ -12,8 +14,16 @@ export interface TeamMessageProps {
|
|
|
12
14
|
readonly senderTitle?: string;
|
|
13
15
|
/** Continuation of one same-sender run: suppress repeated identity chrome. */
|
|
14
16
|
readonly grouped?: boolean;
|
|
17
|
+
readonly attachments?: readonly AgentTeamMessageAttachment[] | undefined;
|
|
18
|
+
/** Cache readback for thumbnails; absent on surfaces without the remotes. */
|
|
19
|
+
readonly loadAttachment?: TeamConversationProps['getAttachment'] | undefined;
|
|
20
|
+
readonly t?: TeamConversationProps['t'] | undefined;
|
|
21
|
+
/** Resolve a branded ref found in the body; absent surfaces render refs as plain text. */
|
|
22
|
+
readonly onOpenRef?: ((ref: string) => void) | undefined;
|
|
23
|
+
/** Host lookup turning task refs into human-facing numbers; absent keeps raw refs. */
|
|
24
|
+
readonly onResolveTaskRefs?: ((taskRefs: readonly AgentTeamTaskRef[]) => Promise<readonly ResolvedTaskRef[]>) | undefined;
|
|
15
25
|
readonly children?: ReactNode;
|
|
16
26
|
}
|
|
17
27
|
/** One chat message row with identity chrome and sender-appropriate rendering. */
|
|
18
|
-
export declare function TeamMessage({ senderName, memberId, human, body, occurredAt, mentionNames, senderTitle, grouped, children }: TeamMessageProps): import("react").JSX.Element;
|
|
28
|
+
export declare function TeamMessage({ senderName, memberId, human, body, occurredAt, mentionNames, senderTitle, grouped, attachments, loadAttachment, t, onOpenRef, onResolveTaskRefs, children }: TeamMessageProps): import("react").JSX.Element;
|
|
19
29
|
//# sourceMappingURL=TeamMessage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamMessage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"TeamMessage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamMessage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2F,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAE/H,OAAO,KAAK,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AACrH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEvD,OAAO,EAA4E,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAI/H,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;IACpC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,uFAAuF;IACvF,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,0BAA0B,EAAE,GAAG,SAAS,CAAA;IACxE,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,CAAC,EAAE,qBAAqB,CAAC,eAAe,CAAC,GAAG,SAAS,CAAA;IAC5E,QAAQ,CAAC,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,GAAG,SAAS,CAAA;IACnD,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IACxD,sFAAsF;IACtF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,KAAK,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IACzH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAC9B;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,EAAE,SAAS,EAAE,iBAAiB,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BAkG1M"}
|
|
@@ -1,24 +1,162 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment } from 'react';
|
|
3
|
-
import { MarkdownText, MessageText } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
-
import {
|
|
2
|
+
import { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { MarkdownText, MessageText, Modal } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import { cachedAttachmentDataUrl, formatByteSize, loadAttachmentDataUrl } from "./attachment-preview.js";
|
|
5
|
+
import { cachedResolvedTaskRef, resolveUnknownTaskRefs, useResolvedTaskRefVersion } from "./task-refs.js";
|
|
6
|
+
import { formatMessageTime, isPlainTextBody, memberHue, splitBrandedRefs, splitMentionNames, stripAttachmentLines } from "./team-formatters.js";
|
|
5
7
|
import css from './conversation.module.css';
|
|
6
8
|
/** One chat message row with identity chrome and sender-appropriate rendering. */
|
|
7
|
-
export function TeamMessage({ senderName, memberId, human, body, occurredAt, mentionNames, senderTitle, grouped, children }) {
|
|
9
|
+
export function TeamMessage({ senderName, memberId, human, body, occurredAt, mentionNames, senderTitle, grouped, attachments, loadAttachment, t, onOpenRef, onResolveTaskRefs, children }) {
|
|
8
10
|
const avatarStyle = human ? undefined : { '--team-avatar-hue': memberHue(memberId) };
|
|
9
|
-
// Literal bodies carry
|
|
10
|
-
// prose Agent bodies where literal rendering loses nothing. Rich
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Literal bodies carry mention chips inline — Human input always, and
|
|
12
|
+
// plain-prose Agent bodies where literal rendering loses nothing. Rich
|
|
13
|
+
// Markdown keeps unmatched structured mentions in the trailing row.
|
|
14
|
+
// The stored body carries machine-facing `[attachment] <path>` prompt lines;
|
|
15
|
+
// humans see the attachment strip rendered from the message metadata instead.
|
|
16
|
+
const displayBody = stripAttachmentLines(body) === '' ? body : stripAttachmentLines(body);
|
|
17
|
+
const inline = (human || isPlainTextBody(displayBody)) && mentionNames !== undefined && mentionNames.length > 0
|
|
18
|
+
? splitMentionNames(displayBody, mentionNames)
|
|
15
19
|
: undefined;
|
|
16
20
|
const fallbackNames = inline === undefined ? (mentionNames ?? []) : inline.unmatched;
|
|
21
|
+
const richAgentBody = !human && !isPlainTextBody(displayBody);
|
|
22
|
+
// Non-Task branded refs retain the legacy fallback row. Task refs instead
|
|
23
|
+
// stay at their authored prose position; unresolved refs remain literal.
|
|
24
|
+
const fallbackRefs = richAgentBody && onOpenRef !== undefined
|
|
25
|
+
? splitBrandedRefs(displayBody).filter(segment => segment.ref !== undefined && !segment.ref.startsWith('task:')).map(segment => segment.ref)
|
|
26
|
+
: [];
|
|
27
|
+
// Resolved refs re-label once the Host lookup lands; the version token
|
|
28
|
+
// refreshes literal links and rendered Markdown prose.
|
|
29
|
+
const refVersion = useResolvedTaskRefVersion();
|
|
30
|
+
const bodyTaskRefs = onOpenRef === undefined || richAgentBody ? [] : splitBrandedRefs(displayBody)
|
|
31
|
+
.filter(segment => segment.ref !== undefined && segment.ref.startsWith('task:'))
|
|
32
|
+
.map(segment => segment.ref);
|
|
33
|
+
const bodyTaskRefKey = bodyTaskRefs.join(',');
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (onResolveTaskRefs === undefined || bodyTaskRefKey === '')
|
|
36
|
+
return;
|
|
37
|
+
void resolveUnknownTaskRefs(bodyTaskRefs, onResolveTaskRefs);
|
|
38
|
+
}, [onResolveTaskRefs, bodyTaskRefKey, refVersion]);
|
|
39
|
+
const taskLabel = useCallback((taskNumber) => t?.('taskLabel', { number: taskNumber }) ?? `Task #${taskNumber}`, [t]);
|
|
40
|
+
const markdownRef = useRef(null);
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
const root = markdownRef.current;
|
|
43
|
+
if (!richAgentBody || root === null || onOpenRef === undefined)
|
|
44
|
+
return;
|
|
45
|
+
const textNodes = markdownProseTextNodes(root);
|
|
46
|
+
const taskRefs = [...new Set(textNodes.flatMap(node => splitBrandedRefs(node.data)
|
|
47
|
+
.filter(segment => segment.ref?.startsWith('task:') === true)
|
|
48
|
+
.map(segment => segment.ref)))];
|
|
49
|
+
if (onResolveTaskRefs !== undefined && taskRefs.length > 0)
|
|
50
|
+
void resolveUnknownTaskRefs(taskRefs, onResolveTaskRefs);
|
|
51
|
+
for (const node of textNodes)
|
|
52
|
+
renderResolvedMarkdownTaskRefs(node, taskLabel);
|
|
53
|
+
for (const button of root.querySelectorAll('button[data-task-ref]')) {
|
|
54
|
+
const taskRef = button.dataset.taskRef;
|
|
55
|
+
const hit = taskRef === undefined ? undefined : cachedResolvedTaskRef(taskRef);
|
|
56
|
+
if (taskRef !== undefined && hit !== undefined)
|
|
57
|
+
button.textContent = taskLabel(hit.taskNumber);
|
|
58
|
+
}
|
|
59
|
+
}, [richAgentBody, onOpenRef, onResolveTaskRefs, refVersion, taskLabel]);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const root = markdownRef.current;
|
|
62
|
+
if (!richAgentBody || root === null || onOpenRef === undefined)
|
|
63
|
+
return;
|
|
64
|
+
const openTask = (event) => {
|
|
65
|
+
const target = event.target;
|
|
66
|
+
if (!(target instanceof Element))
|
|
67
|
+
return;
|
|
68
|
+
const button = target.closest('button[data-task-ref]');
|
|
69
|
+
if (button === null || !root.contains(button))
|
|
70
|
+
return;
|
|
71
|
+
const taskRef = button instanceof HTMLElement ? button.dataset.taskRef : undefined;
|
|
72
|
+
if (taskRef !== undefined)
|
|
73
|
+
onOpenRef(taskRef);
|
|
74
|
+
};
|
|
75
|
+
root.addEventListener('click', openTask);
|
|
76
|
+
return () => { root.removeEventListener('click', openTask); };
|
|
77
|
+
}, [richAgentBody, onOpenRef]);
|
|
17
78
|
return (_jsxs("article", { className: css.messageRow, "data-human": human || undefined, "data-grouped": grouped || undefined, children: [_jsx("div", { className: css.messageIdentity, style: avatarStyle, "aria-hidden": "true", children: senderName.replace('@', '').slice(0, 1).toUpperCase() }), _jsxs("div", { className: css.messageBody, children: [!grouped && (_jsxs("div", { className: css.nameRow, children: [_jsx("strong", { ...(senderTitle === undefined ? {} : { title: senderTitle }), children: senderName }), occurredAt !== undefined && _jsx("span", { className: css.messageTime, children: formatMessageTime(occurredAt) })] })), inline !== undefined
|
|
18
79
|
? _jsx("div", { className: css.messageText, children: inline.segments.map((segment, index) => segment.mention
|
|
19
80
|
? _jsx("span", { className: css.mention, children: segment.text }, index)
|
|
20
|
-
: _jsx(Fragment, { children: segment.text }, index)) })
|
|
21
|
-
: human
|
|
22
|
-
? _jsx("div", { className: css.messageText, children: _jsx(MessageText, { text:
|
|
23
|
-
: _jsx("div", { className: css.messageMarkdown, children: _jsx(MarkdownText, { text:
|
|
81
|
+
: _jsx(Fragment, { children: renderRefs(segment.text, onOpenRef, taskLabel) }, index)) })
|
|
82
|
+
: human || (onOpenRef !== undefined && isPlainTextBody(displayBody) && splitBrandedRefs(displayBody).some(segment => segment.ref !== undefined))
|
|
83
|
+
? _jsx("div", { className: css.messageText, children: onOpenRef === undefined ? _jsx(MessageText, { text: displayBody }) : renderRefs(displayBody, onOpenRef, taskLabel) })
|
|
84
|
+
: _jsx("div", { ref: markdownRef, className: css.messageMarkdown, children: _jsx(MarkdownText, { text: displayBody }, `${displayBody}:${onOpenRef === undefined ? 'literal' : 'refs'}`) }), (fallbackNames.length > 0 || fallbackRefs.length > 0) && (_jsxs("div", { className: css.mentionsRow, children: [fallbackNames.map(name => _jsxs("span", { className: css.mention, children: ["@", name] }, name)), fallbackRefs.map(ref => {
|
|
85
|
+
const resolved = cachedResolvedTaskRef(ref);
|
|
86
|
+
const label = resolved !== undefined && ref.startsWith('task:') ? taskLabel(resolved.taskNumber) : ref;
|
|
87
|
+
return _jsx("button", { type: "button", className: css.refLink, title: ref, onClick: () => { onOpenRef(ref); }, children: label }, ref);
|
|
88
|
+
})] })), attachments !== undefined && attachments.length > 0 && _jsx(TeamAttachmentStrip, { attachments: attachments, ...(loadAttachment === undefined ? {} : { loadAttachment }), ...(t === undefined ? {} : { t }) }), children] })] }));
|
|
89
|
+
}
|
|
90
|
+
/** Text nodes that Markdown rendered as prose rather than code or a link. */
|
|
91
|
+
function markdownProseTextNodes(root) {
|
|
92
|
+
const nodes = [];
|
|
93
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
94
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
95
|
+
const parent = node.parentElement;
|
|
96
|
+
if (parent !== null && parent.closest('code, pre, a, button') === null)
|
|
97
|
+
nodes.push(node);
|
|
98
|
+
}
|
|
99
|
+
return nodes;
|
|
100
|
+
}
|
|
101
|
+
/** Replace resolved refs in one prose text node without changing Markdown structure. */
|
|
102
|
+
function renderResolvedMarkdownTaskRefs(node, taskLabel) {
|
|
103
|
+
const segments = splitBrandedRefs(node.data);
|
|
104
|
+
if (!segments.some(segment => segment.ref?.startsWith('task:') === true && cachedResolvedTaskRef(segment.ref) !== undefined))
|
|
105
|
+
return;
|
|
106
|
+
const fragment = document.createDocumentFragment();
|
|
107
|
+
for (const segment of segments) {
|
|
108
|
+
const taskRef = segment.ref?.startsWith('task:') === true ? segment.ref : undefined;
|
|
109
|
+
if (taskRef === undefined) {
|
|
110
|
+
fragment.append(segment.text);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const resolved = cachedResolvedTaskRef(taskRef);
|
|
114
|
+
if (resolved === undefined) {
|
|
115
|
+
fragment.append(segment.text);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const button = document.createElement('button');
|
|
119
|
+
button.type = 'button';
|
|
120
|
+
if (css.refLink !== undefined)
|
|
121
|
+
button.className = css.refLink;
|
|
122
|
+
button.dataset.taskRef = taskRef;
|
|
123
|
+
button.title = taskRef;
|
|
124
|
+
button.textContent = taskLabel(resolved.taskNumber);
|
|
125
|
+
fragment.append(button);
|
|
126
|
+
}
|
|
127
|
+
node.replaceWith(fragment);
|
|
128
|
+
}
|
|
129
|
+
/** Render one literal text run, linkifying branded refs when navigation is available. */
|
|
130
|
+
function renderRefs(text, onOpenRef, taskLabel) {
|
|
131
|
+
if (onOpenRef === undefined)
|
|
132
|
+
return text;
|
|
133
|
+
return splitBrandedRefs(text).map((segment, index) => {
|
|
134
|
+
if (segment.ref === undefined)
|
|
135
|
+
return _jsx(Fragment, { children: segment.text }, index);
|
|
136
|
+
const resolved = cachedResolvedTaskRef(segment.ref);
|
|
137
|
+
const label = resolved !== undefined && segment.ref.startsWith('task:') ? taskLabel(resolved.taskNumber) : segment.ref;
|
|
138
|
+
return _jsx("button", { type: "button", className: css.refLink, title: segment.ref, onClick: () => { onOpenRef(segment.ref); }, children: label }, index);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** One message's attachment strip: image thumbnails with a large view, or name chips when bytes are gone. */
|
|
142
|
+
function TeamAttachmentStrip({ attachments, loadAttachment, t }) {
|
|
143
|
+
const [zoomed, setZoomed] = useState();
|
|
144
|
+
return _jsxs("div", { className: css.attachmentStrip, children: [attachments.map(attachment => _jsx(TeamAttachment, { attachment: attachment, ...(loadAttachment === undefined ? {} : { loadAttachment }), ...(t === undefined ? {} : { t }), onZoom: setZoomed }, attachment.attachmentId)), zoomed !== undefined && _jsx(Modal, { open: true, ...(css.attachmentModal === undefined ? {} : { className: css.attachmentModal }), title: zoomed.name, onClose: () => { setZoomed(undefined); }, children: _jsx("img", { className: css.attachmentZoom, src: cachedAttachmentDataUrl(zoomed.attachmentId) ?? undefined, alt: zoomed.name }) })] });
|
|
145
|
+
}
|
|
146
|
+
function TeamAttachment({ attachment, loadAttachment, t, onZoom }) {
|
|
147
|
+
const wantsPreview = loadAttachment !== undefined && attachment.mediaType.startsWith('image/');
|
|
148
|
+
const [dataUrl, setDataUrl] = useState(wantsPreview ? cachedAttachmentDataUrl(attachment.attachmentId) : null);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!wantsPreview || dataUrl !== undefined)
|
|
151
|
+
return;
|
|
152
|
+
let mounted = true;
|
|
153
|
+
void loadAttachmentDataUrl(loadAttachment, attachment).then(url => { if (mounted)
|
|
154
|
+
setDataUrl(url); });
|
|
155
|
+
return () => { mounted = false; };
|
|
156
|
+
}, [wantsPreview, dataUrl, loadAttachment, attachment]);
|
|
157
|
+
const expired = t?.('attachmentExpired') ?? 'File no longer cached';
|
|
158
|
+
if (wantsPreview && dataUrl !== null) {
|
|
159
|
+
return _jsx("button", { type: "button", className: css.attachmentThumb, "aria-label": t?.('viewImage', { name: attachment.name }) ?? attachment.name, title: attachment.name, onClick: () => { onZoom(attachment); }, children: _jsx("img", { src: dataUrl, alt: attachment.name }) });
|
|
160
|
+
}
|
|
161
|
+
return _jsxs("span", { className: css.attachmentChip, title: wantsPreview ? expired : `${attachment.name} · ${formatByteSize(attachment.byteSize)}`, children: [_jsx("span", { className: css.attachmentChipName, children: attachment.name }), _jsx("span", { className: css.attachmentChipSize, children: wantsPreview ? expired : formatByteSize(attachment.byteSize) })] });
|
|
24
162
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit boundary between two same-sender Messages of one run separated by
|
|
3
|
+
* a real waiting gap: the hairline restores the block boundary that grouping
|
|
4
|
+
* removed, and the label below it restores the instant that the suppressed
|
|
5
|
+
* identity chrome would have shown.
|
|
6
|
+
*/
|
|
7
|
+
export declare function TeamRunDivider({ occurredAt }: {
|
|
8
|
+
readonly occurredAt: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=TeamRunDivider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TeamRunDivider.d.ts","sourceRoot":"","sources":["../../../src/client/TeamRunDivider.tsx"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,+BAM7E"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { formatMessageTime } from "./team-formatters.js";
|
|
3
|
+
import css from './conversation.module.css';
|
|
4
|
+
/**
|
|
5
|
+
* Explicit boundary between two same-sender Messages of one run separated by
|
|
6
|
+
* a real waiting gap: the hairline restores the block boundary that grouping
|
|
7
|
+
* removed, and the label below it restores the instant that the suppressed
|
|
8
|
+
* identity chrome would have shown.
|
|
9
|
+
*/
|
|
10
|
+
export function TeamRunDivider({ occurredAt }) {
|
|
11
|
+
return (_jsx("div", { className: css.runDivider, role: "separator", children: _jsx("time", { dateTime: occurredAt, children: formatMessageTime(occurredAt) }) }));
|
|
12
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTeamChannelRef, AgentTeamTaskRef, AgentTeamThreadRef } from '@wowyuarm/dsh-agent-team/types';
|
|
2
2
|
import type { WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client';
|
|
3
3
|
import type { TeamConversationProps } from './slots.ts';
|
|
4
|
+
import type { TeamDraftStore } from './drafts.ts';
|
|
4
5
|
interface TeamThreadPageProps {
|
|
5
6
|
readonly workspaceId: WorkspaceId;
|
|
6
7
|
readonly channelRef?: AgentTeamChannelRef;
|
|
@@ -13,8 +14,14 @@ interface TeamThreadPageProps {
|
|
|
13
14
|
readonly loadThreadHistory: TeamConversationProps['loadThreadHistory'];
|
|
14
15
|
readonly subscribeChanges: TeamConversationProps['subscribeChanges'];
|
|
15
16
|
readonly loadMembers: TeamConversationProps['loadMembers'];
|
|
17
|
+
readonly drafts: TeamDraftStore;
|
|
18
|
+
readonly getAttachment: TeamConversationProps['getAttachment'];
|
|
16
19
|
readonly reply: TeamConversationProps['reply'];
|
|
17
20
|
readonly changeTask: TeamConversationProps['changeTask'];
|
|
21
|
+
readonly putAttachment: TeamConversationProps['putAttachment'];
|
|
22
|
+
readonly selectChannel: TeamConversationProps['selectChannel'];
|
|
23
|
+
readonly selectThread: TeamConversationProps['selectThread'];
|
|
24
|
+
readonly resolveTaskRefs: TeamConversationProps['resolveTaskRefs'];
|
|
18
25
|
readonly t: TeamConversationProps['t'];
|
|
19
26
|
}
|
|
20
27
|
export declare function TeamThreadPage(props: TeamThreadPageProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamThreadPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamThreadPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"TeamThreadPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamThreadPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,mBAAmB,EAInB,gBAAgB,EAGhB,kBAAkB,EAEnB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAEzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AACvD,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,aAAa,CAAA;AAa/D,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,mBAAmB,CAAA;IACzC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAA;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAA;IACxD,QAAQ,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAA;IACtE,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9C,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAA;IACxD,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AA8BD,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,+BAgkBxD"}
|