@wowyuarm/dsh-agent-team 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +31 -23
  2. package/packages/agent-team/lib/index.js +51 -62
  3. package/packages/agent-team/lib/ledger.js +63 -39
  4. package/packages/agent-team/lib/spec.js +10 -4
  5. package/packages/agent-team/lib/typert.host.js +60 -73
  6. package/packages/agent-team/lib/typert.remote-client.d.ts +3 -3
  7. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  8. package/packages/agent-team/lib/typert.remote-client.js +41 -56
  9. package/packages/agent-team/lib/types/index.d.ts +10 -13
  10. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  11. package/packages/agent-team/lib/types/ledger.d.ts +11 -11
  12. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  13. package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
  14. package/packages/agent-team/lib/types/types.d.ts +20 -12
  15. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  16. package/packages/agent-team/preset/team-member/agent.cordis.yml +2 -2
  17. package/packages/client-agent-team/README.md +3 -2
  18. package/packages/client-agent-team/README.zh.md +3 -2
  19. package/packages/client-agent-team/lib/client.js +758 -184
  20. package/packages/client-agent-team/lib/client.js.map +1 -1
  21. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +1 -2
  22. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
  23. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +20 -17
  24. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +3 -1
  25. package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
  26. package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +38 -2
  27. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
  28. package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +16 -2
  29. package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
  30. package/packages/client-agent-team/lib/types/client/TeamComposer.js +22 -1
  31. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
  32. package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
  33. package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
  34. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +7 -2
  35. package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
  36. package/packages/client-agent-team/lib/types/client/TeamMessage.js +123 -11
  37. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +4 -0
  38. package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
  39. package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +78 -8
  40. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
  41. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
  42. package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +2 -2
  43. package/packages/client-agent-team/lib/types/client/index.js +1 -1
  44. package/packages/client-agent-team/lib/types/client/locales.d.ts +8 -2
  45. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  46. package/packages/client-agent-team/lib/types/client/locales.js +8 -2
  47. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts +35 -0
  48. package/packages/client-agent-team/lib/types/client/sidebar-drag.d.ts.map +1 -0
  49. package/packages/client-agent-team/lib/types/client/sidebar-drag.js +70 -0
  50. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts +27 -0
  51. package/packages/client-agent-team/lib/types/client/sidebar-order.d.ts.map +1 -0
  52. package/packages/client-agent-team/lib/types/client/sidebar-order.js +170 -0
  53. package/packages/client-agent-team/lib/types/client/slots.d.ts +2 -2
  54. package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
  55. package/packages/client-agent-team/lib/types/client/task-refs.d.ts +20 -0
  56. package/packages/client-agent-team/lib/types/client/task-refs.d.ts.map +1 -0
  57. package/packages/client-agent-team/lib/types/client/task-refs.js +58 -0
  58. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +13 -0
  59. package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/team-formatters.js +26 -2
  61. package/packages/tool-agent-team/lib/index.js +4 -4
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Fragment, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
- import { Button, DisclosureRow, IconChevronLeftOutline14, IconChecklistOutline14, Pill } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { Button, DisclosureRow, IconChevronLeftOutline14, IconChecklistOutline14, Modal, Pill } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { TeamComposer } from "./TeamComposer.js";
5
5
  import { TeamPresenceDot } from "./TeamPresenceDot.js";
6
6
  import { TeamMessage } from "./TeamMessage.js";
@@ -8,6 +8,8 @@ import { TeamRunDivider } from "./TeamRunDivider.js";
8
8
  import { formatActivity, formatClaimState, formatTaskStatus, formatTaskTitle, mentionNamesOf } from "./team-formatters.js";
9
9
  import { daySeparatorLabel, isRunGap, timelineDayKey } from "./team-separators.js";
10
10
  import { useTimelineScroll } from "./timeline-scroll.js";
11
+ import { rememberResolvedTaskRef } from "./task-refs.js";
12
+ import { bytesToBase64 } from "./attachment-preview.js";
11
13
  import css from './conversation.module.css';
12
14
  import threadCss from './thread.module.css';
