@wowyuarm/dsh-agent-team 0.1.11 → 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.
Files changed (83) hide show
  1. package/README.md +4 -4
  2. package/README.zh.md +4 -4
  3. package/package.json +3 -2
  4. package/packages/agent-team/README.md +2 -2
  5. package/packages/agent-team/README.zh.md +2 -2
  6. package/packages/agent-team/lib/context-projection.js +5 -1
  7. package/packages/agent-team/lib/index.js +11 -44
  8. package/packages/agent-team/lib/ledger.js +251 -142
  9. package/packages/agent-team/lib/member-runtime.js +47 -9
  10. package/packages/agent-team/lib/mentions.js +56 -0
  11. package/packages/agent-team/lib/spec.js +1 -0
  12. package/packages/agent-team/lib/typert.host.js +80 -31
  13. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  14. package/packages/agent-team/lib/typert.remote-client.js +71 -26
  15. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  16. package/packages/agent-team/lib/types/index.d.ts +1 -7
  17. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  18. package/packages/agent-team/lib/types/ledger.d.ts +75 -17
  19. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  20. package/packages/agent-team/lib/types/member-runtime.d.ts +2 -1
  21. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  22. package/packages/agent-team/lib/types/mentions.d.ts +34 -0
  23. package/packages/agent-team/lib/types/mentions.d.ts.map +1 -0
  24. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/types/entities.d.ts +0 -18
  26. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/types/requests-results.d.ts +57 -15
  28. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  29. package/packages/agent-team/preset/team-member/agent.cordis.yml +48 -8
  30. package/packages/client-agent-team/lib/client.js +1366 -909
  31. package/packages/client-agent-team/lib/client.js.map +1 -1
  32. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  33. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +3 -3
  34. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +18 -0
  35. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -0
  36. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +22 -0
  37. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -1
  38. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +125 -13
  40. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +10 -3
  42. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  43. package/packages/client-agent-team/lib/types/client/TeamComposer.js +11 -17
  44. package/packages/client-agent-team/lib/types/client/TeamConversation.js +1 -1
  45. package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts +20 -0
  46. package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts.map +1 -0
  47. package/packages/client-agent-team/lib/types/client/TeamCountBadge.js +25 -0
  48. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +9 -5
  49. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -1
  50. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +82 -14
  51. package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts +43 -0
  52. package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts.map +1 -0
  53. package/packages/client-agent-team/lib/types/client/TeamMemberRow.js +27 -0
  54. package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts +2 -2
  55. package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts.map +1 -1
  56. package/packages/client-agent-team/lib/types/client/TeamMembersAction.js +2 -2
  57. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +6 -2
  59. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +28 -10
  61. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  62. package/packages/client-agent-team/lib/types/client/index.js +3 -4
  63. package/packages/client-agent-team/lib/types/client/locales.d.ts +36 -16
  64. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/locales.js +36 -16
  66. package/packages/client-agent-team/lib/types/client/slots.d.ts +0 -1
  67. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  68. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +7 -1
  69. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/team-changes.js +62 -31
  71. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +38 -5
  72. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  73. package/packages/client-agent-team/lib/types/client/team-formatters.js +54 -7
  74. package/packages/tool-agent-team/README.md +2 -2
  75. package/packages/tool-agent-team/README.zh.md +2 -2
  76. package/packages/tool-agent-team/lib/index.js +29 -16
  77. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  78. package/packages/agent-team/lib/progress-nudge.js +0 -328
  79. package/packages/agent-team/lib/types/progress-nudge.d.ts +0 -136
  80. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +0 -1
  81. package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts +0 -3
  82. package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts.map +0 -1
  83. 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;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,+BAwO/N"}
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,7 +1,7 @@
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 { TeamMemberAvatar } from "./TeamMemberAvatar.js";
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";
@@ -195,7 +195,7 @@ export function TeamAgentsPanel({ workspaceId, loadMembers, subscribeChanges, ad
195
195
  ...(model === undefined ? {} : { model }),
196
196
  });
197
197
  };
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 && 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') })] }))] }));
199
199
  }
