@anionex/dsh-pinned-sessions 0.1.0 → 0.1.1

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,20 @@ var createLucideIcon = (iconName, iconNode) => {
100
101
  return Component;
101
102
  };
102
103
 
104
+ // node_modules/lucide-react/dist/esm/icons/pin-off.js
105
+ var PinOff = createLucideIcon("PinOff", [
106
+ ["path", { d: "M12 17v5", key: "bb1du9" }],
107
+ ["path", { d: "M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89", key: "znwnzq" }],
108
+ ["path", { d: "m2 2 20 20", key: "1ooewy" }],
109
+ [
110
+ "path",
111
+ {
112
+ 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",
113
+ key: "c9qhm2"
114
+ }
115
+ ]
116
+ ]);
117
+
103
118
  // node_modules/lucide-react/dist/esm/icons/pin.js
104
119
  var Pin = createLucideIcon("Pin", [
105
120
  ["path", { d: "M12 17v5", key: "bb1du9" }],
@@ -112,12 +127,6 @@ var Pin = createLucideIcon("Pin", [
112
127
  ]
113
128
  ]);
114
129
 
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" }]
119
- ]);
120
-
121
130
  // src/client/index.tsx
122
131
  var import_react_dom = require("react-dom");
123
132
 
@@ -135,7 +144,7 @@ var defaultAttributes2 = {
135
144
  };
136
145
 
137
146
  // node_modules/lucide/dist/esm/icons/pin-off.js
138
- var PinOff = [
147
+ var PinOff2 = [
139
148
  "svg",
140
149
  defaultAttributes2,
141
150
  [
@@ -271,7 +280,7 @@ function updateSessionMenuItem(target, pinned, label) {
271
280
  target.label.textContent = label;
272
281
  target.icon.replaceChildren(createLucideIcon2(
273
282
  target.menu.ownerDocument,
274
- pinned ? PinOff : Pin2,
283
+ pinned ? PinOff2 : Pin2,
275
284
  pinned ? "pin-off" : "pin"
276
285
  ));
277
286
  }
@@ -429,23 +438,69 @@ function sessionDisplayTitle(session, workspaceTitle) {
429
438
 
430
439
  // src/client/index.tsx
431
440
  var import_jsx_runtime = require("react/jsx-runtime");
441
+ var BooleanStore = class {
442
+ value = false;
443
+ listeners = /* @__PURE__ */ new Set();
444
+ getSnapshot = () => this.value;
445
+ subscribe = (listener) => {
446
+ this.listeners.add(listener);
447
+ return () => {
448
+ this.listeners.delete(listener);
449
+ };
450
+ };
451
+ set(value) {
452
+ if (value === this.value) return;
453
+ this.value = value;
454
+ for (const listener of this.listeners) listener();
455
+ }
456
+ };
432
457
  var NS = "pinnedSessions";
433
458
  var STYLE_ID = "@anionex/dsh-pinned-sessions";
434
459
  var MENU_ASSOCIATION_MS = 2e3;
435
460
  var zh = {
461
+ "menu.rename": "\u91CD\u547D\u540D",
462
+ "menu.fork": "\u5206\u53C9\u4F1A\u8BDD",
436
463
  "menu.pin": "\u7F6E\u9876\u4F1A\u8BDD",
437
464
  "menu.unpin": "\u53D6\u6D88\u7F6E\u9876",
465
+ "menu.archive": "\u5F52\u6863\u4F1A\u8BDD",
466
+ "menu.delete": "\u5220\u9664\u4F1A\u8BDD",
438
467
  "section.label": "\u7F6E\u9876\u4F1A\u8BDD",
439
468
  "session.open": "\u6253\u5F00\u7F6E\u9876\u4F1A\u8BDD\u201C{name}\u201D",
469
+ "session.actions": "\u7F6E\u9876\u4F1A\u8BDD\u201C{name}\u201D\u7684\u64CD\u4F5C",
470
+ "session.archiveFailed": "\u5F52\u6863\u4F1A\u8BDD\u5931\u8D25\uFF1A{detail}",
471
+ "session.forkFailed": "\u5206\u53C9\u4F1A\u8BDD\u5931\u8D25\uFF1A{detail}",
440
472
  "session.unpin": "\u53D6\u6D88\u7F6E\u9876\u201C{name}\u201D",
473
+ "dialog.close": "\u5173\u95ED",
474
+ "dialog.cancel": "\u53D6\u6D88",
475
+ "rename.title": "\u91CD\u547D\u540D\u4F1A\u8BDD",
476
+ "rename.field": "\u4F1A\u8BDD\u540D\u79F0",
477
+ "rename.confirm": "\u91CD\u547D\u540D",
478
+ "delete.title": "\u5220\u9664\u4F1A\u8BDD",
479
+ "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",
480
+ "delete.pending": "\u6B63\u5728\u5220\u9664\u4F1A\u8BDD\u2026",
441
481
  "workspace.ungrouped": "\u672A\u5206\u7EC4"
442
482
  };
443
483
  var en = {
484
+ "menu.rename": "Rename",
485
+ "menu.fork": "Fork session",
444
486
  "menu.pin": "Pin session",
445
487
  "menu.unpin": "Unpin session",
488
+ "menu.archive": "Archive session",
489
+ "menu.delete": "Delete session",
446
490
  "section.label": "Pinned sessions",
447
491
  "session.open": "Open pinned session \u201C{name}\u201D",
492
+ "session.actions": "Actions for pinned session \u201C{name}\u201D",
493
+ "session.archiveFailed": "Could not archive session: {detail}",
494
+ "session.forkFailed": "Could not fork session: {detail}",
448
495
  "session.unpin": "Unpin \u201C{name}\u201D",
496
+ "dialog.close": "Close",
497
+ "dialog.cancel": "Cancel",
498
+ "rename.title": "Rename session",
499
+ "rename.field": "Session name",
500
+ "rename.confirm": "Rename",
501
+ "delete.title": "Delete session",
502
+ "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.",
503
+ "delete.pending": "Deleting session\u2026",
449
504
  "workspace.ungrouped": "Ungrouped"
450
505
  };
451
506
  var styles = `
@@ -455,19 +510,58 @@ var styles = `
455
510
  .dsh-pins-header svg{flex:none;color:var(--dsw-alias-state-business-primary)}
456
511
  .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
512
  .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)}
513
+ .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
514
  .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}
515
+ .dsh-pins-open:focus-visible,.dsh-pins-actions:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
461
516
  .dsh-pins-open>svg{flex:none;color:var(--dsw-alias-state-business-primary)}
462
517
  .dsh-pins-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;flex:1}
