@lihengxi20/dsh-tabs 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 (3) hide show
  1. package/README.md +2 -1
  2. package/lib/client.js +120 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,9 +20,10 @@ DSH(DeepSeek Harness)Web GUI 的**顶部会话标签页**插件:在会话
20
20
  - **标签文案**:`工作区 / 会话标题`;工作区取侧边栏的工作区标题(缺省回退目录名),空白会话显示 `工作区 / 新会话`。
21
21
  - **切换与关闭**:点击切换;悬停出现 `×`(单个标签也可关闭);中键点击关闭。关闭只移除标签,不归档、不停止任务;关闭最后一个标签会回到「未选中会话」的空白页。
22
22
  - **右键菜单**:重命名 / 关闭 / 关闭其他 (n) / 全部关闭 (n);重命名走 Host 的 `session/rename`,空白会话禁用。
23
+ - **固定标签**:鼠标移入标签时,关闭按钮左侧出现图钉;点击可固定/取消固定。固定标签始终排在标签栏最左侧(按固定先后排列),刷新后保留;固定只影响显示顺序,不改变关闭语义。
23
24
  - **状态圆点**:运行中(蓝/脉冲)、等待审批或回答(橙)、完成未读(绿)。
24
25
  - **持久化**:标签顺序、活动标签、标题与工作区缓存存在浏览器 `localStorage`(键 `dsh-session-tabs.v1`),刷新/重开客户端后恢复。
25
- - **跟随主题**:全部使用 DSH 主题变量,深浅色、字体大小自动适配。
26
+ - **跟随主题**:文字、边框、背景、右键菜单与重命名弹层全部使用 DSH 主题变量;标签文字浅色下纯黑、深色下纯白;激活标签下划线深色为暖金 `#E6CCA0`,浅色自动换成对比度更高的 `#A67C2E`。
26
27
 
27
28
  标签是导航状态:DSH 同一时刻仍只渲染一个会话,后台任务照常运行。
28
29
 
package/lib/client.js CHANGED
@@ -59,6 +59,8 @@
59
59
  "menu.close": "关闭",
60
60
  "menu.closeOthers": "关闭其他",
61
61
  "menu.closeAll": "全部关闭",
62
+ "pin": "固定标签",
63
+ "unpin": "取消固定标签",
62
64
  "rename.title": "重命名会话",
63
65
  "rename.placeholder": "输入会话标题",
64
66
  "rename.cancel": "取消",
@@ -76,6 +78,8 @@
76
78
  "menu.close": "Close",
77
79
  "menu.closeOthers": "Close Others",
78
80
  "menu.closeAll": "Close All",
81
+ "pin": "Pin tab",
82
+ "unpin": "Unpin tab",
79
83
  "rename.title": "Rename session",
80
84
  "rename.placeholder": "Session title",
81
85
  "rename.cancel": "Cancel",
@@ -90,6 +94,7 @@
90
94
  // ─────────────────────────────────────────────────────────────────────
91
95
  var state = {
92
96
  tabs: [], // session ids, opening order
97
+ pinned: [], // session ids, pinned order (shown first, left side)
93
98
  titles: {}, // sessionId -> last known durable title
94
99
  wsTitles: {}, // sessionId -> workspace display title
95
100
  currentId: null, // the session the header is actually showing
@@ -119,7 +124,7 @@
119
124
  var lastPersisted = null;
120
125
  function persist() {
121
126
  try {
122
- var payload = JSON.stringify({ tabs: state.tabs, titles: state.titles, wsTitles: state.wsTitles, active: state.active });
127
+ var payload = JSON.stringify({ tabs: state.tabs, pinned: state.pinned, titles: state.titles, wsTitles: state.wsTitles, active: state.active });
123
128
  if (payload === lastPersisted) return;
124
129
  root.localStorage.setItem(LS_KEY, payload);
125
130
  lastPersisted = payload;
@@ -149,8 +154,15 @@
149
154
  if (typeof value === "string") wsTitles[id] = value;
150
155
  });
151
156
  }