200
200
  /**
201
201
  * One sidebar Agent row: the select button opens the Member's own Session
@@ -255,7 +255,7 @@ function AgentRow({ status, current, updateMember, recoverMember, archiveMember,
255
255
  setRowAlert(t('archiveAgentFailed', { message: cause instanceof Error ? cause.message : String(cause) }));
256
256
  }
257
257
  };
258
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.agentRow, "data-menu-open": menuOpen || undefined, children: [_jsxs("button", { type: "button", className: css.agentSelect, "aria-label": t('openAgentSession', { name: status.member.handle }), "aria-current": current ? 'page' : undefined, disabled: status.availability !== 'active', onClick: () => { openMemberSession(status.member.sessionId); }, children: [_jsx(TeamMemberAvatar, { status: status, t: t }), _jsxs("span", { className: css.agentCopy, children: [_jsx("strong", { children: status.member.handle }), _jsx("small", { children: status.member.description })] })] }), _jsx("span", { className: css.rowMenu, children: _jsx(TeamRowMenu, { label: t('actionsAgent', { name: status.member.handle }), items: [
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: [
259
259
  { id: 'edit', label: t('editAgent'), icon: _jsx(IconEditOutline16, {}) },
260
260
  ...(status.presence === 'error' ? [{ id: 'resume', label: t('resumeAgent'), icon: _jsx(IconPlayOutline16, {}) }] : []),
261
261
  ...(status.availability === 'unavailable' && restartOffered(status) ? [{ id: 'restart', label: t('restartAgent'), icon: _jsx(IconRefreshOutline16, {}) }] : []),
@@ -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;AAEvD,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,aAAa,CAAA;AAe/D,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;IACjC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAA;IACxC,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAA;IAC1D,QAAQ,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,qBAAqB,CAAC,CAAA;IAC1E,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,cAAc,EAAE,qBAAqB,CAAC,gBAAgB,CAAC,CAAA;IAChE,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AAmBD,wBAAgB,eAAe,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC,EAAE,EAAE,oBAAoB,+BA4VnR"}
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"}
@@ -3,10 +3,12 @@ import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { Button, IconChevronLeftOutline14, IconChevronRightOutline14, Modal } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { mintRequestId, uploadComposerFiles } from "./requests.js";
5
5
  import { TeamComposer } from "./TeamComposer.js";
6
- import { TeamPresenceDot } from "./TeamPresenceDot.js";
6
+ import { TeamMemberRow } from "./TeamMemberRow.js";
7
7
  import { TeamMessage } from "./TeamMessage.js";
8
+ import { TeamAvatarStack } from "./TeamAvatarStack.js";
9
+ import { TeamCountBadge } from "./TeamCountBadge.js";
8
10
  import { TeamRunDivider } from "./TeamRunDivider.js";
9
- import { formatTaskStatus, taskStatusDot, mentionNamesOf } from "./team-formatters.js";
11
+ import { formatAbsoluteTime, formatInboxTime, formatTaskStatus, taskStatusDot, mentionNamesOf } from "./team-formatters.js";
10
12
  import { TeamStateDot } from "./TeamStateDot.js";
11
13
  import { useChannelMembership } from "./team-membership.js";
12
14
  import { useTimelineScroll } from "./timeline-scroll.js";
@@ -16,12 +18,14 @@ import channelCss from './channel.module.css';
16
18
  import css from './conversation.module.css';
17
19
  import threadCss from './thread.module.css';
18
20
  /**
19
- * Merge the freshest top-level window over what the reader already has:
20
- * a change-driven refresh must not discard older messages loaded earlier.
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.
21
25
  */
