dsh-claude-workbench 0.1.1 → 0.2.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/README.en.md CHANGED
@@ -1,16 +1,21 @@
1
1
  # dsh-claude-workbench
2
2
 
3
- A Claude-flavoured workbench enhancement for the **DeepSeek Harness (DSH)** Web GUI — one plugin, four features:
3
+ [![npm version](https://img.shields.io/npm/v/dsh-claude-workbench.svg)](https://www.npmjs.com/package/dsh-claude-workbench) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
+
5
+ A Claude-flavoured workbench enhancement for the **DeepSeek Harness (DSH)** Web GUI — one plugin, six features:
4
6
 
5
7
  1. **Claude skin** — warm cream canvas, warm stone surfaces and clay accents; serif **Crimson Pro** prose / **DM Sans** UI (fonts inlined, works offline), complete light/dark/system support.
6
8
  2. **Split view** — the “◫ Split view” pill at the bottom-right splits the window into 2–3 full DSH panes, each with its own session view (the same idea as Claude’s Split view).
7
9
  3. **Side chat** — select text in a conversation → right-click → **“在旁聊中提问”**; a docked right-hand panel boots a fork of the current session with the selection quoted in the composer. Closing archives the fork.
8
10
  4. **Quick appearance switch** — a theme button at the top-right of the session header toggles follow-system / light / dark (same as Settings → General → Appearance, persisted).
11
+ 5. **Completion chime** — a short, crisp chime when a turn finishes (like OpenCode’s completion sound); toggle it in the appearance menu, adjust its **volume** with the slider (0–250%, default 150%, previews while dragging), and the choice is remembered.
12
+ 6. **Inline next-step chips** — the assistant ends a message with a `suggestions` fenced block (a JSON array); the plugin renders it as clickable chips under the message and clicking one sends its prompt directly — the Claude-style next-step buttons. Blocking choices still use the built-in `ask_user_question` form card.
9
13
 
10
14
  ![Claude skin (light)](preview/light.png)
11
15
  ![Split view](preview/panes-2col.png)
12
16
  ![Side chat](preview/sidechat.png)
13
17
  ![Quick appearance switch](preview/theme-menu.png)
18
+ ![Inline suggestions](preview/suggestions.png)
14
19
 
15
20
  > Preview images are **synthetic renders** (plugin styles + mock data), not real conversations.
16
21
 
package/README.md CHANGED
@@ -1,11 +1,17 @@
1
1
  # dsh-claude-workbench
2
2
 
3
- 给 **DeepSeek Harness(DSH)Web GUI** 的 Claude 风格工作台增强包 —— 一个插件,四件事:
3
+ [![npm version](https://img.shields.io/npm/v/dsh-claude-workbench.svg)](https://www.npmjs.com/package/dsh-claude-workbench) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
+
5
+ 给 **DeepSeek Harness(DSH)Web GUI** 的 Claude 风格工作台增强包 —— 一个插件,六件事:
4
6
 
5
7
  1. **Claude 皮肤**:暖米白画布 + 暖石灰 + 陶土色,正文/标题用衬线 **Crimson Pro**、界面用 **DM Sans**(字体已内联,离线可用),浅色/深色/跟随系统都完整适配;
6
8
  2. **Split view(并排会话)**:右下角「◫ Split view」→ 把窗口切成 **2 栏或 3 栏**,每栏是一个完整 DSH 界面,可各自打开/切换不同会话(对应 Claude 里的 Split view);
7
9
  3. **旁聊(side chat)**:在对话里**选中文字 → 右键 → 「在旁聊中提问」**,右侧 dock 出当前会话的 fork(继承上下文),选中的内容以引用形式预填进输入框;关闭时自动归档该 fork;
8
- 4. **外观快切**:右上角主题按钮,一键在 **跟随系统 / 浅色 / 深色** 之间切换(与「设置 → 通用 → 外观」等价、持久化)。
10
+ 4. **外观快切**:右上角主题按钮,一键在 **跟随系统 / 浅色 / 深色** 之间切换(与「设置 → 通用 → 外观」等价、持久化);
11
+ 5. **完成提示音**:一轮对话结束时播放一声清脆提示音(类似 OpenCode 的完成提醒),可在右上角外观菜单里开关、并可拖动**音量滑块**(0–250%,默认 150%,调节时会试听),设置会记住。
12
+ 6. **下一步建议(内联 chips)**:助手在消息末尾输出一个 `suggestions` 代码块(JSON 数组),插件把它渲染成**消息下方的可点按钮**,点一下直接把对应 prompt 发出去 —— 和 Claude 里的下一步建议同款。
13
+
14
+ > 两种"下一步"并存:**内联 chips** 用于非阻塞的 2–3 个建议(点了直接发);**表单**(内置 `ask_user_question` 卡片)用于需要阻塞确认的决策。
9
15
 
10
16
  ![Claude 皮肤(浅色)](preview/light.png)
11
17
  ![Split view](preview/panes-2col.png)
@@ -41,7 +47,9 @@ dsh plugin --profile web add github:<owner>/dsh-claude-workbench
41
47
 
42
48
  - **Split view**:点右下角「◫ Split view」→ 默认 2 栏;顶部可切 2/3 栏、退出;每栏像平常一样点左侧会话列表即可显示不同对话。
43
49
  - **旁聊**:选中对话内容 → 右键 →「◧ 在旁聊中提问」;面板左缘可拖动调宽;「在主对话打开」把 fork 提为主对话;关闭自动归档 fork。
44
- - **外观**:会话 header 右上角按钮(右侧栏展开按钮左边),点开选 跟随系统/浅色/深色。
50
+ - **外观**:会话 header 右上角按钮(右侧栏展开按钮左边),点开选 跟随系统/浅色/深色;菜单里还有「完成提示音」开关。
51
+ - **完成提示音**:默认开启;对话完成(运行中 → 空闲)时响一声;子代理会话不响。
52
+ - **下一步建议**:只要让我"给下一步建议",我就会在消息末尾输出 `suggestions` 块,插件渲染成按钮;点按钮直接发送。
45
53
 
46
54
  ## 兼容性
47
55
 
package/lib/client.js CHANGED
@@ -3,7 +3,9 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
- const inject = [];
6
+ const inject = ["slots", "sessions"];
7
+ const react = require("react");
8
+ const jsxRuntime = require("react/jsx-runtime");
7
9
  function panesUi(boot, ctx) {
8
10
  try {
9
11
  if (window.self !== window.top) return;
@@ -103,10 +105,22 @@ window.__ModuleLoader__.load({
103
105
  '#dsh-theme-menu button svg{width:14px;height:14px;flex:none;color:var(--dsw-alias-label-secondary,#666)}',
104
106
  '#dsh-theme-menu .dsh-theme-check{width:14px;height:14px;flex:none;margin-left:auto;color:var(--dsw-alias-state-business-primary,#c6613f);visibility:hidden}',
105
107
  '#dsh-theme-menu button[data-active="1"] .dsh-theme-check{visibility:visible}',
108
+ '#dsh-theme-menu .dsh-theme-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l2,#0001)}',
109
+ '#dsh-theme-menu .dsh-theme-volume{display:flex;align-items:center;gap:8px;padding:2px 9px 7px;color:var(--dsw-alias-label-secondary,#555);font:500 12px/1 var(--dsw-font-family,system-ui)}',
110
+ '#dsh-theme-menu .dsh-theme-volume-label{flex:none}',
111
+ '#dsh-theme-menu .dsh-theme-volume input{flex:1;min-width:84px;height:14px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#c6613f);cursor:pointer}',
112
+ '#dsh-theme-menu .dsh-theme-volume-value{flex:none;width:38px;text-align:right;color:var(--dsw-alias-label-caption,#999);font-variant-numeric:tabular-nums}',
106
113
  '#dsh-sidechat-menu{position:fixed;z-index:2147483002;display:none;min-width:196px;padding:4px;border-radius:10px;background:var(--dsw-menu-surface-fill,#fff);border:.5px solid var(--dsw-alias-border-l3,#0002);box-shadow:0 8px 24px #00000022,0 2px 6px #00000012;backdrop-filter:blur(20px);font-family:var(--dsw-font-family,system-ui)}',
107
114
  '#dsh-sidechat-menu button{display:flex;align-items:center;gap:8px;width:100%;height:30px;padding:0 9px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary,#111);font:500 12px/1 var(--dsw-font-family,system-ui);cursor:pointer;text-align:left}',
108
115
  '#dsh-sidechat-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,#00000012)}',
109
- '#dsh-sidechat-menu .dsh-ctx-icon{width:16px;text-align:center;color:var(--dsw-alias-label-tertiary,#888)}'
116
+ '#dsh-sidechat-menu .dsh-ctx-icon{width:16px;text-align:center;color:var(--dsw-alias-label-tertiary,#888)}',
117
+ '#dsh-suggestions,.dsh-suggestions{display:flex;flex-wrap:wrap;align-items:center;gap:.5em;margin:1em 0 .35em}',
118
+ '.dsh-suggestions button{display:inline-flex;align-items:center;gap:.5em;min-height:2.4em;padding:.45em 1em;border-radius:.5em;border:1px solid var(--dsw-alias-border-l3,#0002);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#111);font-family:var(--dsw-font-family,system-ui);font-weight:500;font-size:.95em;line-height:1.3;cursor:pointer;transition:background .15s ease,border-color .15s ease,opacity .15s ease}',
119
+ '.dsh-suggestions button:hover{background:var(--dsw-alias-interactive-bg-hover,#00000010);border-color:var(--dsw-alias-border-l2,#0003)}',
120
+ '.dsh-suggestions button.dsh-suggestion-primary{background:var(--dsw-alias-label-primary,#141413);color:var(--dsw-alias-bg-base,#fbfaf6);border-color:transparent}',
121
+ '.dsh-suggestions button.dsh-suggestion-primary:hover{background:var(--dsw-alias-label-primary,#141413);opacity:.92}',
122
+ '.dsh-suggestions[data-sent="1"] button{opacity:.55;pointer-events:none}',
123
+ '.dsh-suggestions .dsh-suggestion-hint{font-family:var(--dsw-font-family-code,ui-monospace,SFMono-Regular,monospace);font-weight:500;font-size:.72em;line-height:1;opacity:.6}'
110
124
  ].join("\n");
111
125
  document.head.appendChild(style);
112
126
  document.body.setAttribute("data-dsh-sidechat", "0");
@@ -554,6 +568,9 @@ window.__ModuleLoader__.load({
554
568
  '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M13.4 9.6A5.9 5.9 0 0 1 6.4 2.6 6 6 0 1 0 13.4 9.6Z"/></svg>'
555
569
  };
556
570
 
571
+ var SOUND_ICON =
572
+ '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11V7a4 4 0 0 1 8 0v4l1.2 1.6H2.8L4 11Z"/><path d="M6.2 13.2a1.9 1.9 0 0 0 3.6 0"/></svg>';
573
+
557
574
  var themeButton = document.createElement("button");
558
575
  themeButton.id = "dsh-theme-toggle";
559
576
  themeButton.type = "button";
@@ -566,7 +583,10 @@ window.__ModuleLoader__.load({
566
583
  themeMenu.innerHTML =
567
584
  '<button type="button" data-pref="system"><span class="dsh-theme-icon"></span><span>跟随系统</span><span class="dsh-theme-check">✓</span></button>' +
568
585
  '<button type="button" data-pref="light"><span class="dsh-theme-icon"></span><span>浅色</span><span class="dsh-theme-check">✓</span></button>' +
569
- '<button type="button" data-pref="dark"><span class="dsh-theme-icon"></span><span>深色</span><span class="dsh-theme-check">✓</span></button>';
586
+ '<button type="button" data-pref="dark"><span class="dsh-theme-icon"></span><span>深色</span><span class="dsh-theme-check">✓</span></button>' +
587
+ '<div class="dsh-theme-sep"></div>' +
588
+ '<button type="button" data-sound="1"><span class="dsh-theme-icon">' + SOUND_ICON + '</span><span>完成提示音</span><span class="dsh-theme-check">✓</span></button>' +
589
+ '<div class="dsh-theme-volume"><span class="dsh-theme-volume-label">音量</span><input id="dsh-theme-volume" type="range" min="0" max="250" step="10" aria-label="完成提示音音量"><span class="dsh-theme-volume-value"></span></div>';
570
590
  document.body.appendChild(themeMenu);
571
591
  var themeMenuItems = Array.prototype.slice.call(themeMenu.querySelectorAll("button[data-pref]"));
572
592
  themeMenuItems.forEach(function (button) {
@@ -574,6 +594,173 @@ window.__ModuleLoader__.load({
574
594
  if (icon !== null) icon.innerHTML = THEME_ICONS[button.getAttribute("data-pref")] || "";
575
595
  });
576
596
 
597
+ // ------------------------------------------ completion sound (turn finished)
598
+ var CHIME_KEY = "dsh-workbench.completion-sound";
599
+ var CHIME_VOLUME_KEY = "dsh-workbench.chime-volume";
600
+ var chimeEnabled = true;
601
+ try { if (localStorage.getItem(CHIME_KEY) === "0") chimeEnabled = false; } catch (error) { /* ignore */ }
602
+ var chimeVolume = 1.5;
603
+ try {
604
+ var storedChimeVolumeRaw = localStorage.getItem(CHIME_VOLUME_KEY);
605
+ if (storedChimeVolumeRaw !== null && storedChimeVolumeRaw !== "") {
606
+ var storedChimeVolume = Number(storedChimeVolumeRaw);
607
+ if (Number.isFinite(storedChimeVolume) && storedChimeVolume >= 0 && storedChimeVolume <= 2.5) chimeVolume = storedChimeVolume;
608
+ }
609
+ } catch (error) {
610
+ /* ignore */
611
+ }
612
+ var chimeLog = [];
613
+ var chimeAttachAttempts = 0;
614
+ var chimeUnsubscribe = null;
615
+ var chimeAudioContext = null;
616
+ var chimeLastPlayedAt = 0;
617
+ var chimePreviousRunning = new Map();
618
+ var soundButton = themeMenu.querySelector("button[data-sound]");
619
+ var volumeInput = themeMenu.querySelector("#dsh-theme-volume");
620
+ var volumeValueLabel = themeMenu.querySelector(".dsh-theme-volume-value");
621
+
622
+ function refreshVolumeControl() {
623
+ var percent = Math.round(chimeVolume * 100);
624
+ if (volumeInput !== null) volumeInput.value = String(percent);
625
+ if (volumeValueLabel !== null) volumeValueLabel.textContent = percent + "%";
626
+ }
627
+
628
+ refreshVolumeControl();
629
+ if (volumeInput !== null) {
630
+ var volumePreviewTimer = null;
631
+ volumeInput.addEventListener("input", function () {
632
+ var nextVolume = Number(volumeInput.value) / 100;
633
+ if (!Number.isFinite(nextVolume)) return;
634
+ chimeVolume = Math.max(0, Math.min(2.5, nextVolume));
635
+ try { localStorage.setItem(CHIME_VOLUME_KEY, String(chimeVolume)); } catch (error) { /* ignore */ }
636
+ refreshVolumeControl();
637
+ if (chimeEnabled) {
638
+ clearTimeout(volumePreviewTimer);
639
+ volumePreviewTimer = setTimeout(function () { playCompletionChime(); }, 350);
640
+ }
641
+ });
642
+ }
643
+
644
+ function ensureChimeAudio() {
645
+ try {
646
+ var AudioCtor = window.AudioContext || window.webkitAudioContext;
647
+ if (AudioCtor === undefined) return null;
648
+ if (chimeAudioContext === null) chimeAudioContext = new AudioCtor();
649
+ if (chimeAudioContext.state === "suspended" && typeof chimeAudioContext.resume === "function") {
650
+ chimeAudioContext.resume().catch(function () { /* waits for the next user gesture */ });
651
+ }
652
+ return chimeAudioContext;
653
+ } catch (error) {
654
+ return null;
655
+ }
656
+ }
657
+
658
+ function playCompletionChime() {
659
+ chimeLog.push(Date.now());
660
+ if (chimeLog.length > 50) chimeLog.shift();
661
+ if (!chimeEnabled) return;
662
+ var audio = ensureChimeAudio();
663
+ if (audio === null) return;
664
+ try {
665
+ var start = audio.currentTime + 0.01;
666
+ var master = audio.createGain();
667
+ master.gain.value = chimeVolume;
668
+ master.connect(audio.destination);
669
+ function tone(frequency, offset, duration, peak, type) {
670
+ var oscillator = audio.createOscillator();
671
+ var gain = audio.createGain();
672
+ oscillator.type = type === undefined ? "sine" : type;
673
+ oscillator.frequency.value = frequency;
674
+ gain.gain.setValueAtTime(0.0001, start + offset);
675
+ gain.gain.exponentialRampToValueAtTime(peak, start + offset + 0.006);
676
+ gain.gain.exponentialRampToValueAtTime(0.0001, start + offset + duration);
677
+ oscillator.connect(gain);
678
+ gain.connect(master);
679
+ oscillator.start(start + offset);
680
+ oscillator.stop(start + offset + duration + 0.03);
681
+ }
682
+ tone(1046.5, 0, 0.34, 0.16, "sine");
683
+ tone(1567.98, 0.07, 0.3, 0.11, "sine");
684
+ tone(3135.96, 0.07, 0.14, 0.03, "sine");
685
+ } catch (error) {
686
+ /* audio is best effort */
687
+ }
688
+ }
689
+
690
+ function triggerCompletionChime() {
691
+ var now = Date.now();
692
+ if (now - chimeLastPlayedAt < 750) return;
693
+ chimeLastPlayedAt = now;
694
+ playCompletionChime();
695
+ }
696
+
697
+ function isSubagentChimeSession(sessionId) {
698
+ try {
699
+ var sessions = getService("sessions");
700
+ if (sessions === undefined || sessions.list === undefined || sessions.list.getSnapshot === undefined) return false;
701
+ var row = (sessions.list.getSnapshot().byId || {})[sessionId];
702
+ return row !== undefined && row !== null && row.subagent !== undefined && row.subagent !== null;
703
+ } catch (error) {
704
+ return false;
705
+ }
706
+ }
707
+
708
+ function onSessionRunningChanged(sessionId, running) {
709
+ var value = chimePreviousRunning.get(sessionId) === true;
710
+ chimePreviousRunning.set(sessionId, running);
711
+ if (!value || running) return;
712
+ if (isSubagentChimeSession(sessionId)) return;
713
+ triggerCompletionChime();
714
+ }
715
+
716
+ function installCompletionChime() {
717
+ var uiSession = getService("uiSession");
718
+ var status = uiSession === undefined || uiSession === null ? undefined : uiSession.sessionStatus;
719
+ if (status === undefined || status === null || typeof status.subscribe !== "function") {
720
+ chimeAttachAttempts += 1;
721
+ if (chimeAttachAttempts < 100) setTimeout(installCompletionChime, 300);
722
+ return;
723
+ }
724
+ try {
725
+ var initial = status.getSnapshot();
726
+ initial.forEach(function (value, id) {
727
+ chimePreviousRunning.set(id, value !== undefined && value !== null && value.running === true);
728
+ });
729
+ chimeUnsubscribe = status.subscribe(function () {
730
+ try {
731
+ // Client stores notify subscribers without passing the snapshot; read it.
732
+ var snapshot = status.getSnapshot();
733
+ snapshot.forEach(function (value, id) {
734
+ onSessionRunningChanged(id, value !== undefined && value !== null && value.running === true);
735
+ });
736
+ } catch (error) {
737
+ /* ignore */
738
+ }
739
+ });
740
+ } catch (error) {
741
+ console.error("[claude-workbench] completion sound subscription failed", error);
742
+ }
743
+ }
744
+
745
+ function toggleCompletionSound() {
746
+ chimeEnabled = !chimeEnabled;
747
+ try { localStorage.setItem(CHIME_KEY, chimeEnabled ? "1" : "0"); } catch (error) { /* ignore */ }
748
+ if (chimeEnabled) {
749
+ ensureChimeAudio();
750
+ if (soundButton !== null) soundButton.setAttribute("data-active", "1");
751
+ playCompletionChime();
752
+ }
753
+ refreshThemeButton();
754
+ }
755
+
756
+ function unlockChimeAudio() {
757
+ ensureChimeAudio();
758
+ }
759
+
760
+ document.addEventListener("pointerdown", unlockChimeAudio, true);
761
+ document.addEventListener("keydown", unlockChimeAudio, true);
762
+ installCompletionChime();
763
+
577
764
  function themeService() {
578
765
  return getService("theme");
579
766
  }
@@ -604,6 +791,10 @@ window.__ModuleLoader__.load({
604
791
  themeMenuItems.forEach(function (button) {
605
792
  button.setAttribute("data-active", button.getAttribute("data-pref") === preference ? "1" : "0");
606
793
  });
794
+ if (soundButton !== null) {
795
+ soundButton.setAttribute("data-active", chimeEnabled ? "1" : "0");
796
+ soundButton.title = chimeEnabled ? "完成提示音:开(点击关闭)" : "完成提示音:关(点击开启)";
797
+ }
607
798
  }
608
799
 
609
800
  function closeThemeMenu() {
@@ -631,6 +822,10 @@ window.__ModuleLoader__.load({
631
822
  themeMenu.addEventListener("click", function (event) {
632
823
  var target = event.target;
633
824
  if (!(target instanceof Element)) return;
825
+ if (target.closest("button[data-sound]") !== null) {
826
+ toggleCompletionSound();
827
+ return;
828
+ }
634
829
  var button = target.closest("button[data-pref]");
635
830
  if (button === null) return;
636
831
  var preference = button.getAttribute("data-pref");
@@ -689,6 +884,12 @@ window.__ModuleLoader__.load({
689
884
  try {
690
885
  globalThis.__dshPanesTeardown = function () {
691
886
  try { clearInterval(themeMountTimer); } catch (error) { /* ignore */ }
887
+ try { if (typeof chimeUnsubscribe === "function") chimeUnsubscribe(); } catch (error) { /* ignore */ }
888
+ chimeUnsubscribe = null;
889
+ document.removeEventListener("pointerdown", unlockChimeAudio, true);
890
+ document.removeEventListener("keydown", unlockChimeAudio, true);
891
+ try { if (chimeAudioContext !== null && typeof chimeAudioContext.close === "function") chimeAudioContext.close(); } catch (error) { /* ignore */ }
892
+ chimeAudioContext = null;
692
893
  closeThemeMenu();
693
894
  if (themeButton.parentElement !== null) themeButton.remove();
694
895
  themeMenu.remove();
@@ -720,7 +921,17 @@ window.__ModuleLoader__.load({
720
921
  archiveSession: function (id) {
721
922
  var workspaceService = getService("uiWorkspace");
722
923
  return workspaceService.archiveSession(id);
723
- }
924
+ },
925
+ playChime: playCompletionChime,
926
+ chimeInfo: function () {
927
+ return {
928
+ enabled: chimeEnabled,
929
+ attached: typeof chimeUnsubscribe === "function",
930
+ volume: Math.round(chimeVolume * 100),
931
+ log: chimeLog.slice()
932
+ };
933
+ },
934
+ toggleChime: toggleCompletionSound
724
935
  };
725
936
  } catch (error) {
726
937
  /* debug hook is optional */
@@ -729,6 +940,240 @@ window.__ModuleLoader__.load({
729
940
  console.error("[dsh-panes] initialization failed", error);
730
941
  }
731
942
  }
943
+ function suggestionChips(ctx, runtime) {
944
+ try {
945
+ var react = runtime.react;
946
+ var jsx = runtime.jsxRuntime.jsx;
947
+ var jsxs = runtime.jsxRuntime.jsxs;
948
+ var CURRENT_KEY = "dsh.sessions.current";
949
+ var MAX_ITEMS = 6;
950
+ var attachAttempts = 0;
951
+ var slotState = { found: false, registered: false, error: null, attempts: 0 };
952
+
953
+ function getService(name) {
954
+ try {
955
+ return ctx === undefined || ctx === null || typeof ctx.get !== "function" ? undefined : ctx.get(name);
956
+ } catch (error) {
957
+ return undefined;
958
+ }
959
+ }
960
+
961
+ function currentSessionId() {
962
+ var sessions = getService("sessions");
963
+ try {
964
+ if (sessions !== undefined && sessions.list !== undefined && sessions.list.getSnapshot !== undefined) {
965
+ var list = sessions.list.getSnapshot();
966
+ var byId = (list && list.byId) || {};
967
+ var ids = Object.keys(byId);
968
+ for (var index = 0; index < ids.length; index += 1) {
969
+ var summary = byId[ids[index]];
970
+ var retained = summary && summary.retainedBy;
971
+ if (retained !== undefined && retained !== null && (retained.mainView || 0) > 0) {
972
+ return summary.id !== undefined ? summary.id : summary.sessionId;
973
+ }
974
+ }
975
+ }
976
+ } catch (error) {
977
+ /* fall through to persisted selection */
978
+ }
979
+ try {
980
+ var raw = localStorage.getItem(CURRENT_KEY);
981
+ if (raw !== null) {
982
+ var parsed = JSON.parse(raw);
983
+ if (parsed !== null && typeof parsed === "object" && typeof parsed.sessionId === "string") return parsed.sessionId;
984
+ }
985
+ } catch (error) {
986
+ /* ignore malformed persisted selection */
987
+ }
988
+ return null;
989
+ }
990
+
991
+ /**
992
+ * Parse the fence body into suggestion items.
993
+ * @param {string} code - raw fence text.
994
+ * @returns {Array<{label: string, prompt: string}>|null} parsed items, or null when invalid.
995
+ */
996
+ function parseSuggestions(code) {
997
+ try {
998
+ var data = JSON.parse(String(code === undefined || code === null ? "" : code));
999
+ if (!Array.isArray(data)) return null;
1000
+ var items = [];
1001
+ for (var index = 0; index < data.length && items.length < MAX_ITEMS; index += 1) {
1002
+ var item = data[index];
1003
+ if (item === null || typeof item !== "object") continue;
1004
+ var label = typeof item.label === "string" ? item.label.trim() : "";
1005
+ var prompt = typeof item.prompt === "string" ? item.prompt.trim() : "";
1006
+ if (label.length === 0 || prompt.length === 0) continue;
1007
+ items.push({ label: label, prompt: prompt });
1008
+ }
1009
+ return items.length === 0 ? null : items;
1010
+ } catch (error) {
1011
+ return null;
1012
+ }
1013
+ }
1014
+
1015
+ /**
1016
+ * Send one suggestion as the next user message.
1017
+ * @param {string} prompt - text submitted on click.
1018
+ * @returns {boolean} whether a submission path was found.
1019
+ */
1020
+ function sendPrompt(prompt) {
1021
+ var sessions = getService("sessions");
1022
+ var sessionId = currentSessionId();
1023
+ try {
1024
+ if (sessionId !== null && sessions !== undefined && typeof sessions.binding === "function") {
1025
+ var binding = sessions.binding(sessionId);
1026
+ var live = binding === undefined || binding === null ? undefined : binding.session;
1027
+ if (live !== undefined && live !== null && typeof live.prompt === "function") {
1028
+ var signal = typeof AbortController === "function" ? new AbortController().signal : undefined;
1029
+ var result = live.prompt([{ type: "text", text: prompt }], "queue", signal);
1030
+ if (result !== undefined && result !== null && typeof result.catch === "function") result.catch(function () {});
1031
+ return true;
1032
+ }
1033
+ }
1034
+ } catch (error) {
1035
+ console.error("[claude-workbench] suggestion prompt failed", error);
1036
+ }
1037
+ // Fallback: type into the composer and press Enter.
1038
+ try {
1039
+ var editor = document.querySelector('[contenteditable="true"]');
1040
+ if (editor === null) return false;
1041
+ editor.focus();
1042
+ var selection = window.getSelection();
1043
+ if (selection !== null) {
1044
+ var range = document.createRange();
1045
+ range.selectNodeContents(editor);
1046
+ selection.removeAllRanges();
1047
+ selection.addRange(range);
1048
+ }
1049
+ document.execCommand("insertText", false, prompt);
1050
+ editor.dispatchEvent(
1051
+ new KeyboardEvent("keydown", {
1052
+ key: "Enter",
1053
+ code: "Enter",
1054
+ keyCode: 13,
1055
+ which: 13,
1056
+ bubbles: true,
1057
+ cancelable: true
1058
+ })
1059
+ );
1060
+ return true;
1061
+ } catch (error) {
1062
+ console.error("[claude-workbench] composer fallback failed", error);
1063
+ return false;
1064
+ }
1065
+ }
1066
+
1067
+ var SUGGESTION_LANGS = { suggestions: true, suggestion: true, chips: true, next: true, "next-steps": true };
1068
+
1069
+ function fenceTakesOver(fence, items) {
1070
+ if (items === null) return false;
1071
+ var lang = typeof fence.lang === "string" ? fence.lang.trim().toLowerCase() : "";
1072
+ if (lang !== "") return SUGGESTION_LANGS[lang] === true;
1073
+ // The renderer can drop an unknown info string; fall back to content
1074
+ // detection only for fences that clearly contain our JSON pair shape.
1075
+ return true;
1076
+ }
1077
+
1078
+ function SuggestionChips(props) {
1079
+ var fence = props === undefined || props === null ? {} : props;
1080
+ var ownerContent = fence.children;
1081
+ var items = parseSuggestions(fence.code);
1082
+ if (!fenceTakesOver(fence, items)) return ownerContent === undefined ? null : ownerContent;
1083
+ var state = react.useState(-1);
1084
+ var sentIndex = state[0];
1085
+ var setSentIndex = state[1];
1086
+ var lang = typeof fence.lang === "string" ? fence.lang.trim().toLowerCase() : "";
1087
+ if (lang !== "" && SUGGESTION_LANGS[lang] !== true) return ownerContent === undefined ? null : ownerContent;
1088
+ var children = [];
1089
+ for (var index = 0; index < items.length; index += 1) {
1090
+ children.push(renderChip(items[index], index, sentIndex, setSentIndex));
1091
+ }
1092
+ return jsx("div", {
1093
+ className: "dsh-suggestions",
1094
+ "data-sent": sentIndex >= 0 ? "1" : "0",
1095
+ children: children
1096
+ });
1097
+ }
1098
+
1099
+ function renderChip(item, index, sentIndex, setSentIndex) {
1100
+ var primary = index === 0;
1101
+ return jsx(
1102
+ "button",
1103
+ {
1104
+ type: "button",
1105
+ className: primary ? "dsh-suggestion dsh-suggestion-primary" : "dsh-suggestion",
1106
+ title: item.prompt,
1107
+ onClick: function (event) {
1108
+ if (event !== undefined) {
1109
+ event.preventDefault();
1110
+ event.stopPropagation();
1111
+ }
1112
+ if (sentIndex >= 0) return;
1113
+ setSentIndex(sendPrompt(item.prompt) ? index : -2);
1114
+ },
1115
+ children: primary
1116
+ ? jsxs("span", {
1117
+ className: "dsh-suggestion-content",
1118
+ children: [
1119
+ jsx("span", { className: "dsh-suggestion-label", children: item.label }),
1120
+ jsx("span", { className: "dsh-suggestion-hint", children: "⌘↵" })
1121
+ ]
1122
+ })
1123
+ : jsx("span", { className: "dsh-suggestion-label", children: item.label })
1124
+ },
1125
+ item.label + "#" + index
1126
+ );
1127
+ }
1128
+
1129
+ function attachSlot() {
1130
+ var slots;
1131
+ try {
1132
+ slots = ctx !== undefined && ctx !== null && ctx.slots !== undefined ? ctx.slots : getService("slots");
1133
+ } catch (error) {
1134
+ slots = undefined;
1135
+ }
1136
+ slotState.found = slots !== undefined && slots !== null;
1137
+ if (slots === undefined || slots === null || typeof slots.inject !== "function") {
1138
+ attachAttempts += 1;
1139
+ slotState.attempts = attachAttempts;
1140
+ if (attachAttempts < 150) setTimeout(attachSlot, 200);
1141
+ return;
1142
+ }
1143
+ try {
1144
+ slots.inject("conversation.chat.markdown.fence", function () {
1145
+ return slots.register(
1146
+ {
1147
+ name: "conversation.chat.markdown.fence",
1148
+ id: "dsh-claude-workbench/fence/suggestions"
1149
+ },
1150
+ SuggestionChips
1151
+ );
1152
+ });
1153
+ slotState.registered = true;
1154
+ } catch (error) {
1155
+ slotState.error = String(error && error.message ? error.message : error);
1156
+ console.error("[claude-workbench] suggestion slot registration failed", error);
1157
+ }
1158
+ }
1159
+
1160
+ try {
1161
+ globalThis.__dshPanesSuggestions = {
1162
+ parse: parseSuggestions,
1163
+ send: sendPrompt,
1164
+ state: function () {
1165
+ return { found: slotState.found, registered: slotState.registered, error: slotState.error, attempts: slotState.attempts };
1166
+ }
1167
+ };
1168
+ } catch (error) {
1169
+ /* debug hook is optional */
1170
+ }
1171
+
1172
+ attachSlot();
1173
+ } catch (error) {
1174
+ console.error("[claude-workbench] suggestion chips initialization failed", error);
1175
+ }
1176
+ }
732
1177
 
733
1178
  let started = false;
734
1179
  async function start(ctx) {
@@ -746,6 +1191,7 @@ window.__ModuleLoader__.load({
746
1191
 
747
1192
  function apply(ctx) {
748
1193
  start(ctx);
1194
+ try { suggestionChips(ctx, { react: react, jsxRuntime: jsxRuntime }); } catch (error) { console.error("[claude-workbench] suggestion chips failed", error); }
749
1195
  if (typeof ctx.effect === "function") {
750
1196
  ctx.effect(() => () => {
751
1197
  try { globalThis.__dshPanesTeardown?.(); } catch (error) { /* ignore */ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-claude-workbench",
3
- "version": "0.1.1",
4
- "description": "Claude-inspired workbench for the DeepSeek Harness Web GUI: warm editorial skin (Crimson Pro/DM Sans), split view with 2-3 side-by-side conversations, selection-based side chat, and a top-right light/dark switch.",
3
+ "version": "0.2.2",
4
+ "description": "Claude-inspired workbench for the DeepSeek Harness Web GUI: warm editorial skin (Crimson Pro/DM Sans), split view with 2-3 side-by-side conversations, selection-based side chat, a top-right light/dark switch, and a completion chime when a turn finishes.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -30,7 +30,10 @@
30
30
  "split-view",
31
31
  "splitview",
32
32
  "side-by-side",
33
- "claude-style"
33
+ "claude-style",
34
+ "notification",
35
+ "chime",
36
+ "completion-sound"
34
37
  ],
35
38
  "license": "MIT",
36
39
  "dsh": {
@@ -42,7 +45,9 @@
42
45
  "@deepseek-ai/dsh-api-session-controller",
43
46
  "@deepseek-ai/dsh-client-ui-workspace",
44
47
  "@deepseek-ai/dsh-client-ui-theme",
45
- "@deepseek-ai/dsh-client-ui-conversation"
48
+ "@deepseek-ai/dsh-client-ui-conversation",
49
+ "@deepseek-ai/dsh-client-ui-slots",
50
+ "@deepseek-ai/dsh-client-ui-chat"
46
51
  ],
47
52
  "platform": "web",
48
53
  "immediately": true