@anionex/dsh-pinned-sessions 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(index_exports);
30
30
  var import_react3 = require("react");
31
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
31
32
 
32
33
  // node_modules/lucide-react/dist/esm/createLucideIcon.js
33
34
  var import_react2 = require("react");
@@ -100,6 +101,54 @@ var createLucideIcon = (iconName, iconNode) => {
100
101
  return Component;
101
102
  };
102
103
 
104
+ // node_modules/lucide-react/dist/esm/icons/archive.js
105
+ var Archive = createLucideIcon("Archive", [
106
+ ["rect", { width: "20", height: "5", x: "2", y: "3", rx: "1", key: "1wp1u1" }],
107
+ ["path", { d: "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8", key: "1s80jp" }],
108
+ ["path", { d: "M10 12h4", key: "a56b0p" }]
109
+ ]);
110
+
111
+ // node_modules/lucide-react/dist/esm/icons/ellipsis.js
112
+ var Ellipsis = createLucideIcon("Ellipsis", [
113
+ ["circle", { cx: "12", cy: "12", r: "1", key: "41hilf" }],
114
+ ["circle", { cx: "19", cy: "12", r: "1", key: "1wjl8i" }],
115
+ ["circle", { cx: "5", cy: "12", r: "1", key: "1pcz8c" }]
116
+ ]);
117
+
118
+ // node_modules/lucide-react/dist/esm/icons/git-branch.js
119
+ var GitBranch = createLucideIcon("GitBranch", [
120
+ ["line", { x1: "6", x2: "6", y1: "3", y2: "15", key: "17qcm7" }],
121
+ ["circle", { cx: "18", cy: "6", r: "3", key: "1h7g24" }],
122
+ ["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
123
+ ["path", { d: "M18 9a9 9 0 0 1-9 9", key: "n2h4wq" }]
124
+ ]);
125
+
126
+ // node_modules/lucide-react/dist/esm/icons/pencil.js
127
+ var Pencil = createLucideIcon("Pencil", [
128
+ [
129
+ "path",
130
+ {
131
+ d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",
132
+ key: "1a8usu"
133
+ }
134
+ ],
135
+ ["path", { d: "m15 5 4 4", key: "1mk7zo" }]
136
+ ]);
137
+
138
+ // node_modules/lucide-react/dist/esm/icons/pin-off.js
139
+ var PinOff = createLucideIcon("PinOff", [
140
+ ["path", { d: "M12 17v5", key: "bb1du9" }],
141
+ ["path", { d: "M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89", key: "znwnzq" }],
142
+ ["path", { d: "m2 2 20 20", key: "1ooewy" }],
143
+ [
144
+ "path",
145
+ {
146
+ d: "M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11",
147
+ key: "c9qhm2"
148
+ }
149
+ ]
150
+ ]);
151
+
103
152
  // node_modules/lucide-react/dist/esm/icons/pin.js
104
153
  var Pin = createLucideIcon("Pin", [
105
154
  ["path", { d: "M12 17v5", key: "bb1du9" }],
@@ -112,10 +161,13 @@ var Pin = createLucideIcon("Pin", [
112
161
  ]
113
162
  ]);
114
163
 
115
- // node_modules/lucide-react/dist/esm/icons/x.js
116
- var X = createLucideIcon("X", [
117
- ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
118
- ["path", { d: "m6 6 12 12", key: "d8bk6v" }]
164
+ // node_modules/lucide-react/dist/esm/icons/trash-2.js
165
+ var Trash2 = createLucideIcon("Trash2", [
166
+ ["path", { d: "M3 6h18", key: "d0wm0j" }],
167
+ ["path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6", key: "4alrt4" }],
168
+ ["path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2", key: "v07s0e" }],
169
+ ["line", { x1: "10", x2: "10", y1: "11", y2: "17", key: "1uufr5" }],
170
+ ["line", { x1: "14", x2: "14", y1: "11", y2: "17", key: "xtxkd" }]
119
171
  ]);
120
172
 
121
173
  // src/client/index.tsx
@@ -135,7 +187,7 @@ var defaultAttributes2 = {
135
187
  };
136
188
 
137
189
  // node_modules/lucide/dist/esm/icons/pin-off.js
138
- var PinOff = [
190
+ var PinOff2 = [
139
191
  "svg",
140
192
  defaultAttributes2,
141
193
  [
@@ -271,7 +323,7 @@ function updateSessionMenuItem(target, pinned, label) {
271
323
  target.label.textContent = label;
272
324
  target.icon.replaceChildren(createLucideIcon2(
273
325
  target.menu.ownerDocument,
274
- pinned ? PinOff : Pin2,
326
+ pinned ? PinOff2 : Pin2,
275
327
  pinned ? "pin-off" : "pin"
276
328
  ));
277
329
  }
@@ -429,23 +481,69 @@ function sessionDisplayTitle(session, workspaceTitle) {
429
481
 
430
482
  // src/client/index.tsx
431
483
  var import_jsx_runtime = require("react/jsx-runtime");
484
+ var BooleanStore = class {
485
+ value = false;
486
+ listeners = /* @__PURE__ */ new Set();
487
+ getSnapshot = () => this.value;
488
+ subscribe = (listener) => {
489
+ this.listeners.add(listener);
490
+ return () => {
491
+ this.listeners.delete(listener);
492
+ };
493
+ };
494
+ set(value) {
495
+ if (value === this.value) return;
496
+ this.value = value;
497
+ for (const listener of this.listeners) listener();
498
+ }
499
+ };
432
500
  var NS = "pinnedSessions";
433
501
  var STYLE_ID = "@anionex/dsh-pinned-sessions";
434
502
  var MENU_ASSOCIATION_MS = 2e3;
435
503
  var zh = {
436
- "menu.pin": "\u7F6E\u9876\u4F1A\u8BDD",
437
- "menu.unpin": "\u53D6\u6D88\u7F6E\u9876",
504
+ "menu.rename": "\u91CD\u547D\u540D",
505
+ "menu.fork": "\u5206\u53C9\u4F1A\u8BDD",
506
+ "menu.pin": "\u7F6E\u9876\u5230\u9876\u90E8\u5206\u7EC4",
507
+ "menu.unpin": "\u4ECE\u9876\u90E8\u5206\u7EC4\u53D6\u6D88\u7F6E\u9876",
508
+ "menu.archive": "\u5F52\u6863\u4F1A\u8BDD",
509
+ "menu.delete": "\u5220\u9664\u4F1A\u8BDD",
438
510
  "section.label": "\u7F6E\u9876\u4F1A\u8BDD",
439
511
  "session.open": "\u6253\u5F00\u7F6E\u9876\u4F1A\u8BDD\u201C{name}\u201D",
512
+ "session.actions": "\u7F6E\u9876\u4F1A\u8BDD\u201C{name}\u201D\u7684\u64CD\u4F5C",
513
+ "session.archiveFailed": "\u5F52\u6863\u4F1A\u8BDD\u5931\u8D25\uFF1A{detail}",
514
+ "session.forkFailed": "\u5206\u53C9\u4F1A\u8BDD\u5931\u8D25\uFF1A{detail}",
440
515
  "session.unpin": "\u53D6\u6D88\u7F6E\u9876\u201C{name}\u201D",
516
+ "dialog.close": "\u5173\u95ED",
517
+ "dialog.cancel": "\u53D6\u6D88",
518
+ "rename.title": "\u91CD\u547D\u540D\u4F1A\u8BDD",
519
+ "rename.field": "\u4F1A\u8BDD\u540D\u79F0",
520
+ "rename.confirm": "\u91CD\u547D\u540D",
521
+ "delete.title": "\u5220\u9664\u4F1A\u8BDD",
522
+ "delete.description": "\u5C06\u6C38\u4E45\u5220\u9664\u4F1A\u8BDD\u201C{name}\u201D\u53CA\u5176\u5B50\u4EE3\u7406\uFF08\u542B\u6B63\u5728\u8FD0\u884C\u7684\uFF09\u548C\u5168\u90E8\u8BB0\u5F55\uFF08\u5BF9\u8BDD\u5185\u5BB9\u3001\u7EDF\u8BA1\u3001\u7F13\u5B58\uFF09\uFF0C\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
523
+ "delete.pending": "\u6B63\u5728\u5220\u9664\u4F1A\u8BDD\u2026",
441
524
  "workspace.ungrouped": "\u672A\u5206\u7EC4"
442
525
  };
443
526
  var en = {
444
- "menu.pin": "Pin session",
445
- "menu.unpin": "Unpin session",
527
+ "menu.rename": "Rename",
528
+ "menu.fork": "Fork session",
529
+ "menu.pin": "Pin to top section",
530
+ "menu.unpin": "Unpin from top section",
531
+ "menu.archive": "Archive session",
532
+ "menu.delete": "Delete session",
446
533
  "section.label": "Pinned sessions",
447
534
  "session.open": "Open pinned session \u201C{name}\u201D",
535
+ "session.actions": "Actions for pinned session \u201C{name}\u201D",
536
+ "session.archiveFailed": "Could not archive session: {detail}",
537
+ "session.forkFailed": "Could not fork session: {detail}",
448
538
  "session.unpin": "Unpin \u201C{name}\u201D",
539
+ "dialog.close": "Close",
540
+ "dialog.cancel": "Cancel",
541
+ "rename.title": "Rename session",
542
+ "rename.field": "Session name",
543
+ "rename.confirm": "Rename",
544
+ "delete.title": "Delete session",
545
+ "delete.description": "This permanently deletes session \u201C{name}\u201D, its child agents (including any that are still running), and all of its records (conversation, stats, cache). This cannot be undone.",
546
+ "delete.pending": "Deleting session\u2026",
449
547
  "workspace.ungrouped": "Ungrouped"
450
548
  };
451
549
  var styles = `
@@ -455,19 +553,58 @@ var styles = `
455
553
  .dsh-pins-header svg{flex:none;color:var(--dsw-alias-state-business-primary)}
456
554
  .dsh-pins-list{box-sizing:border-box;max-height:192px;min-width:0;margin:0;padding:0;overflow-y:auto;overscroll-behavior:contain;list-style:none}
457
555
  .dsh-pins-row{box-sizing:border-box;display:flex;align-items:center;gap:0;height:32px;min-width:0;border-radius:6px;color:var(--dsw-alias-label-primary)}
458
- .dsh-pins-row:hover,.dsh-pins-row[data-current="true"]{background:var(--dsw-alias-interactive-bg-hover)}
556
+ .dsh-pins-row:hover,.dsh-pins-row[data-current="true"],.dsh-pins-row[data-menu-open="true"]{background:var(--dsw-alias-interactive-bg-hover)}
459
557
  .dsh-pins-open{box-sizing:border-box;display:flex;align-items:center;gap:6px;min-width:0;height:32px;padding:0 4px 0 8px;border:0;background:transparent;color:inherit;cursor:pointer;flex:1;text-align:left}
460
- .dsh-pins-open:focus-visible,.dsh-pins-remove:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
558
+ .dsh-pins-open:focus-visible,.dsh-pins-actions:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
461
559
  .dsh-pins-open>svg{flex:none;color:var(--dsw-alias-state-business-primary)}
462
560
  .dsh-pins-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;flex:1}
463
561
  .dsh-pins-workspace{max-width:34%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;flex:none}
464
- .dsh-pins-remove{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;margin-right:3px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;flex:none}
465
- .dsh-pins-row:hover .dsh-pins-remove,.dsh-pins-remove:focus-visible{opacity:1}
466
- .dsh-pins-remove:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
467
- .dsh-pins-menu-button{letter-spacing:0}
468
- @media (max-width:767px){.dsh-pins-workspace{display:none}.dsh-pins-remove{opacity:1}}
562
+ .dsh-pins-actions{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;margin-right:3px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;pointer-events:none;flex:none}
563
+ .dsh-pins-row:hover .dsh-pins-actions,.dsh-pins-row:focus-within .dsh-pins-actions,.dsh-pins-row[data-menu-open="true"] .dsh-pins-actions{opacity:1;pointer-events:auto}
564
+ .dsh-pins-actions:hover,.dsh-pins-row[data-menu-open="true"] .dsh-pins-actions{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
565
+ .dsh-pins-action-menu{display:inline-flex;flex:none}
566
+ .dsh-pins-rename-input{box-sizing:border-box;width:100%;height:36px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:0;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit}
567
+ .dsh-pins-rename-input:focus{border-color:var(--dsw-alias-state-business-primary)}
568
+ .dsh-pins-dialog-error{margin-top:8px;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
569
+ .dsh-pins-dialog-status{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
570
+ .dsh-pins-delete-action{color:var(--dsw-alias-state-error-primary)!important;border-color:var(--dsw-alias-state-error-primary)!important}
571
+ @media (max-width:767px){.dsh-pins-workspace{display:none}}
572
+ @media (hover:none),(max-width:767px){.dsh-pins-actions{opacity:1;pointer-events:auto}}
469
573
  `;
470
- var inject = ["slots", "sessions", "locale"];
574
+ var inject = ["slots", "sessions", "workspaces", "locale"];
575
+ function deleteSessionService(ctx) {
576
+ try {
577
+ const service = ctx.get("remote.workspaceRegistry");
578
+ return typeof service?.deleteSession === "function" ? service : null;
579
+ } catch {
580
+ return null;
581
+ }
582
+ }
583
+ function makePinnedSessionActions(ctx, deleteAvailability) {
584
+ const workspaces = ctx.get("workspaces");
585
+ return {
586
+ renameSession: async (sessionId, title) => {
587
+ const session = ctx.sessions.binding(sessionId)?.session;
588
+ if (session === void 0) throw new Error(`unknown session "${sessionId}"`);
589
+ const result = await session.rename(title);
590
+ if (!result.ok) throw new Error(result.error?.message ?? "Session rename failed");
591
+ },
592
+ forkSession: async (sessionId) => {
593
+ const childId = await ctx.sessions.fork({ sessionId, increaseTitle: true });
594
+ ctx.sessions.open(childId);
595
+ },
596
+ archiveSession: async (sessionId) => {
597
+ await workspaces.archiveSession(sessionId);
598
+ },
599
+ deleteAvailability,
600
+ deleteSession: async (sessionId) => {
601
+ const service = deleteSessionService(ctx);
602
+ if (service === null) throw new Error("Session deletion is unavailable");
603
+ const result = await service.deleteSession(sessionId);
604
+ if (!result.ok) throw new Error(result.error?.message ?? "Session deletion failed");
605
+ }
606
+ };
607
+ }
471
608
  function apply(ctx) {
472
609
  let storage;
473
610
  try {
@@ -476,6 +613,53 @@ function apply(ctx) {
476
613
  storage = void 0;
477
614
  }
478
615
  const store = new PinStore(storage);
616
+ const deleteAvailability = new BooleanStore();
617
+ const actions = makePinnedSessionActions(ctx, deleteAvailability);
618
+ const modernMenus = new BooleanStore();
619
+ let openSession = (sessionId) => {
620
+ ctx.sessions.open(sessionId);
621
+ };
622
+ const navigationActions = {
623
+ ...actions,
624
+ openSession: (id) => {
625
+ openSession(id);
626
+ },
627
+ forkSession: async (id) => {
628
+ openSession(await ctx.sessions.fork({ sessionId: id, increaseTitle: true }));
629
+ }
630
+ };
631
+ ctx.inject(["uiWorkspace"], (navigation) => {
632
+ const service = navigation.get("uiWorkspace");
633
+ openSession = (id) => {
634
+ service.openSession(id);
635
+ };
636
+ navigation.effect(() => () => {
637
+ openSession = (id) => {
638
+ ctx.sessions.open(id);
639
+ };
640
+ });
641
+ });
642
+ ctx.slots.inject("sidebar.workspaces.session.menu.item", () => {
643
+ modernMenus.set(true);
644
+ const dispose = ctx.slots.register({
645
+ name: "sidebar.workspaces.session.menu.item",
646
+ id: "pinned-sessions",
647
+ order: 90,
648
+ locale: NS,
649
+ inject: () => ({ store })
650
+ }, PinTopSectionMenuItem);
651
+ return () => {
652
+ dispose();
653
+ modernMenus.set(false);
654
+ };
655
+ });
656
+ ctx.inject(["remote.workspaceRegistry"], (serviceCtx) => {
657
+ const available = deleteSessionService(serviceCtx) !== null;
658
+ deleteAvailability.set(available);
659
+ serviceCtx.effect(() => () => {
660
+ deleteAvailability.set(false);
661
+ }, "pinned-sessions: delete capability");
662
+ });
479
663
  ctx.effect(() => {
480
664
  const onStorage = (event) => {
481
665
  if (event.key === PIN_STORAGE_KEY || event.key === null) store.reload();
@@ -503,10 +687,19 @@ function apply(ctx) {
503
687
  id: "pinned-sessions-bridge",
504
688
  order: 40,
505
689
  locale: NS,
506
- inject: () => ({ store, sessions: ctx.sessions })
690
+ inject: () => ({ store, sessions: ctx.sessions, actions: navigationActions, modernMenus })
507
691
  }, PinnedSessionsBridge));
508
692
  }
509
- function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }) {
693
+ function PinTopSectionMenuItem({ sessionId, store, useMenuOpenState, t }) {
694
+ (0, import_react3.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
695
+ const [, setMenuOpen] = useMenuOpenState();
696
+ const pinned = store.isPinned(sessionId);
697
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.MenuItemButton, { icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinIcon, {}), onSelect: () => {
698
+ store.toggle(sessionId);
699
+ setMenuOpen(false);
700
+ }, children: t(pinned ? "menu.unpin" : "menu.pin") });
701
+ }
702
+ function PinnedSessionsBridge({ store, sessions, actions, useSessions, useWorkspaces, t, modernMenus }) {
510
703
  const sessionSnapshot = useSessions((snapshot) => snapshot);
511
704
  const workspaceSnapshot = useWorkspaces((snapshot) => snapshot);
512
705
  const pins = (0, import_react3.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
@@ -514,7 +707,7 @@ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }
514
707
  const [sidebarWide, setSidebarWide] = (0, import_react3.useState)(true);
515
708
  const pending = (0, import_react3.useRef)(null);
516
709
  const activeMenu = (0, import_react3.useRef)(null);
517
- const focusAfterUnpin = (0, import_react3.useRef)(null);
710
+ const focusAfterRemoval = (0, import_react3.useRef)(null);
518
711
  (0, import_react3.useLayoutEffect)(() => {
519
712
  let active = true;
520
713
  let queued = false;
@@ -603,6 +796,7 @@ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }
603
796
  }
604
797
  const onClickCapture = (event) => {
605
798
  pending.current = null;
799
+ if (modernMenus?.getSnapshot()) return;
606
800
  const target = findSessionActionTarget(event.target);
607
801
  if (target === null) return;
608
802
  const sessionId = captureSessionId(target.row, sessions);
@@ -646,7 +840,7 @@ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }
646
840
  disposeActiveMenu();
647
841
  removeBridgeArtifacts(document);
648
842
  };
649
- }, [sessions, store, t]);
843
+ }, [sessions, store, t, modernMenus]);
650
844
  (0, import_react3.useLayoutEffect)(() => {
651
845
  if (sidebarHost === null) return;
652
846
  const root = sidebarHost.parentElement;
@@ -684,22 +878,30 @@ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }
684
878
  });
685
879
  }, [pins, sessionSnapshot.byId, t, workspaceSnapshot.items]);
686
880
  (0, import_react3.useLayoutEffect)(() => {
687
- const removedIndex = focusAfterUnpin.current;
688
- if (removedIndex === null) return;
689
- focusAfterUnpin.current = null;
690
- focusAfterPinnedRemoval(document, sidebarHost, removedIndex);
881
+ const pendingRemoval = focusAfterRemoval.current;
882
+ if (pendingRemoval === null || rows.some((row) => row.session.id === pendingRemoval.sessionId)) return;
883
+ focusAfterRemoval.current = null;
884
+ focusAfterPinnedRemoval(document, sidebarHost, pendingRemoval.index);
691
885
  }, [rows, sidebarHost]);
692
886
  return sidebarHost !== null && sidebarHost.isConnected && sidebarWide && rows.length > 0 ? (0, import_react_dom.createPortal)(
693
887
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
694
888
  PinnedSessionSection,
695
889
  {
890
+ actions,
891
+ cancelRemoval: (sessionId) => {
892
+ if (focusAfterRemoval.current?.sessionId === sessionId) focusAfterRemoval.current = null;
893
+ },
696
894
  currentId: sessionSnapshot.current,
697
895
  rows,
698
896
  open: (sessionId) => {
699
- sessions.open(sessionId);
897
+ if (actions.openSession) actions.openSession(sessionId);
898
+ else sessions.open(sessionId);
899
+ },
900
+ prepareRemoval: (sessionId, index) => {
901
+ focusAfterRemoval.current = { sessionId, index };
700
902
  },
701
903
  unpin: (sessionId, index) => {
702
- focusAfterUnpin.current = index;
904
+ focusAfterRemoval.current = { sessionId, index };
703
905
  store.toggle(sessionId);
704
906
  },
705
907
  t
@@ -708,24 +910,213 @@ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }
708
910
  sidebarHost
709
911
  ) : null;
710
912
  }
711
- function PinnedSessionSection({ currentId, rows, open, unpin, t }) {
913
+ function PinnedSessionSection({ actions, cancelRemoval, currentId, rows, open, prepareRemoval, unpin, t }) {
712
914
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-pins-section", "aria-label": t("section.label"), children: [
713
915
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-pins-header", children: [
714
916
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinIcon, { size: 13 }),
715
917
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("section.label") })
716
918
  ] }),
717
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dsh-pins-list", children: rows.map(({ session, title, workspace }, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
919
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dsh-pins-list", children: rows.map(({ session, title, workspace }, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
920
+ PinnedSessionRow,
921
+ {
922
+ actions,
923
+ cancelRemoval,
924
+ current: session.id === currentId,
925
+ index,
926
+ open,
927
+ prepareRemoval,
928
+ session,
929
+ t,
930
+ title,
931
+ unpin,
932
+ workspace
933
+ },
934
+ session.id
935
+ )) })
936
+ ] });
937
+ }
938
+ function PinnedSessionRow({ actions, cancelRemoval, current, index, open, prepareRemoval, session, t, title, unpin, workspace }) {
939
+ const [menuOpen, setMenuOpen] = (0, import_react3.useState)(false);
940
+ const [renameOpen, setRenameOpen] = (0, import_react3.useState)(false);
941
+ const [renameDraft, setRenameDraft] = (0, import_react3.useState)("");
942
+ const [renaming, setRenaming] = (0, import_react3.useState)(false);
943
+ const [renameError, setRenameError] = (0, import_react3.useState)(null);
944
+ const [deleteOpen, setDeleteOpen] = (0, import_react3.useState)(false);
945
+ const [deleting, setDeleting] = (0, import_react3.useState)(false);
946
+ const [deleteError, setDeleteError] = (0, import_react3.useState)(null);
947
+ const [actionToast, setActionToast] = (0, import_react3.useState)(null);
948
+ const toastSeq = (0, import_react3.useRef)(0);
949
+ const composing = (0, import_react3.useRef)(false);
950
+ const trigger = (0, import_react3.useRef)(null);
951
+ const menusBeforeOpen = (0, import_react3.useRef)(/* @__PURE__ */ new Set());
952
+ const focusMenuEdge = (0, import_react3.useRef)(null);
953
+ const actionLabel = t("session.actions", { name: title });
954
+ const renameTrimmed = renameDraft.trim();
955
+ const renameBlocked = renaming || renameTrimmed === "";
956
+ const canDelete = (0, import_react3.useSyncExternalStore)(
957
+ actions.deleteAvailability.subscribe,
958
+ actions.deleteAvailability.getSnapshot,
959
+ actions.deleteAvailability.getSnapshot
960
+ );
961
+ const showDesktopActionError = (key, reason) => {
962
+ if (!canDelete) return;
963
+ toastSeq.current += 1;
964
+ setActionToast({
965
+ text: t(key, { detail: reason instanceof Error ? reason.message : String(reason) }),
966
+ seq: toastSeq.current
967
+ });
968
+ };
969
+ const restoreTriggerFocus = () => {
970
+ queueMicrotask(() => {
971
+ trigger.current?.focus({ preventScroll: true });
972
+ });
973
+ };
974
+ const openActionMenu = (focusEdge) => {
975
+ menusBeforeOpen.current = new Set(document.querySelectorAll('[role="menu"]'));
976
+ focusMenuEdge.current = focusEdge;
977
+ setMenuOpen(true);
978
+ };
979
+ (0, import_react3.useLayoutEffect)(() => {
980
+ if (!menuOpen) return;
981
+ let menu;
982
+ let observer;
983
+ let disposed = false;
984
+ const items = () => menu === void 0 ? [] : [...menu.querySelectorAll('[role="menuitem"]:not(:disabled)')];
985
+ const onMenuKeyDown = (event) => {
986
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
987
+ const available = items();
988
+ if (available.length === 0) return;
989
+ const currentIndex = available.indexOf(document.activeElement);
990
+ let nextIndex;
991
+ if (event.key === "Home") nextIndex = 0;
992
+ else if (event.key === "End") nextIndex = available.length - 1;
993
+ else if (event.key === "ArrowDown") nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % available.length;
994
+ else nextIndex = currentIndex <= 0 ? available.length - 1 : currentIndex - 1;
995
+ event.preventDefault();
996
+ available[nextIndex]?.focus({ preventScroll: true });
997
+ };
998
+ const onDocumentKeyDown = (event) => {
999
+ if (event.key === "Escape") restoreTriggerFocus();
1000
+ };
1001
+ const install = () => {
1002
+ if (disposed) return;
1003
+ menu = [...document.querySelectorAll('[role="menu"]')].find((candidate) => !menusBeforeOpen.current.has(candidate));
1004
+ if (menu === void 0) return;
1005
+ observer?.disconnect();
1006
+ menu.addEventListener("keydown", onMenuKeyDown);
1007
+ const edge = focusMenuEdge.current;
1008
+ focusMenuEdge.current = null;
1009
+ if (edge === null) return;
1010
+ const available = items();
1011
+ const target = edge === "first" ? available[0] : available.at(-1);
1012
+ queueMicrotask(() => {
1013
+ if (!disposed && target?.isConnected === true) target.focus({ preventScroll: true });
1014
+ });
1015
+ };
1016
+ document.addEventListener("keydown", onDocumentKeyDown, true);
1017
+ install();
1018
+ if (menu === void 0) {
1019
+ observer = new MutationObserver(install);
1020
+ observer.observe(document.body, { childList: true, subtree: true });
1021
+ install();
1022
+ }
1023
+ return () => {
1024
+ disposed = true;
1025
+ observer?.disconnect();
1026
+ menu?.removeEventListener("keydown", onMenuKeyDown);
1027
+ document.removeEventListener("keydown", onDocumentKeyDown, true);
1028
+ };
1029
+ }, [menuOpen]);
1030
+ const menuItems = [
1031
+ { id: "rename", label: t("menu.rename"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pencil, { size: 16 }) },
1032
+ { id: "fork", label: t("menu.fork"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GitBranch, { size: 16 }) },
1033
+ { id: "unpin", label: t("menu.unpin"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinOff, { size: 16, strokeWidth: 1.8, "aria-hidden": "true" }) },
1034
+ { id: "archive", label: t("menu.archive"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Archive, { size: 16 }) },
1035
+ ...canDelete ? [{ id: "delete", label: t("menu.delete"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2, { size: 16 }), danger: true }] : []
1036
+ ];
1037
+ const closeRename = () => {
1038
+ if (renaming) return;
1039
+ setRenameOpen(false);
1040
+ setRenameError(null);
1041
+ restoreTriggerFocus();
1042
+ };
1043
+ const confirmRename = () => {
1044
+ if (renameBlocked) return;
1045
+ setRenaming(true);
1046
+ setRenameError(null);
1047
+ void actions.renameSession(session.id, renameTrimmed).then(() => {
1048
+ setRenaming(false);
1049
+ setRenameOpen(false);
1050
+ restoreTriggerFocus();
1051
+ }).catch((reason) => {
1052
+ setRenaming(false);
1053
+ setRenameError(reason instanceof Error ? reason.message : String(reason));
1054
+ });
1055
+ };
1056
+ const closeDelete = () => {
1057
+ if (deleting) return;
1058
+ setDeleteOpen(false);
1059
+ setDeleteError(null);
1060
+ restoreTriggerFocus();
1061
+ };
1062
+ const confirmDelete = () => {
1063
+ if (deleting) return;
1064
+ prepareRemoval(session.id, index);
1065
+ setDeleting(true);
1066
+ setDeleteError(null);
1067
+ void actions.deleteSession(session.id).then(() => {
1068
+ setDeleting(false);
1069
+ setDeleteOpen(false);
1070
+ restoreTriggerFocus();
1071
+ }).catch((reason) => {
1072
+ cancelRemoval(session.id);
1073
+ setDeleting(false);
1074
+ setDeleteError(reason instanceof Error ? reason.message : String(reason));
1075
+ });
1076
+ };
1077
+ const onSelect = (id) => {
1078
+ setMenuOpen(false);
1079
+ if (id === "rename") {
1080
+ setRenameDraft(session.title ?? title);
1081
+ setRenameError(null);
1082
+ setRenameOpen(true);
1083
+ }
1084
+ if (id === "fork") {
1085
+ restoreTriggerFocus();
1086
+ void actions.forkSession(session.id).catch((reason) => {
1087
+ showDesktopActionError("session.forkFailed", reason);
1088
+ });
1089
+ }
1090
+ if (id === "unpin") unpin(session.id, index);
1091
+ if (id === "archive") {
1092
+ prepareRemoval(session.id, index);
1093
+ restoreTriggerFocus();
1094
+ void actions.archiveSession(session.id).catch((reason) => {
1095
+ cancelRemoval(session.id);
1096
+ restoreTriggerFocus();
1097
+ showDesktopActionError("session.archiveFailed", reason);
1098
+ if (!canDelete) console.warn("session archive rejected:", reason);
1099
+ });
1100
+ }
1101
+ if (id === "delete" && canDelete) {
1102
+ setDeleteError(null);
1103
+ setDeleteOpen(true);
1104
+ }
1105
+ };
1106
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1107
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
718
1108
  "li",
719
1109
  {
720
1110
  className: "dsh-pins-row",
721
- "data-current": session.id === currentId ? "true" : "false",
1111
+ "data-current": current ? "true" : "false",
1112
+ "data-menu-open": menuOpen ? "true" : void 0,
722
1113
  children: [
723
1114
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
724
1115
  "button",
725
1116
  {
726
1117
  type: "button",
727
1118
  className: "dsh-pins-open",
728
- "aria-current": session.id === currentId ? "page" : void 0,
1119
+ "aria-current": current ? "page" : void 0,
729
1120
  "aria-label": t("session.open", { name: title }),
730
1121
  onClick: () => {
731
1122
  open(session.id);
@@ -738,38 +1129,135 @@ function PinnedSessionSection({ currentId, rows, open, unpin, t }) {
738
1129
  }
739
1130
  ),
740
1131
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
741
- "button",
1132
+ import_dsh_client_ui_primitives.Menu,
742
1133
  {
743
- type: "button",
744
- className: "dsh-pins-remove",
745
- "aria-label": t("session.unpin", { name: title }),
746
- title: t("session.unpin", { name: title }),
747
- onClick: () => {
748
- unpin(session.id, index);
1134
+ className: "dsh-pins-action-menu",
1135
+ open: menuOpen,
1136
+ onClose: () => {
1137
+ setMenuOpen(false);
749
1138
  },
750
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
1139
+ items: menuItems,
1140
+ onSelect,
1141
+ portal: true,
1142
+ closeOnPointerLeave: true,
1143
+ anchor: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1144
+ "button",
1145
+ {
1146
+ ref: trigger,
1147
+ type: "button",
1148
+ className: "dsh-pins-actions",
1149
+ "aria-expanded": menuOpen,
1150
+ "aria-haspopup": "menu",
1151
+ "aria-label": actionLabel,
1152
+ title: actionLabel,
1153
+ onClick: (event) => {
1154
+ event.stopPropagation();
1155
+ if (menuOpen) setMenuOpen(false);
1156
+ else openActionMenu(null);
1157
+ },
1158
+ onKeyDown: (event) => {
1159
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1160
+ event.preventDefault();
1161
+ openActionMenu(event.key === "ArrowDown" ? "first" : "last");
1162
+ },
1163
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Ellipsis, { size: 16 })
1164
+ }
1165
+ )
751
1166
  }
752
1167
  )
753
1168
  ]
1169
+ }
1170
+ ),
1171
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1172
+ import_dsh_client_ui_primitives.Modal,
1173
+ {
1174
+ open: renameOpen,
1175
+ onClose: closeRename,
1176
+ closeLabel: t("dialog.close"),
1177
+ title: t("rename.title"),
1178
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1179
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", disabled: renaming, onClick: closeRename, children: t("dialog.cancel") }),
1180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: renameBlocked, onClick: confirmRename, children: t("rename.confirm") })
1181
+ ] }),
1182
+ children: [
1183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1184
+ "input",
1185
+ {
1186
+ className: "dsh-pins-rename-input",
1187
+ value: renameDraft,
1188
+ "aria-label": t("rename.field"),
1189
+ autoFocus: true,
1190
+ disabled: renaming,
1191
+ onFocus: (event) => {
1192
+ event.currentTarget.select();
1193
+ },
1194
+ onChange: (event) => {
1195
+ setRenameDraft(event.currentTarget.value);
1196
+ setRenameError(null);
1197
+ },
1198
+ onCompositionStart: () => {
1199
+ composing.current = true;
1200
+ },
1201
+ onCompositionEnd: () => {
1202
+ composing.current = false;
1203
+ },
1204
+ onKeyDown: (event) => {
1205
+ if (event.key !== "Enter" || composing.current) return;
1206
+ event.preventDefault();
1207
+ confirmRename();
1208
+ }
1209
+ }
1210
+ ),
1211
+ renameError !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pins-dialog-error", role: "alert", children: renameError })
1212
+ ]
1213
+ }
1214
+ ),
1215
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1216
+ import_dsh_client_ui_primitives.Modal,
1217
+ {
1218
+ open: deleteOpen,
1219
+ onClose: closeDelete,
1220
+ closeLabel: t("dialog.close"),
1221
+ title: t("delete.title"),
1222
+ description: t("delete.description", { name: title }),
1223
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1224
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", autoFocus: true, disabled: deleting, onClick: closeDelete, children: t("dialog.cancel") }),
1225
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", className: "dsh-pins-delete-action", disabled: deleting, onClick: confirmDelete, children: t("delete.title") })
1226
+ ] }),
1227
+ children: [
1228
+ deleting && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pins-dialog-status", role: "status", children: t("delete.pending") }),
1229
+ deleteError !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pins-dialog-error", role: "alert", children: deleteError })
1230
+ ]
1231
+ }
1232
+ ),
1233
+ actionToast !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1234
+ import_dsh_client_ui_primitives.Toast,
1235
+ {
1236
+ text: actionToast.text,
1237
+ onDone: () => {
1238
+ setActionToast(null);
1239
+ }
754
1240
  },
755
- session.id
756
- )) })
1241
+ actionToast.seq
1242
+ )
757
1243
  ] });
758
1244
  }
759
1245
  function PinIcon({ size = 16 }) {
760
1246
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pin, { size, strokeWidth: 1.8, "aria-hidden": "true" });
761
1247
  }
762
- function CloseIcon() {
763
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, { size: 14, strokeWidth: 2, "aria-hidden": "true" });
764
- }
765
1248
  /*! Bundled license information:
766
1249
 
767
1250
  lucide-react/dist/esm/shared/src/utils.js:
768
1251
  lucide-react/dist/esm/defaultAttributes.js:
769
1252
  lucide-react/dist/esm/Icon.js:
770
1253
  lucide-react/dist/esm/createLucideIcon.js:
1254
+ lucide-react/dist/esm/icons/archive.js:
1255
+ lucide-react/dist/esm/icons/ellipsis.js:
1256
+ lucide-react/dist/esm/icons/git-branch.js:
1257
+ lucide-react/dist/esm/icons/pencil.js:
1258
+ lucide-react/dist/esm/icons/pin-off.js:
771
1259
  lucide-react/dist/esm/icons/pin.js:
772
- lucide-react/dist/esm/icons/x.js:
1260
+ lucide-react/dist/esm/icons/trash-2.js:
773
1261
  lucide-react/dist/esm/lucide-react.js:
774
1262
  (**
775
1263
  * @license lucide-react v0.468.0 - ISC