22
26
  function mergeChannelView(current, fresh) {
23
- const known = new Set(current.items.map(item => item.message.messageRef));
24
- const items = [...fresh.items.filter(item => !known.has(item.message.messageRef)), ...current.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]
25
29
  .sort((left, right) => left.message.sequence - right.message.sequence);
26
30
  return {
27
31
  ...fresh,
@@ -31,9 +35,47 @@ function mergeChannelView(current, fresh) {
31
35
  hasMore: fresh.hasMore || current.cursor < fresh.cursor,
32
36
  };
33
37
  }
34
- export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscribeChanges, loadMembers, drafts, putAttachment, getAttachment, sendMessage, joinChannel, removeChannelMember, selectThread, selectChannel, backToChannels, resolveTaskRefs, t }) {
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 }) {
35
76
  const [view, setView] = useState();
36
77
  const [members, setMembers] = useState([]);
78
+ const [unreadByThread, setUnreadByThread] = useState(new Map());
37
79
  const [error, setError] = useState();
38
80
  const [pendingFiles, setPendingFiles] = useState([]);
39
81
  const [statusMessage, setStatusMessage] = useState();
@@ -103,7 +145,11 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
103
145
  // Presence counts ride the header meta line; error and unavailable do not count as online.
104
146
  const onlineCount = channelMembers.filter(status => status.presence === 'available' || status.presence === 'working').length;
105
147
  const messageSender = (item) => item.message.sender;
106
- const mentionHandlesMap = new Map(members.map(status => [status.member.memberId, status.member.handle.replace(/^@/, '')]));
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 ?? []);
107
153
  const refresh = async (clearError = false) => {
108
154
  if (!mountedRef.current)
109
155
  return false;
@@ -118,9 +164,14 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
118
164
  setStatusMessage(undefined);
119
165
  }
120
166
  try {
121
- const [loaded, loadedMembers] = await Promise.all([
167
+ const [loaded, loadedMembers, loadedInbox] = await Promise.all([
122
168
  loadChannels({ workspaceId, channelRef, direction: 'before', topLevelOnly: true, includeActivities: false, limit: 20 }),
123
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 }),
124
175
  ]);
125
176
  if (!mountedRef.current || sequence !== refreshSequenceRef.current)
126
177
  return false;
@@ -134,7 +185,15 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
134
185
  setMembers(loadedMembers.value);
135
186
  else
136
187
  setError(loadedMembers.error.message);
137
- return loaded.ok && loadedMembers.ok;
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;
138
197
  }