13
15
  function factKey(fact) {
@@ -30,7 +32,7 @@ function readMeta(facts) {
30
32
  return new Map(facts.map(fact => [factKey(fact.fact), fact]));
31
33
  }
32
34
  export function TeamThreadPage(props) {
33
- const { workspaceId, channelRef, taskRef, threadRef, taskNumber, backToWorkspace, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, drafts, getAttachment, reply, changeTask, t, } = props;
35
+ const { workspaceId, channelRef, taskRef, threadRef, taskNumber, backToWorkspace, selectChannel, selectThread, resolveTaskRefs, putAttachment, loadChannels, readThread, loadThreadHistory, subscribeChanges, loadMembers, drafts, getAttachment, reply, changeTask, t, } = props;
34
36
  const [projection, setProjection] = useState();
35
37
  const [channelView, setChannelView] = useState();
36
38
  const [members, setMembers] = useState([]);
@@ -46,6 +48,13 @@ export function TeamThreadPage(props) {
46
48
  const draftKey = `thread:${threadRef}`;
47
49
  const { draft, recipients } = useSyncExternalStore(drafts.subscribe, () => drafts.getSnapshot(draftKey));
48
50
  const [claimsOpen, setClaimsOpen] = useState(false);
51
+ // Early acceptance: the Human may accept while Claims are still open; the
52
+ // confirm dialog lists exactly what will be completed with the Task.
53
+ const [confirmingAccept, setConfirmingAccept] = useState(false);
54
+ useEffect(() => {
55
+ if (confirmingAccept && projection?.task.resolution === 'accepted')
56
+ setConfirmingAccept(false);
57
+ }, [confirmingAccept, projection?.task.resolution]);
49
58
  const [replyRequestId, setReplyRequestId] = useState();
50
59
  const [confirmation, setConfirmation] = useState();
51
60
  const [statusMessage, setStatusMessage] = useState();
@@ -294,11 +303,38 @@ export function TeamThreadPage(props) {
294
303
  return status === undefined ? t('memberUnknown') : `@${status.member.handle}`;
295
304
  };
296
305
  const messageSender = (fact) => fact.kind === 'message' ? fact.message.sender : undefined;
306
+ const [pendingFiles, setPendingFiles] = useState([]);
307
+ // Branded-ref navigation for message bodies: channel refs hop to the
308
+ // Channel; the open Task's own refs are already on screen, and other Tasks
309
+ // are not resolvable from this surface, so they degrade to a no-op.
310
+ const openRef = (ref) => {
311
+ if (ref.startsWith('channel:') && ref !== channelRef)
312
+ selectChannel(ref);
313
+ if (ref.startsWith('task:') && ref !== taskRef) {
314
+ // Another Task cited here: resolve its home Channel and jump.
315
+ void resolveTaskRefs({ workspaceId, taskRefs: [ref] })
316
+ .then(result => {
317
+ if (!result.ok)
318
+ return;
319
+ const hit = result.value.resolved[0];
320
+ if (hit !== undefined)
321
+ selectThread(hit.taskRef, hit.threadRef, hit.channelRef, hit.taskNumber);
322
+ });
323
+ }
324
+ };
325
+ const lookupTaskRefs = (refs) => resolveTaskRefs({ workspaceId, taskRefs: refs })
326
+ .then(result => {
327
+ if (!result.ok)
328
+ return [];
329
+ for (const entry of result.value.resolved)
330
+ rememberResolvedTaskRef(entry);
331
+ return result.value.resolved;
332
+ });
297
333
  const renderFact = (fact, grouped = false) => {
298
334
  if (fact.kind === 'message') {
299
335
  const sender = memberName(fact.message.sender);
300
336
  const senderStatus = members.find(candidate => candidate.member.memberId === fact.message.sender);
301
- return _jsx(TeamMessage, { senderName: sender, memberId: fact.message.sender, human: fact.message.sender === channelView?.humanMemberId, body: fact.message.body, attachments: fact.message.attachments, loadAttachment: getAttachment, t: t, occurredAt: fact.message.occurredAt, mentionNames: mentionNamesOf(fact.mentions, mentionHandlesMap), grouped: grouped, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }) }, factKey(fact));
337
+ return _jsx(TeamMessage, { senderName: sender, memberId: fact.message.sender, human: fact.message.sender === channelView?.humanMemberId, body: fact.message.body, attachments: fact.message.attachments, loadAttachment: getAttachment, t: t, occurredAt: fact.message.occurredAt, mentionNames: mentionNamesOf(fact.mentions, mentionHandlesMap), onOpenRef: openRef, onResolveTaskRefs: lookupTaskRefs, grouped: grouped, ...(senderStatus === undefined ? {} : { senderTitle: senderStatus.member.description }) }, factKey(fact));
302
338
  }
303
339
  return _jsxs("p", { className: threadCss.activityRow, children: [_jsx("span", { className: threadCss.activityMark, "aria-hidden": "true" }), _jsx("span", { className: threadCss.activityText, children: formatActivity(fact.activity, { t, actorName: memberName, claims: taskClaims }) })] }, factKey(fact));
304
340
  };
@@ -400,7 +436,24 @@ export function TeamThreadPage(props) {
400
436
  setPending(true);
401
437
  setError(undefined);
402
438
  try {
403
- const result = await reply({ requestId: id, workspaceId, taskRef: task.taskRef, body: draft.trim(), baseRevision: thread.revision, recipients: [...recipients].sort(), ...(confirmation === undefined ? {} : { confirmationToken: confirmation }) });
439
+ // Upload chosen files first; any failure aborts the reply with the
440
+ // existing error surface and keeps the chips for a retry.
441
+ const attachmentIds = [];
442
+ for (const file of pendingFiles) {
443
+ const uploaded = await putAttachment({
444
+ requestId: crypto.randomUUID(),
445
+ workspaceId,
446
+ name: file.name,
447
+ mediaType: file.type === '' ? undefined : file.type,
448
+ bytesBase64: bytesToBase64(new Uint8Array(await file.arrayBuffer())),
449
+ });
450
+ if (!uploaded.ok) {
451
+ setError(uploaded.error.message);
452
+ return;
453
+ }
454
+ attachmentIds.push(uploaded.value.attachmentId);
455
+ }
456
+ const result = await reply({ requestId: id, workspaceId, taskRef: task.taskRef, body: draft.trim(), baseRevision: thread.revision, recipients: [...recipients].sort(), ...(attachmentIds.length === 0 ? {} : { attachments: attachmentIds }), ...(confirmation === undefined ? {} : { confirmationToken: confirmation }) });
404
457
  if (!result.ok) {
405
458
  setError(result.error.message);
406
459
  return;
@@ -414,6 +467,7 @@ export function TeamThreadPage(props) {
414
467
  });
415
468
  setProjection(current => current === undefined ? current : { ...current, task: committed.task, thread: committed.thread });
416
469
  drafts.clear(draftKey);
470
+ setPendingFiles([]);
417
471
  setReplyRequestId(undefined);
418
472
  setConfirmation(undefined);
419
473
  setStatusMessage(undefined);
@@ -460,12 +514,28 @@ export function TeamThreadPage(props) {
460
514
  // Threads are always entered through a Channel page, so a Channel origin
461
515
  // returns to its timeline; a Thread restored without one returns further.
462
516
  const backLabel = channelRef === undefined ? t('backToWorkspace') : t('backToChannel');
463
- return _jsxs("main", { className: css.surface, "data-team-thread": threadRef, children: [_jsxs("div", { className: css.surfaceHeader, children: [_jsx("div", { className: css.backRow, children: _jsx(Button, { size: "sm", icon: _jsx(IconChevronLeftOutline14, {}), onClick: backToWorkspace, children: backLabel }) }), _jsxs("header", { className: css.headerRow, children: [_jsxs("div", { className: css.headerCopy, children: [_jsxs("div", { className: threadCss.titleLine, children: [_jsx("h1", { children: task === undefined ? t('taskPending') : t('taskLabel', { number: taskNumber ?? '…' }) }), task !== undefined && _jsx(Pill, { children: formatTaskStatus(task.status, t) })] }), taskTitle !== undefined && taskTitle !== '' && _jsx("p", { className: threadCss.taskTitle, children: taskTitle })] }), task !== undefined && thread !== undefined && task.resolution !== 'closed' && _jsxs("div", { className: css.headerActions, children: [task.status === 'in_review' && task.resolution === 'open' && _jsx(Button, { size: "sm", variant: "primary", disabled: pending, onClick: () => { void mutateTask('accept'); }, children: t('acceptTask') }), task.resolution === 'open'
517
+ return _jsxs("main", { className: css.surface, "data-team-thread": threadRef, children: [_jsxs("div", { className: css.surfaceHeader, children: [_jsx("div", { className: css.backRow, children: _jsx(Button, { size: "sm", icon: _jsx(IconChevronLeftOutline14, {}), onClick: backToWorkspace, children: backLabel }) }), _jsxs("header", { className: css.headerRow, children: [_jsxs("div", { className: css.headerCopy, children: [_jsxs("div", { className: threadCss.titleLine, children: [_jsx("h1", { children: task === undefined ? t('taskPending') : t('taskLabel', { number: taskNumber ?? '…' }) }), task !== undefined && _jsx(Pill, { children: formatTaskStatus(task.status, t) })] }), taskTitle !== undefined && taskTitle !== '' && _jsx("p", { className: threadCss.taskTitle, children: taskTitle })] }), task !== undefined && thread !== undefined && task.resolution !== 'closed' && _jsxs("div", { className: css.headerActions, children: [(() => {
518
+ const activeClaims = projection?.claims.filter(claim => claim.taskRef === task.taskRef && claim.state === 'active') ?? [];
519
+ const earlyAccept = task.resolution === 'open' && task.status === 'in_progress' && activeClaims.length > 0;
520
+ if (!(task.status === 'in_review' || earlyAccept) || task.resolution !== 'open')
521
+ return null;
522
+ return _jsx(Button, { size: "sm", variant: "primary", disabled: pending, onClick: () => { if (earlyAccept) {
523
+ setConfirmingAccept(true);
524
+ }
525
+ else {
526
+ void mutateTask('accept');
527
+ } }, children: t('acceptTask') });
528
+ })(), task.resolution === 'open'
464
529
  ? _jsx(Button, { size: "sm", variant: "outline", disabled: pending, onClick: () => { void mutateTask('close'); }, children: t('closeTask') })
465
- : _jsx(Button, { size: "sm", variant: "primary", disabled: pending, onClick: () => { void mutateTask('reopen'); }, children: t('reopenTask') })] })] }), risks.length > 0 && _jsxs("section", { className: threadCss.riskSection, "aria-label": t('runtimeRisk'), children: [_jsx("h2", { children: t('runtimeRisk') }), risks.map(({ claim, status }) => _jsxs("p", { className: threadCss.riskRow, children: [_jsx(TeamPresenceDot, { status: status, t: t }), _jsxs("span", { children: [t('runtimeRiskDetail', { member: status.member.handle, diagnostic: status.diagnostic ?? t('statusError') }), " \u00B7 ", claim.direction] })] }, claim.claimRef))] }), task !== undefined && thread !== undefined && _jsx("section", { className: threadCss.workSection, "aria-label": t('claims'), children: _jsx(DisclosureRow, { expandOnRowClick: true, expandable: true, open: claimsOpen, onToggle: () => { setClaimsOpen(current => !current); }, icon: _jsx(IconChecklistOutline14, { size: 14 }), title: `${t('claims')} · ${taskClaims.length}`, children: _jsxs("div", { className: threadCss.claimList, children: [taskClaims.length === 0 && _jsx("p", { className: threadCss.emptyClaims, children: t('noClaims') }), taskClaims.map(claim => {
530
+ : _jsx(Button, { size: "sm", variant: "primary", disabled: pending, onClick: () => { void mutateTask('reopen'); }, children: t('reopenTask') })] })] }), risks.length > 0 && _jsxs("section", { className: threadCss.riskSection, "aria-label": t('runtimeRisk'), children: [_jsx("h2", { children: t('runtimeRisk') }), risks.map(({ claim, status }) => _jsxs("p", { className: threadCss.riskRow, children: [_jsx(TeamPresenceDot, { status: status, t: t }), _jsxs("span", { children: [t('runtimeRiskDetail', { member: status.member.handle, diagnostic: status.diagnostic ?? t('statusError') }), " \u00B7 ", claim.direction] })] }, claim.claimRef))] }), task !== undefined && thread !== undefined && (() => {
531
+ // Recomputed here so the confirm list never shows stale rows.
532
+ const activeClaims = projection?.claims.filter(claim => claim.taskRef === task.taskRef && claim.state === 'active') ?? [];
533
+ return _jsxs(Modal, { open: confirmingAccept, onClose: () => { if (!pending)
534
+ setConfirmingAccept(false); }, title: t('acceptEarlyTitle'), closeLabel: t('cancel'), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", disabled: pending, onClick: () => { setConfirmingAccept(false); }, children: t('cancel') }), _jsx(Button, { variant: "primary", disabled: pending, onClick: () => { void mutateTask('accept'); }, children: pending ? t('acceptingTask') : t('acceptTask') })] }), children: [_jsx("p", { className: css.confirmBody, children: t('acceptEarlyBody', { count: activeClaims.length }) }), _jsx("ul", { className: css.confirmList, children: activeClaims.map(claim => (_jsxs("li", { children: [memberName(claim.owner), " \u00B7 ", claim.direction] }, claim.claimRef))) })] });
535
+ })(), task !== undefined && thread !== undefined && _jsx("section", { className: threadCss.workSection, "aria-label": t('claims'), children: _jsx(DisclosureRow, { expandOnRowClick: true, expandable: true, open: claimsOpen, onToggle: () => { setClaimsOpen(current => !current); }, icon: _jsx(IconChecklistOutline14, { size: 14 }), title: `${t('claims')} · ${taskClaims.length}`, children: _jsxs("div", { className: threadCss.claimList, children: [taskClaims.length === 0 && _jsx("p", { className: threadCss.emptyClaims, children: t('noClaims') }), taskClaims.map(claim => {
466
536
  const ownerStatus = members.find(status => status.member.memberId === claim.owner);
467
537
  return _jsxs("article", { className: threadCss.claimRow, children: [ownerStatus === undefined ? _jsx("span", {}) : _jsx(TeamPresenceDot, { status: ownerStatus, t: t }), _jsx("strong", { className: threadCss.claimOwner, children: memberName(claim.owner) }), _jsx("span", { className: threadCss.claimDirection, children: claim.direction }), _jsx("small", { className: threadCss.claimState, children: formatClaimState(claim.state, t) })] }, claim.claimRef);
468
538
  })] }) }) })] }), _jsx("section", { ref: timeline.ref, onScroll: timeline.onScroll, className: css.timeline, "aria-label": t('timelineLabel'), children: _jsxs("div", { className: css.timelineContent, children: [loading && projection === undefined && error === undefined && _jsx("div", { className: css.emptySurface, children: _jsxs("p", { className: css.loadingState, children: [_jsx("span", { className: css.loadingMark, "aria-hidden": "true" }), t('loadingThread')] }) }), projection === undefined && error !== undefined && _jsxs("div", { className: css.errorState, role: "alert", children: [_jsx("span", { children: error }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => { void readCurrent(); }, children: t('retry') })] }), olderFacts.length > 0 && _jsxs("section", { className: threadCss.historySection, "aria-label": t('olderHistory'), children: [_jsx("h2", { children: t('olderHistory') }), renderFactBlocks(olderFacts, undefined)] }), historyHasMore && _jsx("div", { className: css.timelineAction, children: _jsx(Button, { size: "sm", onClick: () => { void loadOlder(); }, children: t('loadOlder') }) }), currentFactsWithAnchor.length > 0 && _jsx("section", { className: threadCss.publicSection, children: renderFactBlocks(currentFactsWithAnchor, unreadBoundary) }), newFactsCount > 0 && _jsxs("div", { className: threadCss.newUpdates, role: "status", children: [_jsx("span", { children: t('readNewUpdates', { count: newFactsCount }) }), _jsx(Button, { size: "sm", variant: "outline", disabled: loading, onClick: () => { timeline.jumpToLatest(); void readCurrent(true); }, children: t('markRead') })] }), remainingUnreadCount > 0 && _jsxs("div", { className: css.timelineAction, role: "status", children: [_jsx("span", { children: t('remainingUnread', { count: remainingUnreadCount }) }), _jsx(Button, { size: "sm", onClick: () => { timeline.jumpToBoundary(); void readCurrent(true); }, disabled: loading, children: t('continueReading') })] })] }) }), projection !== undefined && task !== undefined && thread !== undefined ? (task.resolution === 'closed'
469
539
  ? _jsxs("div", { className: threadCss.closedBar, "data-team-closed": true, children: [error !== undefined && _jsx("p", { className: css.error, role: "alert", children: error }), _jsxs("div", { className: threadCss.closedNotice, children: [_jsx("span", { children: t('taskClosedNotice') }), _jsx(Button, { size: "sm", variant: "outline", disabled: pending, onClick: () => { void mutateTask('reopen'); }, children: t('reopenTask') })] })] })
470
- : _jsx(TeamComposer, { members: channelMembers, recipients: recipients, draft: draft, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onDraftChange: next => { drafts.writeDraft(draftKey, next); setConfirmation(undefined); setReplyRequestId(undefined); setStatusMessage(undefined); }, onRecipientsChange: next => { drafts.writeRecipients(draftKey, next); setConfirmation(undefined); setReplyRequestId(undefined); setStatusMessage(undefined); }, onSubmit: () => { void sendReply(); }, t: t }, draftKey)) : _jsx("div", {})] });
540
+ : _jsx(TeamComposer, { members: channelMembers, recipients: recipients, draft: draft, pending: pending, ...(statusMessage === undefined ? {} : { confirmation: statusMessage }), ...(error === undefined ? {} : { error }), onDraftChange: next => { drafts.writeDraft(draftKey, next); setConfirmation(undefined); setReplyRequestId(undefined); setStatusMessage(undefined); }, onRecipientsChange: next => { drafts.writeRecipients(draftKey, next); setConfirmation(undefined); setReplyRequestId(undefined); setStatusMessage(undefined); }, onSubmit: () => { void sendReply(); }, pendingFiles: pendingFiles, onFilesChange: setPendingFiles, t: t }, draftKey)) : _jsx("div", {})] });
471
541
  }
@@ -1,3 +1,3 @@
1
1
  import type { TeamSidebarProps } from './slots.ts';
2
- export declare function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWorkspace, selectChannel, t, useWorkspaces, loadMembers, subscribeChanges, addMember, loadChannels, createChannel, updateChannel, updateMember, recoverMember, restartMember, joinChannel, removeChannelMember, loadModels, openMemberSession }: TeamSidebarProps): import("react").JSX.Element;
2
+ export declare function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWorkspace, selectChannel, t, useWorkspaces, loadMembers, subscribeChanges, addMember, loadChannels, createChannel, updateChannel, updateMember, recoverMember, joinChannel, removeChannelMember, loadModels, openMemberSession }: TeamSidebarProps): import("react").JSX.Element;
3
3
  //# sourceMappingURL=TeamWorkspaceBrowser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TeamWorkspaceBrowser.d.ts","sourceRoot":"","sources":["../../../src/client/TeamWorkspaceBrowser.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAQlD,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,mBAAmB,EAAE,UAAU,EAAE,iBAAiB,EAAE,EAAE,gBAAgB,+BA4D9U"}
1
+ {"version":3,"file":"TeamWorkspaceBrowser.d.ts","sourceRoot":"","sources":["../../../src/client/TeamWorkspaceBrowser.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAQlD,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,mBAAmB,EAAE,UAAU,EAAE,iBAAiB,EAAE,EAAE,gBAAgB,+BA4D/T"}
@@ -5,7 +5,7 @@ import { TeamWorkspaceRow } from "./TeamWorkspaceRow.js";
5
5
  import { TeamAgentsPanel } from "./TeamAgentsPanel.js";
6
6
  import { TeamChannelsPanel } from "./TeamChannelsPanel.js";
7
7
  import css from './sidebar.module.css';
8
- export function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWorkspace, selectChannel, t, useWorkspaces, loadMembers, subscribeChanges, addMember, loadChannels, createChannel, updateChannel, updateMember, recoverMember, restartMember, joinChannel, removeChannelMember, loadModels, openMemberSession }) {
8
+ export function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWorkspace, selectChannel, t, useWorkspaces, loadMembers, subscribeChanges, addMember, loadChannels, createChannel, updateChannel, updateMember, recoverMember, joinChannel, removeChannelMember, loadModels, openMemberSession }) {
9
9
  const navigationState = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot, navigation.getSnapshot);
10
10
  const workspaces = useWorkspaces(state => state.items);
11
11
  const selected = navigationState.workspaceId;
@@ -36,5 +36,5 @@ export function TeamWorkspaceBrowser({ wide, expandSidebar, navigation, selectWo
36
36
  if (!wide) {
37
37
  return _jsxs("nav", { className: css.railWorkspace, "aria-label": t('workspaceSections'), children: [_jsx(Tooltip, { label: t('channels'), side: "right", children: _jsx("button", { type: "button", className: css.railButton, "aria-label": t('channels'), onClick: () => { setPendingSection('channels'); expandSidebar(); }, children: _jsx(IconListPenOutline16, { size: 16 }) }) }), _jsx(Tooltip, { label: t('agents'), side: "right", children: _jsx("button", { type: "button", className: css.railButton, "aria-label": t('agents'), onClick: () => { setPendingSection('agents'); expandSidebar(); }, children: _jsx(IconAgentPresetOutline16, { size: 16 }) }) })] });
38
38
  }
39
- return _jsxs("section", { className: css.workspaceBrowser, "aria-label": t('workspaces'), children: [_jsx("div", { className: css.workspaceHeader, children: t('workspaces') }), _jsxs("div", { className: css.workspaceList, children: [workspaces.map(workspace => _jsx(TeamWorkspaceRow, { workspaceId: workspace.workspaceId, title: workspace.title, path: workspace.path, selected: workspace.workspaceId === selectedId, current: workspace.workspaceId === selectedId && overviewIsCurrent, onSelect: selectWorkspace }, workspace.workspaceId)), workspaces.length === 0 && _jsx("p", { className: css.emptyState, children: t('empty') })] }), selectedId !== undefined && _jsxs("div", { className: css.workspaceSection, children: [_jsx("div", { ref: channelsRef, children: _jsx(TeamChannelsPanel, { workspaceId: selectedId, loadMembers: loadMembers, loadChannels: loadChannels, subscribeChanges: subscribeChanges, createChannel: createChannel, updateChannel: updateChannel, joinChannel: joinChannel, removeChannelMember: removeChannelMember, creatingAgents: creatingAgents.filter(request => request.workspaceId === selectedId), ...(navigationState.memberSessionId !== undefined || navigationState.channelRef === undefined ? {} : { selectedChannelRef: navigationState.channelRef }), selectChannel: selectChannel, t: t }, selectedId) }), _jsx("div", { ref: agentsRef, children: _jsx(TeamAgentsPanel, { workspaceId: selectedId, loadMembers: loadMembers, subscribeChanges: subscribeChanges, loadChannels: loadChannels, addMember: addMember, updateMember: updateMember, recoverMember: recoverMember, restartMember: restartMember, joinChannel: joinChannel, removeChannelMember: removeChannelMember, loadModels: loadModels, ...(navigationState.memberSessionId === undefined ? {} : { memberSessionId: navigationState.memberSessionId }), openMemberSession: openMemberSession, onCreatingChange: (request, creating) => { setCreatingAgents(current => creating ? [...current.filter(item => item.requestId !== request.requestId), request] : current.filter(item => item.requestId !== request.requestId)); }, t: t }, selectedId) })] })] });
39
+ return _jsxs("section", { className: css.workspaceBrowser, "aria-label": t('workspaces'), children: [_jsx("div", { className: css.workspaceHeader, children: t('workspaces') }), _jsxs("div", { className: css.workspaceList, children: [workspaces.map(workspace => _jsx(TeamWorkspaceRow, { workspaceId: workspace.workspaceId, title: workspace.title, path: workspace.path, selected: workspace.workspaceId === selectedId, current: workspace.workspaceId === selectedId && overviewIsCurrent, onSelect: selectWorkspace }, workspace.workspaceId)), workspaces.length === 0 && _jsx("p", { className: css.emptyState, children: t('empty') })] }), selectedId !== undefined && _jsxs("div", { className: css.workspaceSection, children: [_jsx("div", { ref: channelsRef, children: _jsx(TeamChannelsPanel, { workspaceId: selectedId, loadMembers: loadMembers, loadChannels: loadChannels, subscribeChanges: subscribeChanges, createChannel: createChannel, updateChannel: updateChannel, joinChannel: joinChannel, removeChannelMember: removeChannelMember, creatingAgents: creatingAgents.filter(request => request.workspaceId === selectedId), ...(navigationState.memberSessionId !== undefined || navigationState.channelRef === undefined ? {} : { selectedChannelRef: navigationState.channelRef }), selectChannel: selectChannel, t: t }, selectedId) }), _jsx("div", { ref: agentsRef, children: _jsx(TeamAgentsPanel, { workspaceId: selectedId, loadMembers: loadMembers, subscribeChanges: subscribeChanges, loadChannels: loadChannels, addMember: addMember, updateMember: updateMember, recoverMember: recoverMember, joinChannel: joinChannel, removeChannelMember: removeChannelMember, loadModels: loadModels, ...(navigationState.memberSessionId === undefined ? {} : { memberSessionId: navigationState.memberSessionId }), openMemberSession: openMemberSession, onCreatingChange: (request, creating) => { setCreatingAgents(current => creating ? [...current.filter(item => item.requestId !== request.requestId), request] : current.filter(item => item.requestId !== request.requestId)); }, t: t }, selectedId) })] })] });
40
40
  }
@@ -24,7 +24,6 @@ function registerModeShadow(ctx, navigation, changes, drafts, name, component, e
24
24
  updateChannel: (request) => ctx.remote.agentTeam.updateChannel(request),
25
25
  updateMember: (request) => ctx.remote.agentTeam.updateMember(request),
26
26
  recoverMember: (request) => ctx.remote.agentTeam.recoverMember(request),
27
- restartMember: (request) => ctx.remote.agentTeam.restartMember(request),
28
27
  // The Host-scoped catalog needs no live Member, so suspended ones stay editable too.
29
28
  loadModels: async () => {
30
29
  const connection = ctx.get('connection');
@@ -71,6 +70,7 @@ function registerModeShadow(ctx, navigation, changes, drafts, name, component, e
71
70
  getAttachment: (request) => ctx.remote.agentTeam.getAttachment(request),
72
71
  reply: (request) => ctx.remote.agentTeam.reply(request),
73
72
  changeTask: (request) => ctx.remote.agentTeam.changeTask(request),
73
+ resolveTaskRefs: (request) => ctx.remote.agentTeam.resolveTaskRefs(request),
74
74
  } : {}),
75
75
  ...(name === 'sidebar.workspaces' ? {
76
76
  addMember: (request) => ctx.remote.agentTeam.addMember(request),
@@ -37,7 +37,6 @@ export declare const zh: {
37
37
  readonly editChannel: "编辑频道";
38
38
  readonly editAgent: "编辑 Agent";
39
39
  readonly resumeAgent: "恢复";
40
- readonly restartAgent: "重启会话";
41
40
  readonly optionalSuffix: "(可选)";
42
41
  readonly agentDescriptionPlaceholder: "留空则暂无描述";
43
42
  readonly channelsPickerEmpty: "选择初始频道";
@@ -115,6 +114,10 @@ export declare const zh: {
115
114
  readonly activityClaimReleased: "{actor} 释放了「{direction}」";
116
115
  readonly activityClaimsReleased: "{actor} 因成员权限变化释放了 {count} 个 Claim";
117
116
  readonly activityAccepted: "{actor} 验收了此 Task";
117
+ readonly activityAcceptedWithClaims: "{actor} 验收了此 Task,并一并完成了 {count} 个未完成 Claim";
118
+ readonly acceptEarlyTitle: "提前验收任务";
119
+ readonly acceptEarlyBody: "将验收本 Task,并把以下 {count} 个未完成的 Claim 一并标记为完成:";
120
+ readonly acceptingTask: "验收中…";
118
121
  readonly activityClosed: "{actor} 关闭了此 Task";
119
122
  readonly activityReopened: "{actor} 重新打开了此 Task";
120
123
  readonly mentionConfirmation: "提及未关注成员需要确认,再次发送以继续";
@@ -162,7 +165,6 @@ export declare const en: {
162
165
  readonly editChannel: "Edit Channel";
163
166
  readonly editAgent: "Edit Agent";
164
167
  readonly resumeAgent: "Resume";
165
- readonly restartAgent: "Restart session";
166
168
  readonly optionalSuffix: " (optional)";
167
169
  readonly agentDescriptionPlaceholder: "Leave empty if not needed";
168
170
  readonly channelsPickerEmpty: "Choose initial channels";
@@ -240,6 +242,10 @@ export declare const en: {
240
242
  readonly activityClaimReleased: "{actor} released “{direction}”";
241
243
  readonly activityClaimsReleased: "{actor} released {count} Claim(s) after a membership change";
242
244
  readonly activityAccepted: "{actor} accepted this Task";
245
+ readonly activityAcceptedWithClaims: "{actor} accepted this Task and completed {count} unfinished Claims with it";
246
+ readonly acceptEarlyTitle: "Accept Task early";
247
+ readonly acceptEarlyBody: "This accepts the Task and marks the following {count} unfinished Claims as done:";
248
+ readonly acceptingTask: "Accepting…";
243
249
  readonly activityClosed: "{actor} closed this Task";
244
250
  readonly activityReopened: "{actor} reopened this Task";
245
251
  readonly mentionConfirmation: "Mentioning an unfollowed member requires confirmation; send again to continue";
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4HL,CAAA;AAEV,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4HL,CAAA;AAEV,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,EAAE,CAAA"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+HL,CAAA;AAEV,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+HL,CAAA;AAEV,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,EAAE,CAAA"}
@@ -37,7 +37,6 @@ export const zh = {
37
37
  editChannel: '编辑频道',
38
38
  editAgent: '编辑 Agent',
39
39
  resumeAgent: '恢复',
40
- restartAgent: '重启会话',
41
40
  optionalSuffix: '(可选)',
42
41
  agentDescriptionPlaceholder: '留空则暂无描述',
43
42
  channelsPickerEmpty: '选择初始频道',
@@ -115,6 +114,10 @@ export const zh = {
115
114
  activityClaimReleased: '{actor} 释放了「{direction}」',
116
115
  activityClaimsReleased: '{actor} 因成员权限变化释放了 {count} 个 Claim',
117
116
  activityAccepted: '{actor} 验收了此 Task',
117
+ activityAcceptedWithClaims: '{actor} 验收了此 Task,并一并完成了 {count} 个未完成 Claim',
118
+ acceptEarlyTitle: '提前验收任务',
119
+ acceptEarlyBody: '将验收本 Task,并把以下 {count} 个未完成的 Claim 一并标记为完成:',
120
+ acceptingTask: '验收中…',
118
121
  activityClosed: '{actor} 关闭了此 Task',
119
122
  activityReopened: '{actor} 重新打开了此 Task',
120
123
  mentionConfirmation: '提及未关注成员需要确认,再次发送以继续',
@@ -162,7 +165,6 @@ export const en = {
162
165
  editChannel: 'Edit Channel',
163
166
  editAgent: 'Edit Agent',
164
167
  resumeAgent: 'Resume',
165
- restartAgent: 'Restart session',
166
168
  optionalSuffix: ' (optional)',
167
169
  agentDescriptionPlaceholder: 'Leave empty if not needed',
168
170
  channelsPickerEmpty: 'Choose initial channels',
@@ -240,6 +242,10 @@ export const en = {
240
242
  activityClaimReleased: '{actor} released “{direction}”',
241
243
  activityClaimsReleased: '{actor} released {count} Claim(s) after a membership change',
242
244
  activityAccepted: '{actor} accepted this Task',
245
+ activityAcceptedWithClaims: '{actor} accepted this Task and completed {count} unfinished Claims with it',
246
+ acceptEarlyTitle: 'Accept Task early',
247
+ acceptEarlyBody: 'This accepts the Task and marks the following {count} unfinished Claims as done:',
248
+ acceptingTask: 'Accepting…',
243
249
  activityClosed: '{actor} closed this Task',
244
250
  activityReopened: '{actor} reopened this Task',
245
251
  mentionConfirmation: 'Mentioning an unfollowed member requires confirmation; send again to continue',
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from 'react';
2
+ /** Insert side of an in-flight drop relative to the hovered row. */
3
+ export type SidebarDropMarker = 'before' | 'after';
4
+ export interface SidebarRowDragHandlers<K extends string> {
5
+ /** Per-row native wiring plus the marker this row currently shows. */
6
+ readonly rowProps: (orderKey: K) => {
7
+ readonly draggable: true;
8
+ readonly className?: string | undefined;
9
+ readonly onDragStart: (event: React.DragEvent<HTMLElement>) => void;
10
+ readonly onDragEnd: () => void;
11
+ readonly onDragOver: (event: React.DragEvent<HTMLElement>) => void;
12
+ readonly onDrop: (event: React.DragEvent<HTMLElement>) => void;
13
+ };
14
+ }
15
+ /**
16
+ * One drag gesture per panel instance. `onCommit` receives the moved ref,
17
+ * the drop target and the insertion side exactly once per completed gesture;
18
+ * releases outside the list never reach it.
19
+ */
20
+ export declare function useSidebarRowDrag<K extends string>({ refs, onCommit }: {
21
+ /** Effective current order of the owning list, used for hit validation. */
22
+ readonly refs: readonly K[];
23
+ readonly onCommit: (movedRef: K, targetRef: K, marker: SidebarDropMarker) => void;
24
+ }): SidebarRowDragHandlers<K>;
25
+ /**
26
+ * Transparent wrapper that owns the drop-marker styling for one draggable
27
+ * sidebar row. It adds no layout of its own; the styled row stays inside so
28
+ * hover/focus descendant selectors keep working.
29
+ */
30
+ export declare function SortableRow<K extends string>({ drag, orderKey, children }: {
31
+ readonly drag: SidebarRowDragHandlers<K>;
32
+ readonly orderKey: K;
33
+ readonly children: ReactNode;
34
+ }): import("react").JSX.Element;
35
+ //# sourceMappingURL=sidebar-drag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidebar-drag.d.ts","sourceRoot":"","sources":["../../../src/client/sidebar-drag.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,oEAAoE;AACpE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,OAAO,CAAA;AAElD,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,MAAM;IACtD,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK;QAClC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAA;QACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QACvC,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;QACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,IAAI,CAAA;QAC9B,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;QAClE,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;KAC/D,CAAA;CACF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IACtE,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAA;CAClF,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAwC5B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE;IAC1E,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAC,CAAA;IACxC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,+BAOA"}
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Native whole-row drag for the Team sidebar lists. Unlike the Harness
4
+ * workspace list there is no document-level acceptance here: hovering another
5
+ * row shows the before/after insertion marker and releasing on a row commits
6
+ * exactly once per gesture, while a release outside any row — including over
7
+ * the other sidebar list, whose panel never responds — just cancels without
8
+ * committing.
9
+ */
10
+ import { useRef, useState } from 'react';
11
+ import css from './sidebar.module.css';
12
+ /**
13
+ * One drag gesture per panel instance. `onCommit` receives the moved ref,
14
+ * the drop target and the insertion side exactly once per completed gesture;
15
+ * releases outside the list never reach it.
16
+ */
17
+ export function useSidebarRowDrag({ refs, onCommit }) {
18
+ const [state, setState] = useState(null);
19
+ const committed = useRef(false);
20
+ const half = (event) => {
21
+ const rect = event.currentTarget.getBoundingClientRect();
22
+ return event.clientY - rect.top < rect.height / 2 ? 'before' : 'after';
23
+ };
24
+ return {
25
+ rowProps: orderKey => ({
26
+ draggable: true,
27
+ className: state === null ? undefined
28
+ : state.active === orderKey ? css.sidebarRowDragging
29
+ : state.over === orderKey ? (state.marker === 'before' ? css.sidebarRowDropBefore : css.sidebarRowDropAfter)
30
+ : undefined,
31
+ onDragStart: event => {
32
+ if (event.dataTransfer === null)
33
+ return;
34
+ event.dataTransfer.effectAllowed = 'move';
35
+ event.dataTransfer.setData('text/plain', orderKey);
36
+ committed.current = false;
37
+ setState({ active: orderKey, over: orderKey, marker: 'after' });
38
+ },
39
+ onDragEnd: () => { setState(null); },
40
+ onDragOver: event => {
41
+ if (state === null || !refs.includes(orderKey))
42
+ return;
43
+ event.preventDefault();
44
+ if (event.dataTransfer !== null)
45
+ event.dataTransfer.dropEffect = 'move';
46
+ const marker = half(event);
47
+ if (state.over !== orderKey || state.marker !== marker)
48
+ setState({ ...state, over: orderKey, marker });
49
+ },
50
+ onDrop: event => {
51
+ if (state === null || !refs.includes(orderKey))
52
+ return;
53
+ event.preventDefault();
54
+ if (committed.current)
55
+ return;
56
+ committed.current = true;
57
+ onCommit(state.active, orderKey, half(event));
58
+ },
59
+ }),
60
+ };
61
+ }
62
+ /**
63
+ * Transparent wrapper that owns the drop-marker styling for one draggable
64
+ * sidebar row. It adds no layout of its own; the styled row stays inside so
65
+ * hover/focus descendant selectors keep working.
66
+ */
67
+ export function SortableRow({ drag, orderKey, children }) {
68
+ const props = drag.rowProps(orderKey);
69
+ return (_jsx("div", { ...props, children: children }));
70
+ }
@@ -0,0 +1,27 @@
1
+ import type { WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client';
2
+ /** Which sidebar list an order belongs to; the two ledgers are independent. */
3
+ export type TeamSidebarListKind = 'channels' | 'agents';
4
+ /** Insert side of a drop relative to the row under the pointer. */
5
+ export type SidebarDropMarker = 'before' | 'after';
6
+ /**
7
+ * Merge the saved personal order into the current Remote order: kept refs
8
+ * stay in the user's relative order with duplicates collapsed, removed refs
9
+ * disappear, and new refs join in their Remote default positions after the
10
+ * known ones.
11
+ */
12
+ export declare function reconcileSidebarOrder<R extends string>(saved: readonly string[], current: readonly R[]): readonly R[];
13
+ /**
14
+ * Effective row order for one sidebar list: the user's saved order folded
15
+ * into the given Remote default order. Identity-stable across renders while
16
+ * neither the data nor this browser's preference changes.
17
+ */
18
+ export declare function useSidebarOrder<R extends string>(workspaceId: WorkspaceId | undefined, kind: TeamSidebarListKind, refs: readonly R[]): readonly R[];
19
+ /**
20
+ * The only mutation path: move `movedRef` to the given side of `targetRef`
21
+ * inside the list's current effective order. Returns the new order, or
22
+ * `undefined` when the request cannot change anything (unknown refs, dropping
23
+ * a row onto itself, or an adjacent marker that would put it right back).
24
+ * Persistence and subscriber notification happen here.
25
+ */
26
+ export declare function moveSidebarItem<R extends string>(workspaceId: WorkspaceId | undefined, kind: TeamSidebarListKind, refs: readonly R[], movedRef: R, targetRef: R, marker: SidebarDropMarker): readonly R[] | undefined;
27
+ //# sourceMappingURL=sidebar-order.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidebar-order.d.ts","sourceRoot":"","sources":["../../../src/client/sidebar-order.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAA;AAEzE,+EAA+E;AAC/E,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,QAAQ,CAAA;AAEvD,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,OAAO,CAAA;AA2ElD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,GAAG,SAAS,CAAC,EAAE,CAerH;AAqCD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,SAAS,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,SAAS,CAAC,EAAE,CAQnJ;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAC9C,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,QAAQ,EAAE,CAAC,EACX,SAAS,EAAE,CAAC,EACZ,MAAM,EAAE,iBAAiB,GACxB,SAAS,CAAC,EAAE,GAAG,SAAS,CAe1B"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Human presentation preference for the Team sidebar: the row order of the
3
+ * Channels and Agents lists. The order lives in this browser only — it is UI
4
+ * taste, never a shared Team fact, so nothing here touches the ledger.
5
+ *
6
+ * One deep module owns the whole concern: storage, reconcile against the
7
+ * current Remote order (drop removed refs, keep the user's relative order,
8
+ * append newcomers in default order), the single `moveSidebarItem` mutation
9
+ * behind whole-row drag, and a small subscription hook with snapshot-stable
10
+ * results for `useSyncExternalStore`.
11
+ */
12
+ import { useCallback, useSyncExternalStore } from 'react';
13
+ const STORAGE_KEY = 'dsh.agent-team.sidebar-order';
14
+ function listKey(workspaceId, kind) {
15
+ return workspaceId === undefined ? undefined : `${workspaceId}|${kind}`;
16
+ }
17
+ const MEMORY_ONLY = Symbol('sidebar-order.memory');
18
+ /**
19
+ * Read-through parse cache over the raw persisted payload: repeated snapshots
20
+ * skip re-validating an unchanged payload, while anything written outside this
21
+ * module — another tab, devtools, a cleared store — is picked up because the
22
+ * raw comparison misses.
23
+ */
24
+ let cachedRaw;
25
+ let cachedOrders = {};
26
+ /**
27
+ * Parse persisted orders defensively: private mode, quota errors, and stale
28
+ * or corrupted payloads all degrade to "no preference" instead of breaking
29
+ * the sidebar.
30
+ */
31
+ function storedOrders() {
32
+ let raw = null;
33
+ try {
34
+ raw = typeof localStorage === 'undefined' ? null : localStorage.getItem(STORAGE_KEY);
35
+ }
36
+ catch {
37
+ raw = null;
38
+ }
39
+ if (cachedRaw !== undefined && raw !== null && raw === cachedRaw)
40
+ return cachedOrders;
41
+ if (raw === null) {
42
+ if (cachedRaw === MEMORY_ONLY)
43
+ return cachedOrders;
44
+ cachedRaw = raw;
45
+ cachedOrders = {};
46
+ return cachedOrders;
47
+ }
48
+ try {
49
+ const parsed = JSON.parse(raw);
50
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
51
+ throw new Error('not an object');
52
+ const clean = {};
53
+ for (const [key, value] of Object.entries(parsed)) {
54
+ if (!Array.isArray(value) || !value.every(entry => typeof entry === 'string'))
55
+ continue;
56
+ clean[key] = value;
57
+ }
58
+ cachedRaw = raw;
59
+ cachedOrders = clean;
60
+ return cachedOrders;
61
+ }
62
+ catch {
63
+ return {};
64
+ }
65
+ }
66
+ function saveOrder(key, refs) {
67
+ const next = { ...structuredClone(storedOrders()), [key]: refs };
68
+ cachedOrders = next;
69
+ try {
70
+ if (typeof localStorage !== 'undefined') {
71
+ const raw = JSON.stringify(next);
72
+ localStorage.setItem(STORAGE_KEY, raw);
73
+ cachedRaw = raw;
74
+ return;
75
+ }
76
+ }
77
+ catch {
78
+ // Private mode and quota failures do not block reordering; the session
79
+ // keeps serving the in-memory order below.
80
+ }
81
+ cachedRaw = MEMORY_ONLY;
82
+ }
83
+ /**
84
+ * Merge the saved personal order into the current Remote order: kept refs
85
+ * stay in the user's relative order with duplicates collapsed, removed refs
86
+ * disappear, and new refs join in their Remote default positions after the
87
+ * known ones.
88
+ */
89
+ export function reconcileSidebarOrder(saved, current) {
90
+ const present = new Set(current);
91
+ const seen = new Set();
92
+ const merged = [];
93
+ for (const ref of saved) {
94
+ if (seen.has(ref) || !present.has(ref))
95
+ continue;
96
+ seen.add(ref);
97
+ merged.push(ref);
98
+ }
99
+ for (const ref of current) {
100
+ if (seen.has(ref))
101
+ continue;
102
+ seen.add(ref);
103
+ merged.push(ref);
104
+ }
105
+ return merged;
106
+ }
107
+ let revision = 0;
108
+ const listeners = new Set();
109
+ const snapshots = new Map();
110
+ function savedFingerprint(key) {
111
+ const saved = storedOrders()[key];
112
+ return saved === undefined ? '' : `f${saved.length}:${saved.join(',')}`;
113
+ }
114
+ function emitChange() {
115
+ revision += 1;
116
+ for (const listener of listeners)
117
+ listener();
118
+ }
119
+ function orderedSnapshot(workspaceId, kind, refs) {
120
+ const key = listKey(workspaceId, kind);
121
+ if (key === undefined)
122
+ return refs;
123
+ const signature = refs.join('\u0000');
124
+ const fingerprint = savedFingerprint(key);
125
+ const hit = snapshots.get(key);
126
+ if (hit !== undefined && hit.atRevision === revision && hit.signature === signature && hit.savedFingerprint === fingerprint) {
127
+ return hit.result;
128
+ }
129
+ const result = reconcileSidebarOrder(storedOrders()[key] ?? [], refs);
130
+ snapshots.set(key, { atRevision: revision, signature, savedFingerprint: fingerprint, result });
131
+ return result;
132
+ }
133
+ /**
134
+ * Effective row order for one sidebar list: the user's saved order folded
135
+ * into the given Remote default order. Identity-stable across renders while
136
+ * neither the data nor this browser's preference changes.
137
+ */
138
+ export function useSidebarOrder(workspaceId, kind, refs) {
139
+ const subscribe = useCallback((listener) => {
140
+ listeners.add(listener);
141
+ return () => { listeners.delete(listener); };
142
+ }, []);
143
+ return useSyncExternalStore(subscribe, () => orderedSnapshot(workspaceId, kind, refs), () => orderedSnapshot(workspaceId, kind, refs));
144
+ }
145
+ /**
146
+ * The only mutation path: move `movedRef` to the given side of `targetRef`
147
+ * inside the list's current effective order. Returns the new order, or
148
+ * `undefined` when the request cannot change anything (unknown refs, dropping
149
+ * a row onto itself, or an adjacent marker that would put it right back).
150
+ * Persistence and subscriber notification happen here.
151
+ */
152
+ export function moveSidebarItem(workspaceId, kind, refs, movedRef, targetRef, marker) {
153
+ const key = listKey(workspaceId, kind);
154
+ const from = refs.indexOf(movedRef);
155
+ const nextFromTarget = refs.indexOf(targetRef);
156
+ if (key === undefined || from < 0 || nextFromTarget < 0 || movedRef === targetRef)
157
+ return undefined;
158
+ const next = [...refs];
159
+ const [item] = next.splice(from, 1);
160
+ if (item === undefined)
161
+ return undefined;
162
+ const insertAt = next.indexOf(targetRef) + (marker === 'after' ? 1 : 0);
163
+ if (insertAt === from)
164
+ return undefined;
165
+ next.splice(insertAt, 0, item);
166
+ saveOrder(key, next);
167
+ snapshots.delete(key);
168
+ emitChange();
169
+ return next;
170
+ }