@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.
Files changed (135) hide show
  1. package/README.md +17 -4
  2. package/README.zh.md +17 -4
  3. package/package.json +53 -47
  4. package/packages/agent-team/README.md +6 -4
  5. package/packages/agent-team/README.zh.md +6 -4
  6. package/packages/agent-team/lib/context-management.js +40 -50
  7. package/packages/agent-team/lib/context-projection.js +15 -1
  8. package/packages/agent-team/lib/index.js +244 -154
  9. package/packages/agent-team/lib/invariant.js +38 -4
  10. package/packages/agent-team/lib/ledger.js +775 -383
  11. package/packages/agent-team/lib/member-context.js +6 -2
  12. package/packages/agent-team/lib/member-runtime.js +47 -9
  13. package/packages/agent-team/lib/member-time-context.js +20 -5
  14. package/packages/agent-team/lib/mentions.js +56 -0
  15. package/packages/agent-team/lib/pressure-policy.js +33 -9
  16. package/packages/agent-team/lib/session-event-cursor.js +116 -0
  17. package/packages/agent-team/lib/session-remediation.js +38 -26
  18. package/packages/agent-team/lib/spec.js +47 -34
  19. package/packages/agent-team/lib/stored-session-reader.js +138 -0
  20. package/packages/agent-team/lib/typert.host.js +157 -41
  21. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  22. package/packages/agent-team/lib/typert.remote-client.js +129 -32
  23. package/packages/agent-team/lib/types/context-management.d.ts +7 -0
  24. package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
  25. package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
  26. package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
  27. package/packages/agent-team/lib/types/index.d.ts +77 -7
  28. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  29. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/ledger.d.ts +251 -20
  31. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  32. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  33. package/packages/agent-team/lib/types/member-runtime.d.ts +2 -1
  34. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  35. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
  36. package/packages/agent-team/lib/types/mentions.d.ts +34 -0
  37. package/packages/agent-team/lib/types/mentions.d.ts.map +1 -0
  38. package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
  39. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
  41. package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
  42. package/packages/agent-team/lib/types/session-remediation.d.ts +23 -0
  43. package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -1
  44. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  45. package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
  46. package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
  47. package/packages/agent-team/lib/types/types/entities.d.ts +28 -20
  48. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  49. package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
  50. package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
  51. package/packages/agent-team/lib/types/types/requests-results.d.ts +87 -4
  52. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  53. package/packages/agent-team/preset/team-member/agent.cordis.yml +49 -7
  54. package/packages/client-agent-team/README.md +1 -1
  55. package/packages/client-agent-team/README.zh.md +1 -1
  56. package/packages/client-agent-team/lib/client.js +2357 -1207
  57. package/packages/client-agent-team/lib/client.js.map +1 -1
  58. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  59. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +27 -7
  60. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts +18 -0
  61. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.d.ts.map +1 -0
  62. package/packages/client-agent-team/lib/types/client/TeamAvatarStack.js +22 -0
  63. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -1
  64. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  65. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +167 -35
  66. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  67. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +23 -27
  68. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
  69. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  70. package/packages/client-agent-team/lib/types/client/TeamComposer.js +26 -26
  71. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  72. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  73. package/packages/client-agent-team/lib/types/client/TeamConversation.js +9 -2
  74. package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts +20 -0
  75. package/packages/client-agent-team/lib/types/client/TeamCountBadge.d.ts.map +1 -0
  76. package/packages/client-agent-team/lib/types/client/TeamCountBadge.js +25 -0
  77. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +25 -0
  78. package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
  79. package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +139 -0
  80. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
  81. package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
  82. package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts +43 -0
  83. package/packages/client-agent-team/lib/types/client/TeamMemberRow.d.ts.map +1 -0
  84. package/packages/client-agent-team/lib/types/client/TeamMemberRow.js +27 -0
  85. package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts +2 -2
  86. package/packages/client-agent-team/lib/types/client/TeamMembersAction.d.ts.map +1 -1
  87. package/packages/client-agent-team/lib/types/client/TeamMembersAction.js +2 -2
  88. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
  89. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  90. package/packages/client-agent-team/lib/types/client/TeamMessage.js +11 -4
  91. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
  92. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  93. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
  94. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  95. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +108 -30
  96. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  97. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  98. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +68 -7
  99. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  100. package/packages/client-agent-team/lib/types/client/index.js +19 -9
  101. package/packages/client-agent-team/lib/types/client/locales.d.ts +40 -2
  102. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  103. package/packages/client-agent-team/lib/types/client/locales.js +40 -2
  104. package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
  105. package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
  106. package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
  107. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
  108. package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
  109. package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
  110. package/packages/client-agent-team/lib/types/client/slots.d.ts +7 -2
  111. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  112. package/packages/client-agent-team/lib/types/client/team-changes.d.ts +33 -2
  113. package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
  114. package/packages/client-agent-team/lib/types/client/team-changes.js +107 -25
  115. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
  116. package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
  117. package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
  118. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +43 -1
  119. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  120. package/packages/client-agent-team/lib/types/client/team-formatters.js +81 -6
  121. package/packages/tool-agent-team/README.md +2 -2
  122. package/packages/tool-agent-team/README.zh.md +2 -2
  123. package/packages/tool-agent-team/lib/context-tools.js +18 -14
  124. package/packages/tool-agent-team/lib/host-access.js +20 -0
  125. package/packages/tool-agent-team/lib/index.js +44 -32
  126. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  127. package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
  128. package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
  129. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  130. package/packages/agent-team/lib/progress-nudge.js +0 -328
  131. package/packages/agent-team/lib/types/progress-nudge.d.ts +0 -136
  132. package/packages/agent-team/lib/types/progress-nudge.d.ts.map +0 -1
  133. package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts +0 -3
  134. package/packages/client-agent-team/lib/types/client/TeamSettings.d.ts.map +0 -1
  135. 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;AAYlD,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,+BA2N/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,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 { 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";
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
- setLoading(true);
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.diagnostic ?? t('statusUnavailable'));
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
- setRowAlert(t('restartStillUnavailable', { diagnostic: result.value.status.diagnostic ?? t('statusUnavailable') }));
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: [_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: [
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;AAIvD,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,+BA8UnR"}
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, useSyncExternalStore } from 'react';
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 { bytesToBase64 } from "./attachment-preview.js";
5
- import { mintRequestId } from "./requests.js";
4
+ import { mintRequestId, uploadComposerFiles } from "./requests.js";
6
5
  import { TeamComposer } from "./TeamComposer.js";
7
- import { TeamPresenceDot } from "./TeamPresenceDot.js";
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
- * 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.
22
25
  */
23
26
  function mergeChannelView(current, fresh) {
24
- const known = new Set(current.items.map(item => item.message.messageRef));
25
- 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]
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
- 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 }) {
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 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 ?? []);
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
- setLoading(true);
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
- 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;
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 attachmentIds = [];
243
- for (const file of pendingFiles) {
244
- const uploaded = await putAttachment({
245
- requestId: mintRequestId(),
246
- workspaceId,
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
- 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');
298
380
  const rowError = membership.errors.get(status.member.memberId);
299
- 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);
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 }), _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);
309
- }) }, `run-${block.items[0].message.messageRef}`))] }) }), channel !== undefined ? _jsx(TeamComposer, { members: channelMembers, recipients: recipients, draft: draft, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onDraftChange: next => { drafts.writeDraft(draftKey, next); setStatusMessage(undefined); }, onRecipientsChange: next => { drafts.writeRecipients(draftKey, next); setStatusMessage(undefined); }, onSubmit: () => { void send(); }, pendingFiles: pendingFiles, onFilesChange: setPendingFiles, asTask: asTask, onAsTaskChange: setAsTask, t: t }, draftKey) : _jsx("div", {})] });
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;AAalD,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,+BA+L9D"}
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"}