139
198
  catch (cause) {
140
199
  if (mountedRef.current && sequence === refreshSequenceRef.current)
@@ -314,9 +373,16 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
314
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 => {
315
374
  const joined = channelMemberIds.has(status.member.memberId);
316
375
  const rowPending = membership.pending.has(status.member.memberId);
317
- const disabled = rowPending || (!joined && status.presence === 'unavailable');
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');
318
380
  const rowError = membership.errors.get(status.member.memberId);
319
- return _jsxs("div", { className: channelCss.memberRow, children: [_jsx(TeamPresenceDot, { status: status, t: t }), _jsxs("span", { className: channelCss.memberCopy, children: [_jsxs("strong", { children: ["@", status.member.handle] }), _jsx("small", { children: status.member.description })] }), _jsx(Button, { className: channelCss.memberAction, size: "sm", disabled: disabled, onClick: () => { void membership.change({ workspaceId, channelRef, memberId: status.member.memberId, joined }); }, children: rowPending ? t('membershipUpdating') : joined ? t('removeFromChannel') : t('addToChannel') }), rowError !== undefined && _jsx("p", { className: channelCss.memberError, role: "alert", children: rowError })] }, status.member.memberId);
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);
320
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'
321
387
  ? _jsx("p", { className: threadCss.daySeparator, children: _jsx("span", { children: block.label }) }, `day-${blockIndex}-${block.label}`)
322
388
  : _jsx("div", { className: css.messageRun, children: block.items.map((item, index) => {
@@ -324,7 +390,53 @@ export function TeamChannelPage({ workspaceId, channelRef, loadChannels, subscri
324
390
  const human = item.message.sender === view.humanMemberId;
325
391
  const sender = human ? t('human') : senderStatus?.member.handle ?? item.message.sender;
326
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;
327
400
  // A turn divider already labels the gapped entry; its row stays chrome-free.
328
- return _jsxs(Fragment, { children: [turnGap && _jsx(TeamRunDivider, { occurredAt: item.message.occurredAt }), _jsxs(TeamMessage, { senderName: sender, memberId: item.message.sender, human: human, body: item.message.body, attachments: item.message.attachments, loadAttachment: getAttachment, t: t, occurredAt: item.message.occurredAt, mentionNames: mentionNamesOf(item.mentions, mentionHandlesMap), onOpenRef: openRef, onResolveTaskRefs: lookupTaskRefs, grouped: index > 0, showGroupedTime: item.message.topLevel === true && !turnGap, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }), children: [item.message.topLevel && item.task !== undefined && item.taskNumber !== undefined && _jsxs("button", { type: "button", className: channelCss.taskFooter, "aria-label": t('openTask', { number: item.taskNumber }), onClick: () => { selectThread(item.thread.threadRef, channelRef, item.task?.taskRef, item.taskNumber); }, children: [_jsx("span", { className: channelCss.taskDot, children: _jsx(TeamStateDot, { size: 8, state: taskStatusDot(item.task.status) }) }), _jsx("span", { className: channelCss.taskNumber, children: t('taskLabel', { number: item.taskNumber }) }), _jsx("span", { className: channelCss.taskStatus, children: formatTaskStatus(item.task.status, t) }), _jsx("span", { className: channelCss.taskCount, children: t('taskMessageCount', { count: item.messageCount }) }), _jsx("span", { className: channelCss.taskArrow, "aria-hidden": "true", children: _jsx(IconChevronRightOutline14, { size: 12 }) })] }), item.message.topLevel && item.task === undefined && _jsxs("button", { type: "button", className: channelCss.taskFooter, "aria-label": t('openThread'), onClick: () => { selectThread(item.thread.threadRef, channelRef); }, children: [_jsx("span", { className: channelCss.taskNumber, children: t('threadLabel') }), _jsx("span", { className: channelCss.taskCount, children: t('taskMessageCount', { count: item.messageCount }) }), _jsx("span", { className: channelCss.taskArrow, "aria-hidden": "true", children: _jsx(IconChevronRightOutline14, { size: 12 }) })] })] })] }, item.message.messageRef);
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);
329
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", {})] });
330
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 }) })] })] });
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;AAclD,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,+BAmM9D"}
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"}
@@ -2,6 +2,7 @@ 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, IconArchiveOutline20, IconEditOutline16, IconPlusOutline16, Input, Modal, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { TeamPresenceDot } from "./TeamPresenceDot.js";
5
+ import { TeamMemberRow } from "./TeamMemberRow.js";
5
6
  import { MultiMenuField } from "./multi-menu-field.js";
6
7
  import { SortableRow, useSidebarRowDrag } from "./sidebar-drag.js";
7
8
  import { moveSidebarItem, useSidebarOrder } from "./sidebar-order.js";
@@ -152,7 +153,7 @@ export function TeamChannelsPanel(props) {
152
153
  return next;
153
154
  });
154
155
  setPendingCreate(undefined);
155
- }, triggerEmptyLabel: t('membersPickerEmpty'), formatCount: count => t('membersPickerCount', { count }) }), error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('channels'), open: sectionOpen, onToggle: open => { setSidebarSectionOpen(workspaceId, 'channels', open); }, 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 => {
156
+ }, triggerEmptyLabel: t('membersPickerEmpty'), formatCount: count => t('membersPickerCount', { count }) }), error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }), _jsxs(TeamSidebarSection, { title: t('channels'), open: sectionOpen, onToggle: open => { setSidebarSectionOpen(workspaceId, 'channels', open); }, 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 && error === undefined && view !== undefined && view.channels.length === 0 && _jsx("p", { className: css.emptyState, children: t('emptyChannels') }), _jsx("div", { className: css.channelList, children: orderedChannels.map(channel => {
156
157
  const joined = membership.get(channel.channelRef) ?? new Set();
157
158
  return (_jsx(SortableRow, { drag: drag, orderKey: channel.channelRef, children: _jsx(ChannelRow, { channel: channel, members: members, joinedIds: joined, selected: selectedChannelRef === channel.channelRef, updateChannel: updateChannel, archiveChannel: props.archiveChannel, joinChannel: props.joinChannel, removeChannelMember: props.removeChannelMember, onCommitted: () => { void refresh(); }, selectChannel: selectChannel, t: t }) }, channel.channelRef));
158
159
  }) })] }), !formOpen && error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error })] }));