463
518
  .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}}
519
+ .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}
520
+ .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}
521
+ .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)}
522
+ .dsh-pins-action-menu{display:inline-flex;flex:none}
523
+ .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}
524
+ .dsh-pins-rename-input:focus{border-color:var(--dsw-alias-state-business-primary)}
525
+ .dsh-pins-dialog-error{margin-top:8px;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
526
+ .dsh-pins-dialog-status{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
527
+ .dsh-pins-delete-action{color:var(--dsw-alias-state-error-primary)!important;border-color:var(--dsw-alias-state-error-primary)!important}
528
+ @media (max-width:767px){.dsh-pins-workspace{display:none}}
529
+ @media (hover:none),(max-width:767px){.dsh-pins-actions{opacity:1;pointer-events:auto}}
469
530
  `;
470
- var inject = ["slots", "sessions", "locale"];
531
+ var inject = ["slots", "sessions", "workspaces", "locale"];
532
+ function deleteSessionService(ctx) {
533
+ try {
534
+ const service = ctx.get("remote.workspaceRegistry");
535
+ return typeof service?.deleteSession === "function" ? service : null;
536
+ } catch {
537
+ return null;
538
+ }
539
+ }
540
+ function makePinnedSessionActions(ctx, deleteAvailability) {
541
+ const workspaces = ctx.get("workspaces");
542
+ return {
543
+ renameSession: async (sessionId, title) => {
544
+ const session = ctx.sessions.binding(sessionId)?.session;
545
+ if (session === void 0) throw new Error(`unknown session "${sessionId}"`);
546
+ const result = await session.rename(title);
547
+ if (!result.ok) throw new Error(result.error?.message ?? "Session rename failed");
548
+ },
549
+ forkSession: async (sessionId) => {
550
+ const childId = await ctx.sessions.fork({ sessionId, increaseTitle: true });
551
+ ctx.sessions.open(childId);
552
+ },
553
+ archiveSession: async (sessionId) => {
554
+ await workspaces.archiveSession(sessionId);
555
+ },
556
+ deleteAvailability,
557
+ deleteSession: async (sessionId) => {
558
+ const service = deleteSessionService(ctx);
559
+ if (service === null) throw new Error("Session deletion is unavailable");
560
+ const result = await service.deleteSession(sessionId);
561
+ if (!result.ok) throw new Error(result.error?.message ?? "Session deletion failed");
562
+ }
563
+ };
564
+ }
471
565
  function apply(ctx) {
472
566
  let storage;
473
567
  try {
@@ -476,6 +570,15 @@ function apply(ctx) {
476
570
  storage = void 0;
477
571
  }
478
572
  const store = new PinStore(storage);
573
+ const deleteAvailability = new BooleanStore();
574
+ const actions = makePinnedSessionActions(ctx, deleteAvailability);
575
+ ctx.inject(["remote.workspaceRegistry"], (serviceCtx) => {
576
+ const available = deleteSessionService(serviceCtx) !== null;
577
+ deleteAvailability.set(available);
578
+ serviceCtx.effect(() => () => {
579
+ deleteAvailability.set(false);
580
+ }, "pinned-sessions: delete capability");
581
+ });
479
582
  ctx.effect(() => {
480
583
  const onStorage = (event) => {
481
584
  if (event.key === PIN_STORAGE_KEY || event.key === null) store.reload();
@@ -503,10 +606,10 @@ function apply(ctx) {
503
606
  id: "pinned-sessions-bridge",
504
607
  order: 40,
505
608
  locale: NS,
506
- inject: () => ({ store, sessions: ctx.sessions })
609
+ inject: () => ({ store, sessions: ctx.sessions, actions })
507
610
  }, PinnedSessionsBridge));
508
611
  }
509
- function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }) {
612
+ function PinnedSessionsBridge({ store, sessions, actions, useSessions, useWorkspaces, t }) {
510
613
  const sessionSnapshot = useSessions((snapshot) => snapshot);
511
614
  const workspaceSnapshot = useWorkspaces((snapshot) => snapshot);
512
615
  const pins = (0, import_react3.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
@@ -514,7 +617,7 @@ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }
514
617
  const [sidebarWide, setSidebarWide] = (0, import_react3.useState)(true);
515
618
  const pending = (0, import_react3.useRef)(null);
516
619
  const activeMenu = (0, import_react3.useRef)(null);
517
- const focusAfterUnpin = (0, import_react3.useRef)(null);
620
+ const focusAfterRemoval = (0, import_react3.useRef)(null);
518
621
  (0, import_react3.useLayoutEffect)(() => {
519
622
  let active = true;
520
623
  let queued = false;
@@ -684,22 +787,29 @@ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }
684
787
  });
685
788
  }, [pins, sessionSnapshot.byId, t, workspaceSnapshot.items]);
686
789
  (0, import_react3.useLayoutEffect)(() => {
687
- const removedIndex = focusAfterUnpin.current;
688
- if (removedIndex === null) return;
689
- focusAfterUnpin.current = null;
690
- focusAfterPinnedRemoval(document, sidebarHost, removedIndex);
790
+ const pendingRemoval = focusAfterRemoval.current;
791
+ if (pendingRemoval === null || rows.some((row) => row.session.id === pendingRemoval.sessionId)) return;
792
+ focusAfterRemoval.current = null;
793
+ focusAfterPinnedRemoval(document, sidebarHost, pendingRemoval.index);
691
794
  }, [rows, sidebarHost]);
692
795
  return sidebarHost !== null && sidebarHost.isConnected && sidebarWide && rows.length > 0 ? (0, import_react_dom.createPortal)(
693
796
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
694
797
  PinnedSessionSection,
695
798
  {
799
+ actions,
800
+ cancelRemoval: (sessionId) => {
801
+ if (focusAfterRemoval.current?.sessionId === sessionId) focusAfterRemoval.current = null;
802
+ },
696
803
  currentId: sessionSnapshot.current,
697
804
  rows,
698
805
  open: (sessionId) => {
699
806
  sessions.open(sessionId);
700
807
  },
808
+ prepareRemoval: (sessionId, index) => {
809
+ focusAfterRemoval.current = { sessionId, index };
810
+ },
701
811
  unpin: (sessionId, index) => {
702
- focusAfterUnpin.current = index;
812
+ focusAfterRemoval.current = { sessionId, index };
703
813
  store.toggle(sessionId);
704
814
  },
705
815
  t
@@ -708,24 +818,213 @@ function PinnedSessionsBridge({ store, sessions, useSessions, useWorkspaces, t }
708
818
  sidebarHost
709
819
  ) : null;
710
820
  }
711
- function PinnedSessionSection({ currentId, rows, open, unpin, t }) {
821
+ function PinnedSessionSection({ actions, cancelRemoval, currentId, rows, open, prepareRemoval, unpin, t }) {
712
822
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "dsh-pins-section", "aria-label": t("section.label"), children: [
713
823
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-pins-header", children: [
714
824
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinIcon, { size: 13 }),
715
825
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("section.label") })
716
826
  ] }),
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)(
827
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dsh-pins-list", children: rows.map(({ session, title, workspace }, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
828
+ PinnedSessionRow,
829
+ {
830
+ actions,
831
+ cancelRemoval,
832
+ current: session.id === currentId,
833
+ index,
834
+ open,
835
+ prepareRemoval,
836
+ session,
837
+ t,
838
+ title,
839
+ unpin,
840
+ workspace
841
+ },
842
+ session.id
843
+ )) })
844
+ ] });
845
+ }
846
+ function PinnedSessionRow({ actions, cancelRemoval, current, index, open, prepareRemoval, session, t, title, unpin, workspace }) {
847
+ const [menuOpen, setMenuOpen] = (0, import_react3.useState)(false);
848
+ const [renameOpen, setRenameOpen] = (0, import_react3.useState)(false);
849
+ const [renameDraft, setRenameDraft] = (0, import_react3.useState)("");
850
+ const [renaming, setRenaming] = (0, import_react3.useState)(false);
851
+ const [renameError, setRenameError] = (0, import_react3.useState)(null);
852
+ const [deleteOpen, setDeleteOpen] = (0, import_react3.useState)(false);
853
+ const [deleting, setDeleting] = (0, import_react3.useState)(false);
854
+ const [deleteError, setDeleteError] = (0, import_react3.useState)(null);
855
+ const [actionToast, setActionToast] = (0, import_react3.useState)(null);
856
+ const toastSeq = (0, import_react3.useRef)(0);
857
+ const composing = (0, import_react3.useRef)(false);
858
+ const trigger = (0, import_react3.useRef)(null);
859
+ const menusBeforeOpen = (0, import_react3.useRef)(/* @__PURE__ */ new Set());
860
+ const focusMenuEdge = (0, import_react3.useRef)(null);
861
+ const actionLabel = t("session.actions", { name: title });
862
+ const renameTrimmed = renameDraft.trim();
863
+ const renameBlocked = renaming || renameTrimmed === "";
864
+ const canDelete = (0, import_react3.useSyncExternalStore)(
865
+ actions.deleteAvailability.subscribe,
866
+ actions.deleteAvailability.getSnapshot,
867
+ actions.deleteAvailability.getSnapshot
868
+ );
869
+ const showDesktopActionError = (key, reason) => {
870
+ if (!canDelete) return;
871
+ toastSeq.current += 1;
872
+ setActionToast({
873
+ text: t(key, { detail: reason instanceof Error ? reason.message : String(reason) }),
874
+ seq: toastSeq.current
875
+ });
876
+ };
877
+ const restoreTriggerFocus = () => {
878
+ queueMicrotask(() => {
879
+ trigger.current?.focus({ preventScroll: true });
880
+ });
881
+ };
882
+ const openActionMenu = (focusEdge) => {
883
+ menusBeforeOpen.current = new Set(document.querySelectorAll('[role="menu"]'));
884
+ focusMenuEdge.current = focusEdge;
885
+ setMenuOpen(true);
886
+ };
887
+ (0, import_react3.useLayoutEffect)(() => {
888
+ if (!menuOpen) return;
889
+ let menu;
890
+ let observer;
891
+ let disposed = false;
892
+ const items = () => menu === void 0 ? [] : [...menu.querySelectorAll('[role="menuitem"]:not(:disabled)')];
893
+ const onMenuKeyDown = (event) => {
894
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
895
+ const available = items();
896
+ if (available.length === 0) return;
897
+ const currentIndex = available.indexOf(document.activeElement);
898
+ let nextIndex;
899
+ if (event.key === "Home") nextIndex = 0;
900
+ else if (event.key === "End") nextIndex = available.length - 1;
901
+ else if (event.key === "ArrowDown") nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % available.length;
902
+ else nextIndex = currentIndex <= 0 ? available.length - 1 : currentIndex - 1;
903
+ event.preventDefault();
904
+ available[nextIndex]?.focus({ preventScroll: true });
905
+ };
906
+ const onDocumentKeyDown = (event) => {
907
+ if (event.key === "Escape") restoreTriggerFocus();
908
+ };
909
+ const install = () => {
910
+ if (disposed) return;
911
+ menu = [...document.querySelectorAll('[role="menu"]')].find((candidate) => !menusBeforeOpen.current.has(candidate));
912
+ if (menu === void 0) return;
913
+ observer?.disconnect();
914
+ menu.addEventListener("keydown", onMenuKeyDown);
915
+ const edge = focusMenuEdge.current;
916
+ focusMenuEdge.current = null;
917
+ if (edge === null) return;
918
+ const available = items();
919
+ const target = edge === "first" ? available[0] : available.at(-1);
920
+ queueMicrotask(() => {
921
+ if (!disposed && target?.isConnected === true) target.focus({ preventScroll: true });
922
+ });
923
+ };
924
+ document.addEventListener("keydown", onDocumentKeyDown, true);
925
+ install();
926
+ if (menu === void 0) {
927
+ observer = new MutationObserver(install);
928
+ observer.observe(document.body, { childList: true, subtree: true });
929
+ install();
930
+ }
931
+ return () => {
932
+ disposed = true;
933
+ observer?.disconnect();
934
+ menu?.removeEventListener("keydown", onMenuKeyDown);
935
+ document.removeEventListener("keydown", onDocumentKeyDown, true);
936
+ };
937
+ }, [menuOpen]);
938
+ const menuItems = [
939
+ { id: "rename", label: t("menu.rename"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconEditOutline16, {}) },
940
+ { id: "fork", label: t("menu.fork"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconBranchOutline16, {}) },
941
+ { id: "unpin", label: t("menu.unpin"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PinOff, { size: 16, strokeWidth: 1.8, "aria-hidden": "true" }) },
942
+ { id: "archive", label: t("menu.archive"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconArchiveOutline20, { size: 16 }) },
943
+ ...canDelete ? [{ id: "delete", label: t("menu.delete"), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconTrashOutline16, {}), danger: true }] : []
944
+ ];
945
+ const closeRename = () => {
946
+ if (renaming) return;
947
+ setRenameOpen(false);
948
+ setRenameError(null);
949
+ restoreTriggerFocus();
950
+ };
951
+ const confirmRename = () => {
952
+ if (renameBlocked) return;
953
+ setRenaming(true);
954
+ setRenameError(null);
955
+ void actions.renameSession(session.id, renameTrimmed).then(() => {
956
+ setRenaming(false);
957
+ setRenameOpen(false);
958
+ restoreTriggerFocus();
959
+ }).catch((reason) => {
960
+ setRenaming(false);
961
+ setRenameError(reason instanceof Error ? reason.message : String(reason));
962
+ });
963
+ };
964
+ const closeDelete = () => {
965
+ if (deleting) return;
966
+ setDeleteOpen(false);
967
+ setDeleteError(null);
968
+ restoreTriggerFocus();
969
+ };
970
+ const confirmDelete = () => {
971
+ if (deleting) return;
972
+ prepareRemoval(session.id, index);
973
+ setDeleting(true);
974
+ setDeleteError(null);
975
+ void actions.deleteSession(session.id).then(() => {
976
+ setDeleting(false);
977
+ setDeleteOpen(false);
978
+ restoreTriggerFocus();
979
+ }).catch((reason) => {
980
+ cancelRemoval(session.id);
981
+ setDeleting(false);
982
+ setDeleteError(reason instanceof Error ? reason.message : String(reason));
983
+ });
984
+ };
985
+ const onSelect = (id) => {
986
+ setMenuOpen(false);
987
+ if (id === "rename") {
988
+ setRenameDraft(session.title ?? title);
989
+ setRenameError(null);
990
+ setRenameOpen(true);
991
+ }
992
+ if (id === "fork") {
993
+ restoreTriggerFocus();
994
+ void actions.forkSession(session.id).catch((reason) => {
995
+ showDesktopActionError("session.forkFailed", reason);
996
+ });
997
+ }
998
+ if (id === "unpin") unpin(session.id, index);
999
+ if (id === "archive") {
1000
+ prepareRemoval(session.id, index);
1001
+ restoreTriggerFocus();
1002
+ void actions.archiveSession(session.id).catch((reason) => {
1003
+ cancelRemoval(session.id);
1004
+ restoreTriggerFocus();
1005
+ showDesktopActionError("session.archiveFailed", reason);
1006
+ if (!canDelete) console.warn("session archive rejected:", reason);
1007
+ });
1008
+ }
1009
+ if (id === "delete" && canDelete) {
1010
+ setDeleteError(null);
1011
+ setDeleteOpen(true);
1012
+ }
1013
+ };
1014
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1015
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
718
1016
  "li",
719
1017
  {
720
1018
  className: "dsh-pins-row",
721
- "data-current": session.id === currentId ? "true" : "false",
1019
+ "data-current": current ? "true" : "false",
1020
+ "data-menu-open": menuOpen ? "true" : void 0,
722
1021
  children: [
723
1022
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
724
1023
  "button",
725
1024
  {
726
1025
  type: "button",
727
1026
  className: "dsh-pins-open",
728
- "aria-current": session.id === currentId ? "page" : void 0,
1027
+ "aria-current": current ? "page" : void 0,
729
1028
  "aria-label": t("session.open", { name: title }),
730
1029
  onClick: () => {
731
1030
  open(session.id);
@@ -738,38 +1037,130 @@ function PinnedSessionSection({ currentId, rows, open, unpin, t }) {
738
1037
  }
739
1038
  ),
740
1039
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
741
- "button",
1040
+ import_dsh_client_ui_primitives.Menu,
742
1041
  {
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);
1042
+ className: "dsh-pins-action-menu",
1043
+ open: menuOpen,
1044
+ onClose: () => {
1045
+ setMenuOpen(false);
749
1046
  },
750
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CloseIcon, {})
1047
+ items: menuItems,
1048
+ onSelect,
1049
+ portal: true,
1050
+ closeOnPointerLeave: true,
1051
+ anchor: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1052
+ "button",
1053
+ {
1054
+ ref: trigger,
1055
+ type: "button",
1056
+ className: "dsh-pins-actions",
1057
+ "aria-expanded": menuOpen,
1058
+ "aria-haspopup": "menu",
1059
+ "aria-label": actionLabel,
1060
+ title: actionLabel,
1061
+ onClick: (event) => {
1062
+ event.stopPropagation();
1063
+ if (menuOpen) setMenuOpen(false);
1064
+ else openActionMenu(null);
1065
+ },
1066
+ onKeyDown: (event) => {
1067
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1068
+ event.preventDefault();
1069
+ openActionMenu(event.key === "ArrowDown" ? "first" : "last");
1070
+ },
1071
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconEllipsisOutline16, {})
1072
+ }
1073
+ )
751
1074
  }
752
1075
  )
753
1076
  ]
1077
+ }
1078
+ ),
1079
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1080
+ import_dsh_client_ui_primitives.Modal,
1081
+ {
1082
+ open: renameOpen,
1083
+ onClose: closeRename,
1084
+ closeLabel: t("dialog.close"),
1085
+ title: t("rename.title"),
1086
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1087
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", disabled: renaming, onClick: closeRename, children: t("dialog.cancel") }),
1088
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "primary", disabled: renameBlocked, onClick: confirmRename, children: t("rename.confirm") })
1089
+ ] }),
1090
+ children: [
1091
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1092
+ "input",
1093
+ {
1094
+ className: "dsh-pins-rename-input",
1095
+ value: renameDraft,
1096
+ "aria-label": t("rename.field"),
1097
+ autoFocus: true,
1098
+ disabled: renaming,
1099
+ onFocus: (event) => {
1100
+ event.currentTarget.select();
1101
+ },
1102
+ onChange: (event) => {
1103
+ setRenameDraft(event.currentTarget.value);
1104
+ setRenameError(null);
1105
+ },
1106
+ onCompositionStart: () => {
1107
+ composing.current = true;
1108
+ },
1109
+ onCompositionEnd: () => {
1110
+ composing.current = false;
1111
+ },
1112
+ onKeyDown: (event) => {
1113
+ if (event.key !== "Enter" || composing.current) return;
1114
+ event.preventDefault();
1115
+ confirmRename();
1116
+ }
1117
+ }
1118
+ ),
1119
+ renameError !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pins-dialog-error", role: "alert", children: renameError })
1120
+ ]
1121
+ }
1122
+ ),
1123
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1124
+ import_dsh_client_ui_primitives.Modal,
1125
+ {
1126
+ open: deleteOpen,
1127
+ onClose: closeDelete,
1128
+ closeLabel: t("dialog.close"),
1129
+ title: t("delete.title"),
1130
+ description: t("delete.description", { name: title }),
1131
+ footer: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Button, { variant: "outline", autoFocus: true, disabled: deleting, onClick: closeDelete, children: t("dialog.cancel") }),
1133
+ /* @__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") })
1134
+ ] }),
1135
+ children: [
1136
+ deleting && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pins-dialog-status", role: "status", children: t("delete.pending") }),
1137
+ deleteError !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-pins-dialog-error", role: "alert", children: deleteError })
1138
+ ]
1139
+ }
1140
+ ),
1141
+ actionToast !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1142
+ import_dsh_client_ui_primitives.Toast,
1143
+ {
1144
+ text: actionToast.text,
1145
+ onDone: () => {
1146
+ setActionToast(null);
1147
+ }
754
1148
  },
755
- session.id
756
- )) })
1149
+ actionToast.seq
1150
+ )
757
1151
  ] });
758
1152
  }
759
1153
  function PinIcon({ size = 16 }) {
760
1154
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Pin, { size, strokeWidth: 1.8, "aria-hidden": "true" });
761
1155
  }
762
- function CloseIcon() {
763
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(X, { size: 14, strokeWidth: 2, "aria-hidden": "true" });
764
- }
765
1156
  /*! Bundled license information:
766
1157
 
767
1158
  lucide-react/dist/esm/shared/src/utils.js:
768
1159
  lucide-react/dist/esm/defaultAttributes.js:
769
1160
  lucide-react/dist/esm/Icon.js:
770
1161
  lucide-react/dist/esm/createLucideIcon.js:
1162
+ lucide-react/dist/esm/icons/pin-off.js:
771
1163
  lucide-react/dist/esm/icons/pin.js:
772
- lucide-react/dist/esm/icons/x.js:
773
1164
  lucide-react/dist/esm/lucide-react.js:
774
1165
  (**
775
1166
  * @license lucide-react v0.468.0 - ISC