@wowyuarm/dsh-agent-team 0.1.10 → 0.1.12
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 +17 -4
- package/README.zh.md +17 -4
- package/package.json +53 -47
- package/packages/agent-team/README.md +6 -4
- package/packages/agent-team/README.zh.md +6 -4
- package/packages/agent-team/lib/context-management.js +40 -50
- package/packages/agent-team/lib/context-projection.js +15 -1
- package/packages/agent-team/lib/index.js +244 -154
- package/packages/agent-team/lib/invariant.js +38 -4
- package/packages/agent-team/lib/ledger.js +775 -383
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-runtime.js +47 -9
- package/packages/agent-team/lib/member-time-context.js +20 -5
- package/packages/agent-team/lib/mentions.js +56 -0
- package/packages/agent-team/lib/pressure-policy.js +33 -9
- package/packages/agent-team/lib/session-event-cursor.js +116 -0
- package/packages/agent-team/lib/session-remediation.js +38 -26
- package/packages/agent-team/lib/spec.js +47 -34
- package/packages/agent-team/lib/stored-session-reader.js +138 -0
- package/packages/agent-team/lib/typert.host.js +157 -41
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +129 -32
- package/packages/agent-team/lib/types/context-management.d.ts +7 -0
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +77 -7
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +251 -20
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +2 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/mentions.d.ts +34 -0
- package/packages/agent-team/lib/types/mentions.d.ts.map +1 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
- package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
- package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts +23 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -1
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
- package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
- package/packages/agent-team/lib/types/types/entities.d.ts +28 -20
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +87 -4
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +49 -7
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +2357 -1207
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +27 -7
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +18 -0
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +22 -0
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -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 +167 -35
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +23 -27
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +26 -26
- 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 +9 -2
- package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamCountBadge.js +25 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +25 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +139 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
- package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts +43 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberRow.js +27 -0
- package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts +2 -2
- package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMembersAction.js +2 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -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 +11 -4
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +108 -30
- 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 +68 -7
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +19 -9
- package/packages/client-agent-team/lib/types/client/locales.d.ts +40 -2
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +40 -2
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +7 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts +33 -2
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.js +107 -25
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +43 -1
- 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 +81 -6
- package/packages/tool-agent-team/README.md +2 -2
- package/packages/tool-agent-team/README.zh.md +2 -2
- package/packages/tool-agent-team/lib/context-tools.js +18 -14
- package/packages/tool-agent-team/lib/host-access.js +20 -0
- package/packages/tool-agent-team/lib/index.js +44 -32
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
- package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/progress-nudge.js +0 -328
- package/packages/agent-team/lib/types/progress-nudge.d.ts +0 -136
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts +0 -3
- package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/TeamSettings.js +0 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamAgentsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamAgentsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,2BAA2B,EAE5B,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"TeamAgentsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamAgentsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,2BAA2B,EAE5B,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAalD,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,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,aAAa,EAAE,gBAAgB,CAAC,eAAe,CAAC,CAAA;IACzD,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,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC,EAAE,EAAE,oBAAoB,+BA0O/N"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Button, IconArchiveOutline20, IconEditOutline16, IconPlayOutline16, IconPlusOutline16, IconRefreshOutline16, Input, Modal, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
-
import {
|
|
4
|
+
import { TeamMemberIdentity } from "./TeamMemberRow.js";
|
|
5
5
|
import { SortableRow, useSidebarRowDrag } from "./sidebar-drag.js";
|
|
6
6
|
import { moveSidebarItem, useSidebarOrder } from "./sidebar-order.js";
|
|
7
7
|
import { useSidebarSectionOpen, setSidebarSectionOpen } from "./sidebar-sections.js";
|
|
8
8
|
import { mintRequestId } from "./requests.js";
|
|
9
|
+
import { diagnosticText, restartOffered } from "./TeamPresenceDot.js";
|
|
9
10
|
import { TeamRowMenu } from "./TeamRowMenu.js";
|
|
10
11
|
import { TeamSidebarSection } from "./TeamSidebarSection.js";
|
|
11
12
|
import { AgentEditorDialog, ModelPickerField, sameModel } from "./TeamMemberEditor.js";
|
|
@@ -87,8 +88,12 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
}, []);
|
|
91
|
+
// Only the first refresh owns the loading surface; presence wakes (Agent
|
|
92
|
+
// running/idle) ride the dedicated scope and refresh rows in place.
|
|
93
|
+
const loadedRef = useRef(false);
|
|
90
94
|
const refresh = useCallback(async () => {
|
|
91
|
-
|
|
95
|
+
if (!loadedRef.current)
|
|
96
|
+
setLoading(true);
|
|
92
97
|
const result = await loadMembers({ workspaceId });
|
|
93
98
|
if (result.ok) {
|
|
94
99
|
// Archived Members are hidden from every surface; the row disappears
|
|
@@ -97,6 +102,7 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
97
102
|
setMembers(next);
|
|
98
103
|
followRollover(next);
|
|
99
104
|
setError(undefined);
|
|
105
|
+
loadedRef.current = true;
|
|
100
106
|
}
|
|
101
107
|
else {
|
|
102
108
|
setError(result.error.message);
|
|
@@ -113,6 +119,15 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
113
119
|
// rides every workspace invalidation.
|
|
114
120
|
void refresh();
|
|
115
121
|
}), [subscribeChanges, refresh, workspaceId]);
|
|
122
|
+
// Presence transitions commit nothing: the Host wakes only this scope, so
|
|
123
|
+
// the green dots stay live without any catalog refetch.
|
|
124
|
+
useEffect(() => subscribeChanges({ kind: 'presence', workspaceId }, update => {
|
|
125
|
+
if (update.type === 'failed') {
|
|
126
|
+
setError(update.message);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
void refresh();
|
|
130
|
+
}), [subscribeChanges, refresh, workspaceId]);
|
|
116
131
|
const closeForm = () => {
|
|
117
132
|
if (creating)
|
|
118
133
|
return;
|
|
@@ -138,7 +153,7 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
138
153
|
setModel(undefined);
|
|
139
154
|
setFormOpen(false);
|
|
140
155
|
if (result.value.status.presence === 'unavailable') {
|
|
141
|
-
setError(result.value.status
|
|
156
|
+
setError(diagnosticText(result.value.status) || t('statusUnavailable'));
|
|
142
157
|
}
|
|
143
158
|
else {
|
|
144
159
|
setRetryRequest(undefined);
|
|
@@ -180,7 +195,7 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
|
|
|
180
195
|
...(model === undefined ? {} : { model }),
|
|
181
196
|
});
|
|
182
197
|
};
|
|
183
|
-
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 }), formOpen && error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('agents'), open: sectionOpen, onToggle: open => { setSidebarSectionOpen(workspaceId, 'agents', open); }, 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 }), updateMember: updateMember, recoverMember: recoverMember, archiveMember: archiveMember, 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') })] }))] }));
|
|
198
|
+
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 }), formOpen && error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('agents'), open: sectionOpen, onToggle: open => { setSidebarSectionOpen(workspaceId, 'agents', open); }, 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 && error === undefined && 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 }), updateMember: updateMember, recoverMember: recoverMember, archiveMember: archiveMember, 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') })] }))] }));
|
|
184
199
|
}
|
|
185
200
|
/**
|
|
186
201
|
* One sidebar Agent row: the select button opens the Member's own Session
|
|
@@ -207,7 +222,12 @@ function AgentRow({ status, current, updateMember, recoverMember, archiveMember,
|
|
|
207
222
|
return;
|
|
208
223
|
}
|
|
209
224
|
if (result.value.status.availability === 'unavailable') {
|
|
210
|
-
|
|
225
|
+
const diagnostic = result.value.status.diagnostic;
|
|
226
|
+
if (diagnostic?.class === 'session-refused' && diagnostic.remediable === false) {
|
|
227
|
+
setRowAlert(t('restartRefusedNoRestart', { detail: diagnosticText(result.value.status) }));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
setRowAlert(t('restartStillUnavailable', { diagnostic: diagnosticText(result.value.status) || t('statusUnavailable') }));
|
|
211
231
|
return;
|
|
212
232
|
}
|
|
213
233
|
setRowAlert(undefined);
|
|
@@ -235,10 +255,10 @@ function AgentRow({ status, current, updateMember, recoverMember, archiveMember,
|
|
|
235
255
|
setRowAlert(t('archiveAgentFailed', { message: cause instanceof Error ? cause.message : String(cause) }));
|
|
236
256
|
}
|
|
237
257
|
};
|
|
238
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.agentRow, "data-menu-open": menuOpen || undefined, children: [
|
|
258
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.agentRow, "data-agent-row": true, "data-menu-open": menuOpen || undefined, children: [_jsx("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(TeamMemberIdentity, { status: status, name: status.member.handle.replace(/^@/, ''), className: css.agentCopy, t: t }) }), _jsx("span", { className: css.rowMenu, children: _jsx(TeamRowMenu, { label: t('actionsAgent', { name: status.member.handle }), items: [
|
|
239
259
|
{ id: 'edit', label: t('editAgent'), icon: _jsx(IconEditOutline16, {}) },
|
|
240
260
|
...(status.presence === 'error' ? [{ id: 'resume', label: t('resumeAgent'), icon: _jsx(IconPlayOutline16, {}) }] : []),
|
|
241
|
-
...(status.availability === 'unavailable' ? [{ id: 'restart', label: t('restartAgent'), icon: _jsx(IconRefreshOutline16, {}) }] : []),
|
|
261
|
+
...(status.availability === 'unavailable' && restartOffered(status) ? [{ id: 'restart', label: t('restartAgent'), icon: _jsx(IconRefreshOutline16, {}) }] : []),
|
|
242
262
|
{ id: 'archive', label: t('archiveAgent'), icon: _jsx(IconArchiveOutline20, { size: 16 }), danger: true },
|
|
243
263
|
], onSelect: (id) => {
|
|
244
264
|
if (id === 'edit')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AgentTeamMemberId } from '@wowyuarm/dsh-agent-team/types';
|
|
2
|
+
/** One owner as the stack draws it: the id carries the hue, the name the initial. */
|
|
3
|
+
export interface TeamAvatarOwner {
|
|
4
|
+
readonly memberId: AgentTeamMemberId;
|
|
5
|
+
/** Public handle, or the raw Member id when the roster no longer names them. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The compact "who is on this work" stack: overlapping 18px Member circles in
|
|
10
|
+
* the shared identity language, capped at three plus a `+N` chip. The circles
|
|
11
|
+
* are presentational, so the stack is one `role="img"` whose label carries the
|
|
12
|
+
* whole roster — three anonymous initials would read as noise.
|
|
13
|
+
*/
|
|
14
|
+
export declare function TeamAvatarStack({ owners, label }: {
|
|
15
|
+
readonly owners: readonly TeamAvatarOwner[];
|
|
16
|
+
readonly label: string;
|
|
17
|
+
}): import("react").JSX.Element | null;
|
|
18
|
+
//# sourceMappingURL=TeamAvatarStack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TeamAvatarStack.d.ts","sourceRoot":"","sources":["../../../src/client/TeamAvatarStack.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAOvE,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;IACpC,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IACjD,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAA;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB,sCAQA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memberHue } from "./team-formatters.js";
|
|
3
|
+
import css from './avatar-stack.module.css';
|
|
4
|
+
/** Distinct owners past this count collapse into one `+N` chip. */
|
|
5
|
+
const MAX_VISIBLE = 3;
|
|
6
|
+
/**
|
|
7
|
+
* The compact "who is on this work" stack: overlapping 18px Member circles in
|
|
8
|
+
* the shared identity language, capped at three plus a `+N` chip. The circles
|
|
9
|
+
* are presentational, so the stack is one `role="img"` whose label carries the
|
|
10
|
+
* whole roster — three anonymous initials would read as noise.
|
|
11
|
+
*/
|
|
12
|
+
export function TeamAvatarStack({ owners, label }) {
|
|
13
|
+
if (owners.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
const shown = owners.slice(0, MAX_VISIBLE);
|
|
16
|
+
const overflow = owners.length - shown.length;
|
|
17
|
+
return _jsxs("span", { className: css.stack, role: "img", "aria-label": label, children: [shown.map(owner => _jsx("span", { className: css.avatar, style: { '--team-avatar-hue': memberHue(owner.memberId) }, children: initial(owner.name) }, owner.memberId)), overflow > 0 && _jsx("span", { className: css.overflow, children: `+${overflow}` })] });
|
|
18
|
+
}
|
|
19
|
+
/** First visible character of a handle — and of a raw Member id when that is all there is. */
|
|
20
|
+
function initial(name) {
|
|
21
|
+
return name.replace(/^@/, '').replace(/^member:/, '').slice(0, 1).toUpperCase();
|
|
22
|
+
}
|
|
@@ -8,6 +8,8 @@ interface TeamChannelPageProps {
|
|
|
8
8
|
readonly loadChannels: TeamConversationProps['loadChannels'];
|
|
9
9
|
readonly subscribeChanges: TeamConversationProps['subscribeChanges'];
|
|
10
10
|
readonly loadMembers: TeamConversationProps['loadMembers'];
|
|
11
|
+
/** The Human's own unread per Thread: the Host's three-class judgement, never a Client guess. */
|
|
12
|
+
readonly loadInbox: TeamConversationProps['loadInbox'];
|
|
11
13
|
readonly drafts: TeamDraftStore;
|
|
12
14
|
readonly getAttachment: TeamConversationProps['getAttachment'];
|
|
13
15
|
readonly putAttachment: TeamConversationProps['putAttachment'];
|
|
@@ -20,6 +22,6 @@ interface TeamChannelPageProps {
|
|
|
20
22
|
readonly backToChannels: TeamConversationProps['backToChannels'];
|
|
21
23
|
readonly t: TeamConversationProps['t'];
|
|
22
24
|
}
|
|
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;
|
|
25
|
+
export declare function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscribeChanges, loadMembers, loadInbox, drafts, putAttachment, getAttachment, sendMessage, joinChannel, removeChannelMember, selectThread, selectChannel, backToChannels, resolveTaskRefs, t }: TeamChannelPageProps): import("react").JSX.Element;
|
|
24
26
|
export {};
|
|
25
27
|
//# 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,EAE7D,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;
|
|
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,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEvD,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,aAAa,CAAA;AAiB/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,iGAAiG;IACjG,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAA;IACtD,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;AA0DD,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,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,+BAkX9R"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useEffect, useRef, useState
|
|
2
|
+
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Button, IconChevronLeftOutline14, IconChevronRightOutline14, Modal } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
-
import {
|
|
5
|
-
import { mintRequestId } from "./requests.js";
|
|
4
|
+
import { mintRequestId, uploadComposerFiles } from "./requests.js";
|
|
6
5
|
import { TeamComposer } from "./TeamComposer.js";
|
|
7
|
-
import {
|
|
6
|
+
import { TeamMemberRow } from "./TeamMemberRow.js";
|
|
8
7
|
import { TeamMessage } from "./TeamMessage.js";
|
|
8
|
+
import { TeamAvatarStack } from "./TeamAvatarStack.js";
|
|
9
|
+
import { TeamCountBadge } from "./TeamCountBadge.js";
|
|
9
10
|
import { TeamRunDivider } from "./TeamRunDivider.js";
|
|
10
|
-
import { formatTaskStatus, taskStatusDot, mentionNamesOf } from "./team-formatters.js";
|
|
11
|
+
import { formatAbsoluteTime, formatInboxTime, formatTaskStatus, taskStatusDot, mentionNamesOf } from "./team-formatters.js";
|
|
11
12
|
import { TeamStateDot } from "./TeamStateDot.js";
|
|
12
13
|
import { useChannelMembership } from "./team-membership.js";
|
|
13
14
|
import { useTimelineScroll } from "./timeline-scroll.js";
|
|
@@ -17,12 +18,14 @@ import channelCss from './channel.module.css';
|
|
|
17
18
|
import css from './conversation.module.css';
|
|
18
19
|
import threadCss from './thread.module.css';
|
|
19
20
|
/**
|
|
20
|
-
* Merge the freshest top-level window over what the reader already has
|
|
21
|
-
*
|
|
21
|
+
* Merge the freshest top-level window over what the reader already has. The
|
|
22
|
+
* fresh window is authoritative for every Message it covers — a change wake
|
|
23
|
+
* must move that row's live Task state, newest instant, and unread with it —
|
|
24
|
+
* while older Messages loaded earlier are retained instead of discarded.
|
|
22
25
|
*/
|
|
23
26
|
function mergeChannelView(current, fresh) {
|
|
24
|
-
const
|
|
25
|
-
const items = [...
|
|
27
|
+
const freshRefs = new Set(fresh.items.map(item => item.message.messageRef));
|
|
28
|
+
const items = [...current.items.filter(item => !freshRefs.has(item.message.messageRef)), ...fresh.items]
|
|
26
29
|
.sort((left, right) => left.message.sequence - right.message.sequence);
|
|
27
30
|
return {
|
|
28
31
|
...fresh,
|
|
@@ -32,9 +35,47 @@ function mergeChannelView(current, fresh) {
|
|
|
32
35
|
hasMore: fresh.hasMore || current.cursor < fresh.cursor,
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The people the Channel feed shows on one Task: owners of its live Claims, in
|
|
40
|
+
* claim order, deduped. Claim state is a Host fact, not decoration — a released
|
|
41
|
+
* Claim is no longer work — and only a Task still in progress or review has
|
|
42
|
+
* any, because a done or closed Task keeps its Claims as history that the state
|
|
43
|
+
* word already tells the reader.
|
|
44
|
+
*/
|
|
45
|
+
function liveOwnersByTask(items, claims) {
|
|
46
|
+
const live = new Set(items
|
|
47
|
+
.flatMap(item => item.task === undefined ? [] : [item.task])
|
|
48
|
+
.filter(task => task.status === 'in_progress' || task.status === 'in_review')
|
|
49
|
+
.map(task => task.taskRef));
|
|
50
|
+
const owners = new Map();
|
|
51
|
+
for (const claim of claims) {
|
|
52
|
+
if (!live.has(claim.taskRef) || claim.state === 'released')
|
|
53
|
+
continue;
|
|
54
|
+
const known = owners.get(claim.taskRef);
|
|
55
|
+
if (known === undefined)
|
|
56
|
+
owners.set(claim.taskRef, [claim.owner]);
|
|
57
|
+
else if (!known.includes(claim.owner))
|
|
58
|
+
known.push(claim.owner);
|
|
59
|
+
}
|
|
60
|
+
return owners;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The newest fact instant, or nothing when the entry's own Message is still the
|
|
64
|
+
* newest fact on its Thread. That difference is exactly what "has follow-up
|
|
65
|
+
* activity" means, and the Host projects both instants from the ledger.
|
|
66
|
+
*/
|
|
67
|
+
function followUpAt(item) {
|
|
68
|
+
const lastActivityAt = item.lastActivityAt;
|
|
69
|
+
return lastActivityAt === undefined || lastActivityAt === item.message.occurredAt ? undefined : lastActivityAt;
|
|
70
|
+
}
|
|
71
|
+
/** Host unread per Thread, keyed for the feed's rows: zero unread is the absence of a badge, not a row. */
|
|
72
|
+
function unreadCounts(inbox) {
|
|
73
|
+
return new Map(inbox.items.filter(item => item.unreadCount > 0).map(item => [item.thread.threadRef, item.unreadCount]));
|
|
74
|
+
}
|
|
75
|
+
export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscribeChanges, loadMembers, loadInbox, drafts, putAttachment, getAttachment, sendMessage, joinChannel, removeChannelMember, selectThread, selectChannel, backToChannels, resolveTaskRefs, t }) {
|
|
36
76
|
const [view, setView] = useState();
|
|
37
77
|
const [members, setMembers] = useState([]);
|
|
78
|
+
const [unreadByThread, setUnreadByThread] = useState(new Map());
|
|
38
79
|
const [error, setError] = useState();
|
|
39
80
|
const [pendingFiles, setPendingFiles] = useState([]);
|
|
40
81
|
const [statusMessage, setStatusMessage] = useState();
|
|
@@ -45,11 +86,15 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
45
86
|
const [managingMembers, setManagingMembers] = useState(false);
|
|
46
87
|
// The composer draft lives in the keyed draft cache: view switches unmount
|
|
47
88
|
// this page, and a refresh must not cost the half-written message either.
|
|
89
|
+
// The composer owns the subscription — this page only reads a snapshot when
|
|
90
|
+
// it sends, so typing never re-renders the timeline.
|
|
48
91
|
const draftKey = `channel:${channelRef}`;
|
|
49
|
-
const { draft, recipients } = useSyncExternalStore(drafts.subscribe, () => drafts.getSnapshot(draftKey));
|
|
50
92
|
const manageTriggerRef = useRef(null);
|
|
51
93
|
const memberListRef = useRef(null);
|
|
52
94
|
const mountedRef = useRef(false);
|
|
95
|
+
// Flips after the first successful timeline load; later change wakes
|
|
96
|
+
// refresh in place instead of showing the loading surface again.
|
|
97
|
+
const loadedRef = useRef(false);
|
|
53
98
|
const refreshSequenceRef = useRef(0);
|
|
54
99
|
const channelLastItem = view?.items[view.items.length - 1];
|
|
55
100
|
// Branded-ref navigation for message bodies: channel refs hop directly,
|
|
@@ -100,33 +145,55 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
100
145
|
// Presence counts ride the header meta line; error and unavailable do not count as online.
|
|
101
146
|
const onlineCount = channelMembers.filter(status => status.presence === 'available' || status.presence === 'working').length;
|
|
102
147
|
const messageSender = (item) => item.message.sender;
|
|
103
|
-
const
|
|
148
|
+
const handleByMember = new Map(members.map(status => [status.member.memberId, status.member.handle.replace(/^@/, '')]));
|
|
149
|
+
// Claim owners ride the same Host projection as everything else; the map is
|
|
150
|
+
// rebuilt per render because the window is small and identity must never lag
|
|
151
|
+
// a change wake.
|
|
152
|
+
const ownersByTask = liveOwnersByTask(view?.items ?? [], view?.claims ?? []);
|
|
104
153
|
const refresh = async (clearError = false) => {
|
|
105
154
|
if (!mountedRef.current)
|
|
106
155
|
return false;
|
|
107
156
|
const sequence = refreshSequenceRef.current + 1;
|
|
108
157
|
refreshSequenceRef.current = sequence;
|
|
109
|
-
|
|
158
|
+
// Only the first refresh owns the loading surface; change wakes refresh
|
|
159
|
+
// the rendered timeline in place instead of flashing it back to skeleton.
|
|
160
|
+
if (!loadedRef.current)
|
|
161
|
+
setLoading(true);
|
|
110
162
|
if (clearError) {
|
|
111
163
|
setError(undefined);
|
|
112
164
|
setStatusMessage(undefined);
|
|
113
165
|
}
|
|
114
166
|
try {
|
|
115
|
-
const [loaded, loadedMembers] = await Promise.all([
|
|
167
|
+
const [loaded, loadedMembers, loadedInbox] = await Promise.all([
|
|
116
168
|
loadChannels({ workspaceId, channelRef, direction: 'before', topLevelOnly: true, includeActivities: false, limit: 20 }),
|
|
117
169
|
loadMembers({ workspaceId }),
|
|
170
|
+
// The non-direct slice of the Host's own Inbox is the authority for
|
|
171
|
+
// "what needs me on this Thread" — following activity, mentions, and my
|
|
172
|
+
// Task/Claim changes alike. Nothing here re-derives unread from message
|
|
173
|
+
// mentions.
|
|
174
|
+
loadInbox({ workspaceId, limit: 100 }),
|
|
118
175
|
]);
|
|
119
176
|
if (!mountedRef.current || sequence !== refreshSequenceRef.current)
|
|
120
177
|
return false;
|
|
121
|
-
if (loaded.ok)
|
|
178
|
+
if (loaded.ok) {
|
|
122
179
|
setView(current => current === undefined ? loaded.value : mergeChannelView(current, loaded.value));
|
|
180
|
+
loadedRef.current = true;
|
|
181
|
+
}
|
|
123
182
|
else
|
|
124
183
|
setError(loaded.error.message);
|
|
125
184
|
if (loadedMembers.ok)
|
|
126
185
|
setMembers(loadedMembers.value);
|
|
127
186
|
else
|
|
128
187
|
setError(loadedMembers.error.message);
|
|
129
|
-
|
|
188
|
+
// A failed unread read drops the badges instead of leaving counts the
|
|
189
|
+
// reader can no longer trust; the failure surfaces like any other read.
|
|
190
|
+
if (loadedInbox.ok)
|
|
191
|
+
setUnreadByThread(unreadCounts(loadedInbox.value));
|
|
192
|
+
else {
|
|
193
|
+
setUnreadByThread(new Map());
|
|
194
|
+
setError(loadedInbox.error.message);
|
|
195
|
+
}
|
|
196
|
+
return loaded.ok && loadedMembers.ok && loadedInbox.ok;
|
|
130
197
|
}
|
|
131
198
|
catch (cause) {
|
|
132
199
|
if (mountedRef.current && sequence === refreshSequenceRef.current)
|
|
@@ -159,6 +226,7 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
159
226
|
};
|
|
160
227
|
useEffect(() => {
|
|
161
228
|
mountedRef.current = true;
|
|
229
|
+
loadedRef.current = false;
|
|
162
230
|
setView(undefined);
|
|
163
231
|
setError(undefined);
|
|
164
232
|
setLoading(true);
|
|
@@ -183,6 +251,17 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
183
251
|
}
|
|
184
252
|
void refreshMembers();
|
|
185
253
|
}),
|
|
254
|
+
// Presence transitions commit nothing: only the member rows move, so
|
|
255
|
+
// the header presence counts refresh without a timeline refetch.
|
|
256
|
+
subscribeChanges({ kind: 'presence', workspaceId }, update => {
|
|
257
|
+
if (!mountedRef.current)
|
|
258
|
+
return;
|
|
259
|
+
if (update.type === 'failed') {
|
|
260
|
+
setError(update.message);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
void refreshMembers();
|
|
264
|
+
}),
|
|
186
265
|
];
|
|
187
266
|
return () => {
|
|
188
267
|
mountedRef.current = false;
|
|
@@ -227,7 +306,16 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
227
306
|
// Retained across transport failures so a replayed send dedupes on the Host;
|
|
228
307
|
// definitive outcomes (committed or rejected) start the next send fresh.
|
|
229
308
|
const pendingSendId = useRef();
|
|
309
|
+
// Editing the draft invalidates the one-shot status line. The setter returns
|
|
310
|
+
// the same state when there is nothing to clear, so a keystroke does not
|
|
311
|
+
// re-render the timeline now that the composer owns the draft.
|
|
312
|
+
const clearSendState = useCallback(() => {
|
|
313
|
+
setStatusMessage(current => current === undefined ? current : undefined);
|
|
314
|
+
}, []);
|
|
230
315
|
const send = async () => {
|
|
316
|
+
// Read the draft at send time: this page no longer subscribes to it, so a
|
|
317
|
+
// captured render value would be stale after the composer's own edits.
|
|
318
|
+
const { draft, recipients } = drafts.getSnapshot(draftKey);
|
|
231
319
|
if (pending || draft.trim() === '')
|
|
232
320
|
return;
|
|
233
321
|
const recipientIds = [...recipients].sort();
|
|
@@ -239,22 +327,13 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
239
327
|
try {
|
|
240
328
|
// Upload chosen files first; any failure aborts the send with the
|
|
241
329
|
// existing error surface and keeps the chips for a retry.
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
name: file.name,
|
|
248
|
-
mediaType: file.type === '' ? undefined : file.type,
|
|
249
|
-
bytesBase64: bytesToBase64(new Uint8Array(await file.arrayBuffer())),
|
|
250
|
-
});
|
|
251
|
-
if (!uploaded.ok) {
|
|
252
|
-
pendingSendId.current = undefined;
|
|
253
|
-
setError(uploaded.error.message);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
attachmentIds.push(uploaded.value.attachmentId);
|
|
330
|
+
const upload = await uploadComposerFiles(putAttachment, workspaceId, pendingFiles);
|
|
331
|
+
if (!upload.ok) {
|
|
332
|
+
pendingSendId.current = undefined;
|
|
333
|
+
setError(upload.error);
|
|
334
|
+
return;
|
|
257
335
|
}
|
|
336
|
+
const attachmentIds = upload.attachmentIds;
|
|
258
337
|
const request = {
|
|
259
338
|
requestId, workspaceId,
|
|
260
339
|
channelRef, body: draft.trim(), recipients: recipientIds,
|
|
@@ -294,9 +373,16 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
294
373
|
return _jsxs("main", { className: css.surface, "data-team-channel": channelRef, children: [_jsxs("div", { className: css.surfaceHeader, children: [_jsx("div", { className: css.backRow, children: _jsx(Button, { size: "sm", icon: _jsx(IconChevronLeftOutline14, {}), onClick: backToChannels, children: t('backToChannels') }) }), _jsxs("header", { className: css.headerRow, children: [_jsxs("div", { className: css.headerCopy, children: [_jsx("h1", { children: channel === undefined ? '# …' : `# ${channel.name}` }), channel !== undefined && _jsx("p", { children: channel.description }), channel !== undefined && _jsxs("div", { className: channelCss.headerMeta, children: [_jsx("span", { children: t('memberCount', { count: channelMembers.length }) }), _jsx("span", { children: t('onlineCount', { count: onlineCount }) })] })] }), channel !== undefined && _jsx("span", { ref: manageTriggerRef, children: _jsx(Button, { size: "sm", variant: "outline", "aria-haspopup": "dialog", onClick: () => { setManagingMembers(true); }, children: t('manageMembers') }) })] })] }), _jsx(Modal, { open: managingMembers, onClose: closeMembers, title: t('channelMembers'), ...(channel === undefined ? {} : { description: `# ${channel.name} · ${t('memberCount', { count: channelMembers.length })}` }), closeLabel: t('close'), contentClassName: channelCss.modalBody, children: _jsx("div", { ref: memberListRef, className: channelCss.memberList, children: members.filter(status => status.member.state !== 'inactive' && status.member.state !== 'archived').map(status => {
|
|
295
374
|
const joined = channelMemberIds.has(status.member.memberId);
|
|
296
375
|
const rowPending = membership.pending.has(status.member.memberId);
|
|
297
|
-
|
|
376
|
+
// Joining needs an active Member — the Host refuses any other
|
|
377
|
+
// availability. Leaving only needs the Membership fact, so removal
|
|
378
|
+
// stays offered while a joined Member is down.
|
|
379
|
+
const disabled = rowPending || (!joined && status.availability !== 'active');
|
|
298
380
|
const rowError = membership.errors.get(status.member.memberId);
|
|
299
|
-
return
|
|
381
|
+
return _jsx(TeamMemberRow, { status: status, action: {
|
|
382
|
+
label: rowPending ? t('membershipUpdating') : joined ? t('removeFromChannel') : t('addToChannel'),
|
|
383
|
+
disabled,
|
|
384
|
+
onSelect: () => { void membership.change({ workspaceId, channelRef, memberId: status.member.memberId, joined }); },
|
|
385
|
+
}, ...(rowError === undefined ? {} : { error: rowError }), t: t }, status.member.memberId);
|
|
300
386
|
}) }) }), _jsx("section", { ref: timeline.ref, onScroll: timeline.onScroll, className: css.timeline, "aria-label": t('timelineLabel'), children: _jsxs("div", { className: css.timelineContent, children: [loading && channel === undefined && error === undefined && _jsx("div", { className: css.emptySurface, children: _jsxs("p", { className: css.loadingState, children: [_jsx("span", { className: css.loadingMark, "aria-hidden": "true" }), t('loadingChannels')] }) }), !loading && channel === undefined && error === undefined && _jsx("div", { className: css.emptySurface, children: _jsx("p", { className: css.emptyState, children: t('emptyChannels') }) }), !loading && channel === undefined && error !== undefined && _jsxs("div", { className: css.errorState, role: "alert", children: [_jsx("span", { children: error }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => { void refresh(true); }, children: t('retry') })] }), view?.hasMore && _jsx("div", { className: css.timelineAction, children: _jsx(Button, { size: "sm", disabled: loadingOlder, onClick: () => { void loadOlder(); }, children: t('loadOlder') }) }), channel !== undefined && view?.items.length === 0 && _jsx("div", { className: css.emptySurface, children: _jsxs("div", { className: css.emptyState, children: [_jsx("strong", { children: t('emptyMessages') }), _jsx("span", { children: t('emptyMessagesHint') })] }) }), (view?.items.length ?? 0) > 0 && chunkRunsWithDays(view.items, messageSender, item => item.message.occurredAt).map((block, blockIndex) => block.kind === 'day'
|
|
301
387
|
? _jsx("p", { className: threadCss.daySeparator, children: _jsx("span", { children: block.label }) }, `day-${blockIndex}-${block.label}`)
|
|
302
388
|
: _jsx("div", { className: css.messageRun, children: block.items.map((item, index) => {
|
|
@@ -304,7 +390,53 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
|
|
|
304
390
|
const human = item.message.sender === view.humanMemberId;
|
|
305
391
|
const sender = human ? t('human') : senderStatus?.member.handle ?? item.message.sender;
|
|
306
392
|
const turnGap = isRunGap(index > 0 ? block.items[index - 1].message.occurredAt : undefined, item.message.occurredAt);
|
|
393
|
+
const task = item.task;
|
|
394
|
+
// The entry's own line is the gate under the body, and its state
|
|
395
|
+
// leads that line: ownership, status, and unread answer the reader
|
|
396
|
+
// where they are already reading. A reply inside a Thread has none
|
|
397
|
+
// of it — the Thread page owns that surface.
|
|
398
|
+
const owners = task === undefined ? [] : ownersByTask.get(task.taskRef) ?? [];
|
|
399
|
+
const unread = unreadByThread.get(item.thread.threadRef) ?? 0;
|
|
307
400
|
// A turn divider already labels the gapped entry; its row stays chrome-free.
|
|
308
|
-
return _jsxs(Fragment, { children: [turnGap && _jsx(TeamRunDivider, { occurredAt: item.message.occurredAt }),
|
|
309
|
-
}) }, `run-${block.items[0].message.messageRef}`))] }) }), channel !== undefined ? _jsx(TeamComposer, { members: channelMembers,
|
|
401
|
+
return _jsxs(Fragment, { children: [turnGap && _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, handleByMember), onOpenRef: openRef, onResolveTaskRefs: lookupTaskRefs, grouped: index > 0, showGroupedTime: item.message.topLevel === true && !turnGap, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }), children: item.message.topLevel && _jsx(ThreadEntryRow, { item: item, owners: owners.map(memberId => ({ memberId, name: handleByMember.get(memberId) ?? memberId })), unread: unread, t: t, onOpen: () => { selectThread(item.thread.threadRef, channelRef, task?.taskRef, item.taskNumber); } }) })] }, item.message.messageRef);
|
|
402
|
+
}) }, `run-${block.items[0].message.messageRef}`))] }) }), channel !== undefined ? _jsx(TeamComposer, { members: channelMembers, drafts: drafts, draftKey: draftKey, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onEdit: clearSendState, onSubmit: () => { void send(); }, pendingFiles: pendingFiles, onFilesChange: setPendingFiles, asTask: asTask, onAsTaskChange: setAsTask, t: t }, draftKey) : _jsx("div", {})] });
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* One entry's state: who is on the work, where it stands, and how much of it
|
|
406
|
+
* needs the reader. It leads the entry's own line rather than trailing the
|
|
407
|
+
* identity line, so a reader meets it where they are already reading instead of
|
|
408
|
+
* crossing the column for it — and the same placement holds whether this
|
|
409
|
+
* Message opened its run or continued one, where an identity line would have
|
|
410
|
+
* carried nothing else. The unread capsule is the only member a taskless
|
|
411
|
+
* discussion can carry: it needs no Task.
|
|
412
|
+
*/
|
|
413
|
+
function ThreadStateCluster({ task, owners, unread, t }) {
|
|
414
|
+
if (task === undefined && unread === 0)
|
|
415
|
+
return null;
|
|
416
|
+
return _jsxs("span", { className: css.stateCluster, children: [task !== undefined && _jsx(TeamAvatarStack, { owners: owners, label: t('claimers', { names: owners.map(owner => `@${owner.name}`).join(', ') }) }), task !== undefined && _jsx(TeamStateDot, { size: 8, state: taskStatusDot(task.status) }), task !== undefined && _jsx("span", { className: css.statusWord, children: formatTaskStatus(task.status, t) }), _jsx(TeamCountBadge, { count: unread })] });
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* The gate under one top-level Message: the single row that opens its Thread,
|
|
420
|
+
* and the one line that says what stands there. State leads it, then what the
|
|
421
|
+
* entry is: a Task entry keeps its number so the status word never floats free
|
|
422
|
+
* of the Task it describes, follow-up activity adds when the work last moved,
|
|
423
|
+
* and an unanswered Thread says only 回复. The instant uses the Inbox's own
|
|
424
|
+
* 今天/昨天 form with the precise local time on the control's title, and an
|
|
425
|
+
* unread Thread says so in the control's label rather than only in pixels.
|
|
426
|
+
*/
|
|
427
|
+
function ThreadEntryRow({ item, owners, unread, t, onOpen }) {
|
|
428
|
+
const taskNumber = item.taskNumber;
|
|
429
|
+
const followUp = followUpAt(item);
|
|
430
|
+
const label = taskNumber !== undefined
|
|
431
|
+
? t('taskLabel', { number: taskNumber })
|
|
432
|
+
: followUp === undefined ? t('replyAction') : t('threadLabel');
|
|
433
|
+
const text = followUp === undefined ? label : `${label} · ${t('recentActivity', { time: formatInboxTime(followUp, t) })}`;
|
|
434
|
+
const openLabel = taskNumber === undefined
|
|
435
|
+
? unread > 0 ? t('openThreadUnread', { count: unread }) : t('openThread')
|
|
436
|
+
: unread > 0 ? t('openTaskUnread', { number: taskNumber, count: unread }) : t('openTask', { number: taskNumber });
|
|
437
|
+
// The state is a label, not a control: it stays outside the button so the
|
|
438
|
+
// owner stack keeps its own accessible name — a labeled button prunes its
|
|
439
|
+
// descendants from the accessibility tree — and so the control's name says
|
|
440
|
+
// exactly what clicking it does.
|
|
441
|
+
return _jsxs("span", { className: css.entryLine, "data-thread-entry": "", children: [_jsx(ThreadStateCluster, { task: item.task, owners: owners, unread: unread, t: t }), _jsxs("button", { type: "button", className: css.entryRow, "aria-label": openLabel, ...(followUp === undefined ? {} : { title: formatAbsoluteTime(followUp) }), onClick: onOpen, children: [_jsx("span", { children: text }), _jsx("span", { className: css.entryArrow, "aria-hidden": "true", children: _jsx(IconChevronRightOutline14, { size: 12 }) })] })] });
|
|
310
442
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TeamChannelsPanel.d.ts","sourceRoot":"","sources":["../../../src/client/TeamChannelsPanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EAEzB,mBAAmB,EAMpB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,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,EAMpB,MAAM,gCAAgC,CAAA;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAEnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAelD,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,cAAc,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,CAAA;IAC3D,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,+BAsM9D"}
|