@@ -226,8 +227,14 @@ function ChannelEditorDialog({ channel, members, joinedIds, updateChannel, joinC
226
227
  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 => {
227
228
  const joined = joinedIds.has(status.member.memberId);
228
229
  const rowPending = membership.pending.has(status.member.memberId);
229
- const disabled = rowPending || (!joined && status.presence === 'unavailable');
230
+ // Same membership law as the Channel page roster: joining needs an
231
+ // active Member, leaving only needs the fact.
232
+ const disabled = rowPending || (!joined && status.availability !== 'active');
230
233
  const rowError = membership.errors.get(status.member.memberId);
231
- return _jsxs("div", { className: css.editMemberRow, children: [_jsx(TeamPresenceDot, { status: status, t: t }), _jsxs("span", { className: css.editMemberCopy, children: [_jsxs("strong", { children: ["@", status.member.handle] }), _jsx("small", { children: status.member.description })] }), _jsx(Button, { size: "sm", variant: "outline", disabled: disabled, onClick: () => { void membership.change({ workspaceId: channel.workspaceId, channelRef: channel.channelRef, memberId: status.member.memberId, joined }); }, children: rowPending ? t('membershipUpdating') : joined ? t('removeFromChannel') : t('addToChannel') }), rowError !== undefined && _jsx("p", { className: css.rowError, role: "alert", children: rowError })] }, status.member.memberId);
234
+ return _jsx(TeamMemberRow, { status: status, action: {
235
+ label: rowPending ? t('membershipUpdating') : joined ? t('removeFromChannel') : t('addToChannel'),
236
+ disabled,
237
+ onSelect: () => { void membership.change({ workspaceId: channel.workspaceId, channelRef: channel.channelRef, memberId: status.member.memberId, joined }); },
238
+ }, ...(rowError === undefined ? {} : { error: rowError }), t: t }, status.member.memberId);
232
239
  })] }), error !== undefined && _jsx("p", { className: createCss.error, role: "alert", children: error })] }) }));
233
240
  }
@@ -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;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAqE/D,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE;IAClM,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAA;IACxD,oGAAoG;IACpG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;IAC3D;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,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,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;IACnD,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC,+BAyTA"}
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;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAqD/D,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,EAAE,EAAE;IAClM,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAA;IACxD,oGAAoG;IACpG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;IAC3D;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,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,gFAAgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;IACnD,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC,+BAiUA"}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import { IconChecklistOutline14, IconPaperclipOutline16, IconSendOutline16, Tooltip, useAnchoredMaxHeight, useDismissOnOutsidePointer } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { TeamPresenceDot } from "./TeamPresenceDot.js";
5
+ import { allMentionMembers, containsAllMention, containsMention, mentionedMemberIds } from "./team-formatters.js";
5
6
  import css from './composer.module.css';
6
7
  import { formatByteSize } from "./attachment-preview.js";