157
+ var pinned = [];
158
+ if (Array.isArray(parsed.pinned)) {
159
+ parsed.pinned.forEach(function (id) {
160
+ if (typeof id === "string" && tabs.indexOf(id) >= 0 && pinned.indexOf(id) < 0) pinned.push(id);
161
+ });
162
+ }
152
163
  state = Object.assign({}, state, {
153
164
  tabs: tabs,
165
+ pinned: pinned,
154
166
  titles: titles,
155
167
  wsTitles: wsTitles,
156
168
  active: typeof parsed.active === "string" ? parsed.active : null
@@ -178,6 +190,29 @@
178
190
  return "";
179
191
  }
180
192
 
193
+ /** Visible tab order: pinned group (pin order) first, then unpinned tabs (open order). */
194
+ function displayOrder(tabs, pinned) {
195
+ var pinnedIds = [];
196
+ (pinned || []).forEach(function (id) {
197
+ if (tabs.indexOf(id) >= 0 && pinnedIds.indexOf(id) < 0) pinnedIds.push(id);
198
+ });
199
+ return pinnedIds.concat(tabs.filter(function (id) { return pinnedIds.indexOf(id) < 0; }));
200
+ }
201
+
202
+ /** Enforce MAX_TABS by trimming the oldest unpinned tabs; pinned tabs survive. */
203
+ function trimTabs(tabs) {
204
+ if (tabs.length <= MAX_TABS) return tabs;
205
+ var pinnedSet = {};
206
+ (state.pinned || []).forEach(function (id) { pinnedSet[id] = true; });
207
+ var out = tabs.slice();
208
+ var i = 0;
209
+ while (out.length > MAX_TABS && i < out.length) {
210
+ if (pinnedSet[out[i]] === true) i++;
211
+ else out.splice(i, 1);
212
+ }
213
+ return out.length > MAX_TABS ? out.slice(out.length - MAX_TABS) : out;
214
+ }
215
+
181
216
  function syncFromList(list) {
182
217
  if (!list) return;
183
218
  var tabs = state.tabs.slice();
@@ -207,19 +242,22 @@
207
242
 
208
243
  // Prune tabs whose session vanished (deleted / archived away / no
209
244
  // longer catalogued). Never prune the currently open session.
245
+ var pinned = state.pinned.slice();
210
246
  if (ready) {
211
247
  var kept = tabs.filter(function (id) {
212
248
  return id === current || Object.prototype.hasOwnProperty.call(byId, id);
213
249
  });
214
250
  if (kept.length !== tabs.length) { tabs = kept; changed = true; }
215
251
  }
252
+ var validPinned = pinned.filter(function (id) { return tabs.indexOf(id) >= 0; });
253
+ if (validPinned.length !== pinned.length) { pinned = validPinned; changed = true; }
216
254
 
217
255
  // Opening a session (sidebar / search / new session / tab click) adds
218
256
  // it. The current session is always present, so even the first session
219
257
  // of a fresh window gets exactly one tab.
220
258
  if (current && tabs.indexOf(current) < 0 && !isSuppressed(current)) {
221
259
  tabs.push(current);
222
- if (tabs.length > MAX_TABS) tabs = tabs.slice(tabs.length - MAX_TABS);
260
+ tabs = trimTabs(tabs);
223
261
  changed = true;
224
262
  }
225
263
 
@@ -229,6 +267,7 @@
229
267
  state = Object.assign({}, state, {
230
268
  list: list,
231
269
  tabs: tabs,
270
+ pinned: pinned,
232
271
  titles: titles,
233
272
  active: active,
234
273
  ready: state.ready || ready
@@ -293,7 +332,7 @@
293
332
  var titles = Object.assign({}, state.titles);
294
333
  if (id && tabs.indexOf(id) < 0 && !isSuppressed(id)) {
295
334
  tabs.push(id);
296
- if (tabs.length > MAX_TABS) tabs = tabs.slice(tabs.length - MAX_TABS);
335
+ tabs = trimTabs(tabs);
297
336
  }
298
337
  state = Object.assign({}, state, {
299
338
  currentId: id,
@@ -354,10 +393,15 @@
354
393
  tabs.splice(idx, 1);
355
394
  var titles = Object.assign({}, state.titles);
356
395
  delete titles[id];
396
+ var pinned = state.pinned.filter(function (pid) { return pid !== id; });
357
397
  var wasCurrent = (state.currentId || currentSessionId(state.list)) === id;
358
- var nextActive = tabs[Math.min(idx, tabs.length - 1)] || null;
398
+ var order = displayOrder(state.tabs, state.pinned);
399
+ var orderIdx = order.indexOf(id);
400
+ var remaining = order.filter(function (tid) { return tid !== id; });
401
+ var nextActive = remaining[Math.min(Math.max(orderIdx, 0), remaining.length - 1)] || null;
359
402
  state = Object.assign({}, state, {
360
403
  tabs: tabs,
404
+ pinned: pinned,
361
405
  titles: titles,
362
406
  active: nextActive,
363
407
  closed: { id: id, at: Date.now() }
@@ -372,8 +416,10 @@
372
416
  openSession(ctx, keepId);
373
417
  var titles = {};
374
418
  if (state.titles[keepId]) titles[keepId] = state.titles[keepId];
419
+ var pinned = state.pinned.indexOf(keepId) >= 0 ? [keepId] : [];
375
420
  state = Object.assign({}, state, {
376
421
  tabs: [keepId],
422
+ pinned: pinned,
377
423
  titles: titles,
378
424
  active: keepId,
379
425
  currentId: keepId,
@@ -387,6 +433,7 @@
387
433
  if (state.tabs.length === 0) return;
388
434
  state = Object.assign({}, state, {
389
435
  tabs: [],
436
+ pinned: [],
390
437
  titles: {},
391
438
  active: null,
392
439
  currentId: null,
@@ -405,6 +452,17 @@
405
452
  }
406
453
  }
407
454
 
455
+ function togglePin(ctx, id) {
456
+ if (state.tabs.indexOf(id) < 0) return;
457
+ var pinned = state.pinned.slice();
458
+ var idx = pinned.indexOf(id);
459
+ if (idx >= 0) pinned.splice(idx, 1);
460
+ else pinned.push(id);
461
+ state = Object.assign({}, state, { pinned: pinned });
462
+ persist();
463
+ emit();
464
+ }
465
+
408
466
  function renameSession(ctx, id, title) {
409
467
  var sessions = null;
410
468
  try { sessions = ctx.get("sessions"); } catch (e) { sessions = null; }
@@ -441,6 +499,16 @@
441
499
  }));
442
500
  }
443
501
 
502
+ function PinIcon(pinned) {
503
+ return h("svg", {
504
+ width: 11, height: 11, viewBox: "0 0 24 24", fill: pinned ? "currentColor" : "none",
505
+ "aria-hidden": "true"
506
+ }, h("path", {
507
+ d: "M12 17v5M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z",
508
+ stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", strokeLinecap: "round"
509
+ }));
510
+ }
511
+
444
512
  // ─────────────────────────────────────────────────────────────────────
445
513
  // components
446
514
  // ─────────────────────────────────────────────────────────────────────
@@ -480,6 +548,9 @@
480
548
  event.stopPropagation();
481
549
  closeTab(props.ctx, props.id);
482
550
  },
551
+ onAuxClick: function (event) {
552
+ event.stopPropagation();
553
+ },
483
554
  onContextMenu: function (event) {
484
555
  event.preventDefault();
485
556
  event.stopPropagation();
@@ -487,6 +558,30 @@
487
558
  }, CloseIcon());
488
559
  }
489
560
 
561
+ function PinButton(props) {
562
+ var pinned = props.pinned === true;
563
+ var label = props.t(pinned ? "unpin" : "pin");
564
+ return h("button", {
565
+ type: "button",
566
+ className: "dshst-pin",
567
+ title: label,
568
+ "aria-label": label,
569
+ "aria-pressed": pinned ? "true" : "false",
570
+ "data-pinned": pinned ? "true" : "false",
571
+ onClick: function (event) {
572
+ event.stopPropagation();
573
+ togglePin(props.ctx, props.id);
574
+ },
575
+ onAuxClick: function (event) {
576
+ event.stopPropagation();
577
+ },
578
+ onContextMenu: function (event) {
579
+ event.preventDefault();
580
+ event.stopPropagation();
581
+ }
582
+ }, PinIcon(pinned));
583
+ }
584
+
490
585
  function useMenuPosition(x, y, width, height) {
491
586
  var ref = React.useRef(null);
492
587
  var posState = React.useState({ x: x, y: y });
@@ -721,6 +816,8 @@
721
816
  var store = props.store;
722
817
  var list = store.list || { byId: {} };
723
818
  var tabs = store.tabs || [];
819
+ var pinned = store.pinned || [];
820
+ var displayTabs = displayOrder(tabs, pinned);
724
821
  var ctx = props.ctx;
725
822
  var t = props.t;
726
823
  var current = store.currentId || currentSessionId(list) || store.active;
@@ -743,7 +840,7 @@
743
840
  className: "dshst-tabs",
744
841
  role: "tablist",
745
842
  "aria-label": t("tabs.aria")
746
- }, tabs.map(function (id) {
843
+ }, displayTabs.map(function (id) {
747
844
  var row = list.byId ? list.byId[id] : undefined;
748
845
  var label;
749
846
  if (row && row.blank) label = t("newSession");
@@ -752,6 +849,7 @@
752
849
  else label = t("untitled");
753
850
  var workspace = workspaceTitleFor(store, id, row);
754
851
  var active = id === current;
852
+ var isPinned = pinned.indexOf(id) >= 0;
755
853
  var status = row ? statusOf(store, row) : "idle";
756
854
  var title = (workspace ? workspace + " / " : "") + label + (row && row.cwd ? "\n" + row.cwd : "");
757
855
  return h("div", {
@@ -761,6 +859,7 @@
761
859
  tabIndex: active ? 0 : -1,
762
860
  "aria-selected": active ? "true" : "false",
763
861
  "data-active": active ? "true" : "false",
862
+ "data-pinned": isPinned ? "true" : "false",
764
863
  "data-status": status,
765
864
  "data-session-id": id,
766
865
  "data-menu-target": menu !== null && menu.id === id ? "true" : "false",
@@ -785,6 +884,7 @@
785
884
  ? h("span", { className: "dshst-sep", "aria-hidden": "true" }, "/")
786
885
  : null,
787
886
  h("span", { className: "dshst-label" }, label),
887
+ h(PinButton, { ctx: ctx, id: id, t: t, pinned: isPinned }),
788
888
  h(CloseButton, { ctx: ctx, id: id, t: t })
789
889
  );
790
890
  }));
@@ -917,20 +1017,22 @@
917
1017
  // styles
918
1018
  // ─────────────────────────────────────────────────────────────────────
919
1019
  var CSS = [
1020
+ "body{--dshst-tab-accent:#a67c2e;--dshst-tab-text:#000;--dshst-tab-text-hover:#000}",
1021
+ "body[data-ds-dark-theme]{--dshst-tab-accent:#e6cca0;--dshst-tab-text:#fff;--dshst-tab-text-hover:#fff}",
920
1022
  ".dshst-leading{display:inline-flex;align-items:center;gap:6px;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:20px;-webkit-app-region:no-drag}",
921
1023
  ".dshst-icon{flex:none;opacity:.9}",
922
1024
  ".dshst-ws{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
923
1025
  ".dshst-leading .dshst-ws{max-width:180px}",
924
1026
  ".dshst-sep{color:var(--dsw-alias-label-caption);flex:none}",
925
- ".dshst-row{--dshst-tab-accent:#e6cca0;grid-column:1/-1;min-width:0;padding-left:8px;margin-top:0;display:flex;align-items:flex-start;-webkit-app-region:no-drag}",
1027
+ ".dshst-row{grid-column:1/-1;min-width:0;padding-left:8px;margin-top:0;display:flex;align-items:flex-start;-webkit-app-region:no-drag}",
926
1028
  "header:has(> [data-dsh-session-tabs]){padding-bottom:0}",
927
1029
  ".dshst-tabs{display:flex;align-items:center;gap:0;min-width:0;width:100%;overflow-x:auto;scrollbar-width:none;padding:0}",
928
1030
  ".dshst-tabs::-webkit-scrollbar{display:none}",
929
- ".dshst-tab{display:inline-flex;align-items:center;gap:6px;flex:none;max-width:300px;min-width:0;min-height:24px;padding:4px 10px 7px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:13px;font-weight:500;line-height:16px;cursor:pointer;position:relative;-webkit-app-region:no-drag}",
1031
+ ".dshst-tab{display:inline-flex;align-items:center;gap:6px;flex:none;max-width:300px;min-width:0;min-height:24px;padding:4px 10px 7px;border:0;border-radius:6px;background:transparent;color:var(--dshst-tab-text);font:inherit;font-size:13px;font-weight:500;line-height:16px;cursor:pointer;position:relative;-webkit-app-region:no-drag}",
930
1032
  ".dshst-tab:before{content:\"\";position:absolute;inset:0;border:1px solid transparent;border-bottom:0;border-radius:6px 6px 0 0;pointer-events:none}",
931
- ".dshst-tab .dshst-ws{max-width:110px;flex:none;color:var(--dsw-alias-label-caption);font-size:12px;font-weight:500;line-height:16px}",
932
- ".dshst-tab .dshst-sep{font-size:12px;opacity:.9}",
933
- "@media (hover:hover){.dshst-tab:hover{color:var(--dsw-alias-label-secondary)}.dshst-tab:hover:before{border-color:var(--dsw-alias-border-l3)}}",
1033
+ ".dshst-tab .dshst-ws{max-width:110px;flex:none;color:var(--dshst-tab-text);opacity:.65;font-size:12px;font-weight:500;line-height:16px}",
1034
+ ".dshst-tab .dshst-sep{color:var(--dshst-tab-text);font-size:12px;opacity:.6}",
1035
+ "@media (hover:hover){.dshst-tab:hover{color:var(--dshst-tab-text-hover)}.dshst-tab:hover:before{border-color:var(--dsw-alias-border-l3)}}",
934
1036
  ".dshst-tab:focus-visible:before{border-color:var(--dsw-alias-border-l3)}",
935
1037
  ".dshst-tab[data-active=\"true\"]{color:var(--dsw-alias-state-business-primary)}",
936
1038
  ".dshst-tab[data-active=\"true\"]:before{border-color:var(--dsw-alias-border-l4)}",
@@ -945,15 +1047,18 @@
945
1047
  ".dshst-close{display:inline-flex;align-items:center;justify-content:center;flex:none;width:14px;height:14px;margin-left:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0}",
946
1048
  ".dshst-tab:hover .dshst-close,.dshst-tab:focus-within .dshst-close{opacity:1}",
947
1049
  "@media (hover:hover){.dshst-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}}",
1050
+ ".dshst-pin{display:inline-flex;align-items:center;justify-content:center;flex:none;width:14px;height:14px;margin-left:2px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0}",
1051
+ ".dshst-tab:hover .dshst-pin,.dshst-tab:focus-within .dshst-pin{opacity:1}",
1052
+ ".dshst-tab[data-pinned=\"true\"] .dshst-pin{opacity:1;color:var(--dsw-alias-label-secondary)}",
1053
+ "@media (hover:hover){.dshst-pin:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dshst-tab[data-pinned=\"true\"] .dshst-pin:hover{color:var(--dshst-tab-text)}}",
948
1054
  "@media (prefers-reduced-motion:reduce){.dshst-tab[data-status=\"running\"] .dshst-dot{animation:none}}",
949
1055
  ".dshst-tab[data-menu-target=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}",
950
- ".dshst-menu{position:fixed;z-index:1200;min-width:180px;box-sizing:border-box;padding:4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 10px 30px rgba(0,0,0,.35);outline:none;-webkit-app-region:no-drag}",
951
- ".dshst-menu-item{display:flex;align-items:center;justify-content:space-between;gap:18px;height:32px;padding:0 10px;border-radius:5px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:16px;cursor:pointer;white-space:nowrap;user-select:none}",
1056
+ ".dshst-menu{position:fixed;z-index:1200;min-width:180px;box-sizing:border-box;padding:4px;border:0;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);outline:none;-webkit-app-region:no-drag}",
1057
+ ".dshst-menu-item{display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:34px;padding:6px 10px;border-radius:var(--dsw-radius-md);color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;cursor:pointer;white-space:nowrap;user-select:none}",
952
1058
  ".dshst-menu-item[data-hovered=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}",
953
- ".dshst-menu-item[data-disabled=\"true\"]{color:var(--dsw-alias-label-dimmed);cursor:default;background:transparent}",
1059
+ ".dshst-menu-item[data-disabled=\"true\"]{opacity:.4;cursor:default;background:transparent}",
954
1060
  ".dshst-menu-count{color:var(--dsw-alias-label-caption);font-size:12px;font-variant-numeric:tabular-nums}",
955
- ".dshst-menu-item[data-disabled=\"true\"] .dshst-menu-count{color:var(--dsw-alias-label-dimmed)}",
956
- ".dshst-rename{position:fixed;z-index:1201;width:280px;box-sizing:border-box;padding:12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);box-shadow:0 12px 32px rgba(0,0,0,.4);-webkit-app-region:no-drag}",
1061
+ ".dshst-rename{position:fixed;z-index:1201;width:280px;box-sizing:border-box;padding:14px;border:0;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);-webkit-app-region:no-drag}",
957
1062
  ".dshst-rename-title{margin-bottom:8px;color:var(--dsw-alias-label-caption);font-size:12px;line-height:16px}",
958
1063
  ".dshst-rename-input{width:100%;box-sizing:border-box;height:32px;padding:0 9px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;outline:none}",
959
1064
  ".dshst-rename-input:focus{border-color:var(--dsw-alias-state-business-primary)}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lihengxi20/dsh-tabs",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Session tabs in the DSH conversation header: switch, close, rename and persist open sessions, with workspace/title labels and live status.",
5
5
  "license": "MIT",
6
6
  "type": "module",