7
8
  function findMention(draft, caret) {
@@ -18,12 +19,6 @@ function findMention(draft, caret) {
18
19
  return undefined;
19
20
  return { start: at, end: caret, query: beforeCaret.slice(at + 1) };
20
21
  }
21
- function escapeRegExp(value) {
22
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23
- }
24
- function containsMention(draft, handle) {
25
- return new RegExp(`(?:^|[^\\p{L}\\p{N}_])@${escapeRegExp(handle)}(?=$|[^\\p{L}\\p{N}_])`, 'u').test(draft);
26
- }
27
22
  function mentionCandidates(members, query) {
28
23
  const normalized = query.toLocaleLowerCase();
29
24
  return members.filter(status => status.presence !== 'unavailable'
@@ -37,13 +32,6 @@ function rankMentionCandidates(candidates, followers) {
37
32
  return candidates;
38
33
  return [...candidates].sort((left, right) => Number(followers.has(right.member.memberId)) - Number(followers.has(left.member.memberId)));
39
34
  }
40
- /** Every member a manual handle pick could reach: the @all expansion snapshot. */
41
- function allMentionMembers(members) {
42
- return members.filter(status => status.presence !== 'unavailable' && status.member.state !== 'inactive' && status.member.state !== 'archived');
43
- }
44
- function containsAllMention(draft) {
45
- return /(?:^|[^\p{L}\p{N}_])@all(?=$|[^\p{L}\p{N}_])/u.test(draft);
46
- }
47
35
  /** One object URL per draft file; revoked when the draft is removed. */
48
36
  const draftPreviewUrls = new WeakMap();
49
37
  function draftPreviewUrl(file) {
@@ -121,15 +109,17 @@ export function TeamComposer({ members, followerMemberIds, drafts, draftKey, pen
121
109
  return;
122
110
  inputRef.current?.focus({ preventScroll: true });
123
111
  }, [confirmation, pending]);
112
+ // Handle lookup for both directions of the same judgement: whether a picked
113
+ // recipient is still spelled in the text, and which Members the text names.
114
+ const memberHandles = new Map(members.map(status => [status.member.memberId, status.member.handle]));
124
115
  const pruneRecipients = (nextDraft) => {
125
116
  // An @all marker stands for its expansion snapshot: the member handles it
126
117
  // stands for are not in the text, so text-based pruning must stand down.
127
118
  if (containsAllMention(nextDraft))
128
119
  return;
129
- const knownMembers = new Map(members.map(status => [status.member.memberId, status.member]));
130
120
  const next = new Set([...recipients].filter(memberId => {
131
- const member = knownMembers.get(memberId);
132
- return member !== undefined && containsMention(nextDraft, member.handle);
121
+ const handle = memberHandles.get(memberId);
122
+ return handle !== undefined && containsMention(nextDraft, handle);
133
123
  }));
134
124
  if (next.size !== recipients.size)
135
125
  drafts.writeRecipients(draftKey, next);
@@ -143,6 +133,10 @@ export function TeamComposer({ members, followerMemberIds, drafts, draftKey, pen
143
133
  return;
144
134
  pruneRecipients(draft);
145
135
  }, [draft, recipients, members]);
136
+ // The notify row reports what the Host will resolve from this draft, not just
137
+ // what the mention menu picked: an authored `@Handle` delivers exactly like a
138
+ // pick, and `@all` stands for the menu's expansion.
139
+ const notifiedIds = [...new Set([...recipients, ...mentionedMemberIds(draft, members)])].sort();
146
140
  const updateMention = (nextDraft, caret) => {
147
141
  const match = findMention(nextDraft, caret);
148
142
  setMention(match);
@@ -252,7 +246,7 @@ export function TeamComposer({ members, followerMemberIds, drafts, draftKey, pen
252
246
  return option.kind === 'all'
253
247
  ? _jsxs("button", { id: optionId, type: "button", role: "option", "aria-selected": selected, className: css.mentionOption, ref: selected ? activeOptionRef : undefined, onMouseDown: event => { event.preventDefault(); }, onClick: () => { selectOption(option); }, children: [_jsx("span", { className: css.mentionAllDot, "aria-hidden": "true" }), _jsx("span", { className: css.mentionName, children: "@all" }), _jsx("span", { className: css.mentionDescription, children: t('mentionAll', { count: allCount }) })] }, "all")
254
248
  : _jsxs("button", { id: optionId, type: "button", role: "option", "aria-selected": selected, className: css.mentionOption, ref: selected ? activeOptionRef : undefined, onMouseDown: event => { event.preventDefault(); }, onClick: () => { selectOption(option); }, children: [_jsx(TeamPresenceDot, { status: option.status, t: t }), _jsxs("span", { className: css.mentionName, children: ["@", option.status.member.handle] }), _jsx("span", { className: css.mentionDescription, children: option.status.member.description })] }, option.status.member.memberId);
255
- }) }), _jsx("textarea", { ref: inputRef, "aria-label": t('messageDraft'), "aria-autocomplete": "list", "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen && activeOption !== undefined ? activeOption.kind === 'all' ? `${listId}-all` : `${listId}-${activeOption.status.member.memberId}` : undefined, "aria-expanded": menuOpen, value: draft, readOnly: pending, placeholder: placeholder ?? t('messagePlaceholder'), rows: 1, onChange: onChange, onPaste: onPaste, 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) => {
249
+ }) }), _jsx("textarea", { ref: inputRef, "aria-label": t('messageDraft'), "aria-autocomplete": "list", "aria-controls": menuOpen ? listId : undefined, "aria-activedescendant": menuOpen && activeOption !== undefined ? activeOption.kind === 'all' ? `${listId}-all` : `${listId}-${activeOption.status.member.memberId}` : undefined, "aria-expanded": menuOpen, value: draft, readOnly: pending, placeholder: placeholder ?? t('messagePlaceholder'), rows: 1, onChange: onChange, onPaste: onPaste, 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); } })] }), notifiedIds.length > 0 && _jsx("p", { className: css.notifyRow, "data-team-notify": true, children: t('composerNotify', { ids: notifiedIds.map(memberId => `@${memberHandles.get(memberId) ?? memberId}`).join(', ') }) }), onFilesChange !== undefined && pendingFiles !== undefined && pendingFiles.length > 0 && (_jsx("ul", { className: css.fileChips, "aria-label": t('attachFiles'), children: pendingFiles.map((file, index) => {
256
250
  const previewUrl = draftPreviewUrl(file);
257
251
  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: () => {
258
252
  const url = draftPreviewUrls.get(file);
@@ -18,7 +18,7 @@ export function TeamConversation({ t, useWorkspaces, navigation, drafts, putAtta
18
18
  return _jsx(TeamThreadPage, { workspaceId: current.workspaceId, putAttachment: putAttachment, threadRef: navigationState.threadRef, backToWorkspace: backToWorkspace, selectChannel: selectChannel, selectThread: selectThread, resolveTaskRefs: resolveTaskRefs, ...(navigationState.channelRef === undefined ? {} : { channelRef: navigationState.channelRef }), ...(navigationState.taskRef === undefined ? {} : { taskRef: navigationState.taskRef }), ...(navigationState.taskNumber === undefined ? {} : { taskNumber: navigationState.taskNumber }), drafts: drafts, getAttachment: getAttachment, readThread: readThread, loadChannels: loadChannels, loadThreadHistory: loadThreadHistory, threadObservations: threadObservations, subscribeChanges: subscribeChanges, loadMembers: loadMembers, reply: reply, changeTask: changeTask, promoteThread: promoteThread, t: t }, navigationState.threadRef);
19
19
  }
20
20
  if (current !== undefined && navigationState.channelRef !== undefined) {
21
- 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);
21
+ return _jsx(TeamChannelPage, { workspaceId: current.workspaceId, channelRef: navigationState.channelRef, drafts: drafts, putAttachment: putAttachment, getAttachment: getAttachment, loadChannels: loadChannels, subscribeChanges: subscribeChanges, loadMembers: loadMembers, loadInbox: loadInbox, sendMessage: sendMessage, joinChannel: joinChannel, removeChannelMember: removeChannelMember, selectThread: selectThread, selectChannel: selectChannel, backToChannels: backToChannels, resolveTaskRefs: resolveTaskRefs, t: t }, navigationState.channelRef);
22
22
  }
23
23
  const welcome = current === undefined
24
24
  ? { eyebrow: t('teamMode'), title: t('team'), body: t('empty') }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The one count capsule. The Human Inbox entry, the Channel feed's Thread
3
+ * entry, and the Inbox queue row all answer the same question — how much is
4
+ * waiting here — so they wear the same box rather than one hand-written copy
5
+ * of the same declarations per surface. Three copies is how the feed's digit
6
+ * ended up on a different line box from the other two.
7
+ *
8
+ * Zero is the absence of a count rather than a capsule reading zero, which is
9
+ * the rule every caller wants and therefore the component's own.
10
+ */
11
+ export declare function TeamCountBadge({ count, tone, label, className }: {
12
+ readonly count: number;
13
+ /** `hairline` is unread that merely arrived; the solid fill is unread that names this reader. */
14
+ readonly tone?: 'solid' | 'hairline' | undefined;
15
+ /** The accessible name while the count is the only thing saying it; omit it when the surrounding control already carries the number. */
16
+ readonly label?: string | undefined;
17
+ /** Placement belongs to the surface hanging the capsule: the narrow rail pins it to the icon's corner. */
18
+ readonly className?: string | undefined;
19
+ }): import("react").JSX.Element | null;
20
+ //# sourceMappingURL=TeamCountBadge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TeamCountBadge.d.ts","sourceRoot":"","sources":["../../../src/client/TeamCountBadge.tsx"],"names":[],"mappings":"AAKA;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,IAAc,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iGAAiG;IACjG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;IAChD,wIAAwI;IACxI,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,0GAA0G;IAC1G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC,sCASA"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import css from './countBadge.module.css';
3
+ /** Every count the Human reads is capped at this, so one wide number never widens the capsule. */
4
+ const COUNT_CAP = 99;
5
+ /**
6
+ * The one count capsule. The Human Inbox entry, the Channel feed's Thread
7
+ * entry, and the Inbox queue row all answer the same question — how much is
8
+ * waiting here — so they wear the same box rather than one hand-written copy
9
+ * of the same declarations per surface. Three copies is how the feed's digit
10
+ * ended up on a different line box from the other two.
11
+ *
12
+ * Zero is the absence of a count rather than a capsule reading zero, which is
13
+ * the rule every caller wants and therefore the component's own.
14
+ */
15
+ export function TeamCountBadge({ count, tone = 'solid', label, className }) {
16
+ if (count <= 0)
17
+ return null;
18
+ const text = count > COUNT_CAP ? `${COUNT_CAP}+` : String(count);
19
+ const classes = `${css.badge}${tone === 'hairline' ? ` ${css.hairline}` : ''}${className === undefined ? '' : ` ${className}`}`;
20
+ // Tests and the browser journey read the capsule by this attribute rather
21
+ // than by a CSS-module hash, which changes whenever a rule is renamed.
22
+ return label === undefined
23
+ ? _jsx("span", { className: classes, "data-team-count-badge": tone, "aria-hidden": "true", children: text })
24
+ : _jsx("span", { className: classes, "data-team-count-badge": tone, role: "img", "aria-label": label, title: label, children: text });
25
+ }
@@ -9,12 +9,16 @@ interface TeamInboxPageProps {
9
9
  readonly t: TeamConversationProps['t'];
10
10
  }
11
11
  /**
12
- * The Human 「提到我」 queue: one direct-only Inbox call per visible Workspace,
13
- * merged into a single recency-ordered list newest mention first across every
14
- * Workspace, since a queue is read by recency rather than by Workspace.
12
+ * The Human Inbox: one Inbox call per visible Workspace, rendering the Host's
13
+ * two slices the unread queue (「需要我」, mentions counted inside it rather
14
+ * than alone) and the 「最近活跃」 tail of Threads the reader took part in.
15
+ * Both merge across every Workspace into one recency-ordered list, newest
16
+ * first, since a queue is read by recency rather than by Workspace.
15
17
  * Opening the page never acknowledges anything — only a durable Thread read
16
- * consumes a mention marker, so rows and the badge drop after the Thread is
17
- * opened through the existing auto-ack path.
18
+ * advances the watermark and consumes a mention marker, so rows and the badge
19
+ * drop after the Thread is opened through the existing auto-ack path. A Thread
20
+ * holding unread is only ever in the queue: the Host already excludes it from
21
+ * the tail, and this page never re-derives that judgement.
18
22
  */
19
23
  export declare function TeamInboxPage({ useWorkspaces, loadInbox, subscribeChanges, selectWorkspace, selectThread, t }: TeamInboxPageProps): import("react").JSX.Element;
20
24
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"TeamInboxPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamInboxPage.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAWvD,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAA;IACtD,mGAAmG;IACnG,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AAgBD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,kBAAkB,+BAiFjI"}
1
+ {"version":3,"file":"TeamInboxPage.d.ts","sourceRoot":"","sources":["../../../src/client/TeamInboxPage.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAavD,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC9D,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC,WAAW,CAAC,CAAA;IACtD,mGAAmG;IACnG,QAAQ,CAAC,gBAAgB,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IACpE,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC,iBAAiB,CAAC,CAAA;IAClE,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC,cAAc,CAAC,CAAA;IAC5D,QAAQ,CAAC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAA;CACvC;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,EAAE,aAAa,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,kBAAkB,+BA0GjI"}