dsh-claude-workbench 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.
package/README.en.md CHANGED
@@ -1,11 +1,14 @@
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, five 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 and the choice is remembered.
9
12
 
10
13
  ![Claude skin (light)](preview/light.png)
11
14
  ![Split view](preview/panes-2col.png)
package/README.md CHANGED
@@ -1,11 +1,14 @@
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 的完成提醒),可在右上角外观菜单里开关,设置会记住。
9
12
 
10
13
  ![Claude 皮肤(浅色)](preview/light.png)
11
14
  ![Split view](preview/panes-2col.png)
@@ -41,7 +44,8 @@ dsh plugin --profile web add github:<owner>/dsh-claude-workbench
41
44
 
42
45
  - **Split view**:点右下角「◫ Split view」→ 默认 2 栏;顶部可切 2/3 栏、退出;每栏像平常一样点左侧会话列表即可显示不同对话。
43
46
  - **旁聊**:选中对话内容 → 右键 →「◧ 在旁聊中提问」;面板左缘可拖动调宽;「在主对话打开」把 fork 提为主对话;关闭自动归档 fork。
44
- - **外观**:会话 header 右上角按钮(右侧栏展开按钮左边),点开选 跟随系统/浅色/深色。
47
+ - **外观**:会话 header 右上角按钮(右侧栏展开按钮左边),点开选 跟随系统/浅色/深色;菜单里还有「完成提示音」开关。
48
+ - **完成提示音**:默认开启;对话完成(运行中 → 空闲)时响一声;子代理会话不响。
45
49
 
46
50
  ## 兼容性
47
51
 
package/lib/client.js CHANGED
@@ -103,6 +103,7 @@ window.__ModuleLoader__.load({
103
103
  '#dsh-theme-menu button svg{width:14px;height:14px;flex:none;color:var(--dsw-alias-label-secondary,#666)}',
104
104
  '#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
105
  '#dsh-theme-menu button[data-active="1"] .dsh-theme-check{visibility:visible}',
106
+ '#dsh-theme-menu .dsh-theme-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l2,#0001)}',
106
107
  '#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
108
  '#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
109
  '#dsh-sidechat-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,#00000012)}',
@@ -554,6 +555,9 @@ window.__ModuleLoader__.load({
554
555
  '<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
556
  };
556
557
 
558
+ var SOUND_ICON =
559
+ '<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>';
560
+
557
561
  var themeButton = document.createElement("button");
558
562
  themeButton.id = "dsh-theme-toggle";
559
563
  themeButton.type = "button";
@@ -566,7 +570,9 @@ window.__ModuleLoader__.load({
566
570
  themeMenu.innerHTML =
567
571
  '<button type="button" data-pref="system"><span class="dsh-theme-icon"></span><span>跟随系统</span><span class="dsh-theme-check">✓</span></button>' +
568
572
  '<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>';
573
+ '<button type="button" data-pref="dark"><span class="dsh-theme-icon"></span><span>深色</span><span class="dsh-theme-check">✓</span></button>' +
574
+ '<div class="dsh-theme-sep"></div>' +
575
+ '<button type="button" data-sound="1"><span class="dsh-theme-icon">' + SOUND_ICON + '</span><span>完成提示音</span><span class="dsh-theme-check">✓</span></button>';
570
576
  document.body.appendChild(themeMenu);
571
577
  var themeMenuItems = Array.prototype.slice.call(themeMenu.querySelectorAll("button[data-pref]"));
572
578
  themeMenuItems.forEach(function (button) {
@@ -574,6 +580,133 @@ window.__ModuleLoader__.load({
574
580
  if (icon !== null) icon.innerHTML = THEME_ICONS[button.getAttribute("data-pref")] || "";
575
581
  });
576
582
 
583
+ // ------------------------------------------ completion sound (turn finished)
584
+ var CHIME_KEY = "dsh-workbench.completion-sound";
585
+ var chimeEnabled = true;
586
+ try { if (localStorage.getItem(CHIME_KEY) === "0") chimeEnabled = false; } catch (error) { /* ignore */ }
587
+ var chimeLog = [];
588
+ var chimeAttachAttempts = 0;
589
+ var chimeUnsubscribe = null;
590
+ var chimeAudioContext = null;
591
+ var chimeLastPlayedAt = 0;
592
+ var chimePreviousRunning = new Map();
593
+ var soundButton = themeMenu.querySelector("button[data-sound]");
594
+
595
+ function ensureChimeAudio() {
596
+ try {
597
+ var AudioCtor = window.AudioContext || window.webkitAudioContext;
598
+ if (AudioCtor === undefined) return null;
599
+ if (chimeAudioContext === null) chimeAudioContext = new AudioCtor();
600
+ if (chimeAudioContext.state === "suspended" && typeof chimeAudioContext.resume === "function") {
601
+ chimeAudioContext.resume().catch(function () { /* waits for the next user gesture */ });
602
+ }
603
+ return chimeAudioContext;
604
+ } catch (error) {
605
+ return null;
606
+ }
607
+ }
608
+
609
+ function playCompletionChime() {
610
+ chimeLog.push(Date.now());
611
+ if (chimeLog.length > 50) chimeLog.shift();
612
+ if (!chimeEnabled) return;
613
+ var audio = ensureChimeAudio();
614
+ if (audio === null) return;
615
+ try {
616
+ var start = audio.currentTime + 0.01;
617
+ function tone(frequency, offset, duration, peak, type) {
618
+ var oscillator = audio.createOscillator();
619
+ var gain = audio.createGain();
620
+ oscillator.type = type === undefined ? "sine" : type;
621
+ oscillator.frequency.value = frequency;
622
+ gain.gain.setValueAtTime(0.0001, start + offset);
623
+ gain.gain.exponentialRampToValueAtTime(peak, start + offset + 0.006);
624
+ gain.gain.exponentialRampToValueAtTime(0.0001, start + offset + duration);
625
+ oscillator.connect(gain);
626
+ gain.connect(audio.destination);
627
+ oscillator.start(start + offset);
628
+ oscillator.stop(start + offset + duration + 0.03);
629
+ }
630
+ tone(1046.5, 0, 0.34, 0.13, "sine");
631
+ tone(1567.98, 0.07, 0.3, 0.09, "sine");
632
+ tone(3135.96, 0.07, 0.14, 0.02, "sine");
633
+ } catch (error) {
634
+ /* audio is best effort */
635
+ }
636
+ }
637
+
638
+ function triggerCompletionChime() {
639
+ var now = Date.now();
640
+ if (now - chimeLastPlayedAt < 750) return;
641
+ chimeLastPlayedAt = now;
642
+ playCompletionChime();
643
+ }
644
+
645
+ function isSubagentChimeSession(sessionId) {
646
+ try {
647
+ var sessions = getService("sessions");
648
+ if (sessions === undefined || sessions.list === undefined || sessions.list.getSnapshot === undefined) return false;
649
+ var row = (sessions.list.getSnapshot().byId || {})[sessionId];
650
+ return row !== undefined && row !== null && row.subagent !== undefined && row.subagent !== null;
651
+ } catch (error) {
652
+ return false;
653
+ }
654
+ }
655
+
656
+ function onSessionRunningChanged(sessionId, running) {
657
+ var value = chimePreviousRunning.get(sessionId) === true;
658
+ chimePreviousRunning.set(sessionId, running);
659
+ if (!value || running) return;
660
+ if (isSubagentChimeSession(sessionId)) return;
661
+ triggerCompletionChime();
662
+ }
663
+
664
+ function installCompletionChime() {
665
+ var uiSession = getService("uiSession");
666
+ var status = uiSession === undefined || uiSession === null ? undefined : uiSession.sessionStatus;
667
+ if (status === undefined || status === null || typeof status.subscribe !== "function") {
668
+ chimeAttachAttempts += 1;
669
+ if (chimeAttachAttempts < 100) setTimeout(installCompletionChime, 300);
670
+ return;
671
+ }
672
+ try {
673
+ var initial = status.getSnapshot();
674
+ initial.forEach(function (value, id) {
675
+ chimePreviousRunning.set(id, value !== undefined && value !== null && value.running === true);
676
+ });
677
+ chimeUnsubscribe = status.subscribe(function (snapshot) {
678
+ try {
679
+ snapshot.forEach(function (value, id) {
680
+ onSessionRunningChanged(id, value !== undefined && value !== null && value.running === true);
681
+ });
682
+ } catch (error) {
683
+ /* ignore */
684
+ }
685
+ });
686
+ } catch (error) {
687
+ console.error("[claude-workbench] completion sound subscription failed", error);
688
+ }
689
+ }
690
+
691
+ function toggleCompletionSound() {
692
+ chimeEnabled = !chimeEnabled;
693
+ try { localStorage.setItem(CHIME_KEY, chimeEnabled ? "1" : "0"); } catch (error) { /* ignore */ }
694
+ if (chimeEnabled) {
695
+ ensureChimeAudio();
696
+ if (soundButton !== null) soundButton.setAttribute("data-active", "1");
697
+ playCompletionChime();
698
+ }
699
+ refreshThemeButton();
700
+ }
701
+
702
+ function unlockChimeAudio() {
703
+ ensureChimeAudio();
704
+ }
705
+
706
+ document.addEventListener("pointerdown", unlockChimeAudio, true);
707
+ document.addEventListener("keydown", unlockChimeAudio, true);
708
+ installCompletionChime();
709
+
577
710
  function themeService() {
578
711
  return getService("theme");
579
712
  }
@@ -604,6 +737,10 @@ window.__ModuleLoader__.load({
604
737
  themeMenuItems.forEach(function (button) {
605
738
  button.setAttribute("data-active", button.getAttribute("data-pref") === preference ? "1" : "0");
606
739
  });
740
+ if (soundButton !== null) {
741
+ soundButton.setAttribute("data-active", chimeEnabled ? "1" : "0");
742
+ soundButton.title = chimeEnabled ? "完成提示音:开(点击关闭)" : "完成提示音:关(点击开启)";
743
+ }
607
744
  }
608
745
 
609
746
  function closeThemeMenu() {
@@ -631,6 +768,10 @@ window.__ModuleLoader__.load({
631
768
  themeMenu.addEventListener("click", function (event) {
632
769
  var target = event.target;
633
770
  if (!(target instanceof Element)) return;
771
+ if (target.closest("button[data-sound]") !== null) {
772
+ toggleCompletionSound();
773
+ return;
774
+ }
634
775
  var button = target.closest("button[data-pref]");
635
776
  if (button === null) return;
636
777
  var preference = button.getAttribute("data-pref");
@@ -689,6 +830,12 @@ window.__ModuleLoader__.load({
689
830
  try {
690
831
  globalThis.__dshPanesTeardown = function () {
691
832
  try { clearInterval(themeMountTimer); } catch (error) { /* ignore */ }
833
+ try { if (typeof chimeUnsubscribe === "function") chimeUnsubscribe(); } catch (error) { /* ignore */ }
834
+ chimeUnsubscribe = null;
835
+ document.removeEventListener("pointerdown", unlockChimeAudio, true);
836
+ document.removeEventListener("keydown", unlockChimeAudio, true);
837
+ try { if (chimeAudioContext !== null && typeof chimeAudioContext.close === "function") chimeAudioContext.close(); } catch (error) { /* ignore */ }
838
+ chimeAudioContext = null;
692
839
  closeThemeMenu();
693
840
  if (themeButton.parentElement !== null) themeButton.remove();
694
841
  themeMenu.remove();
@@ -720,7 +867,16 @@ window.__ModuleLoader__.load({
720
867
  archiveSession: function (id) {
721
868
  var workspaceService = getService("uiWorkspace");
722
869
  return workspaceService.archiveSession(id);
723
- }
870
+ },
871
+ playChime: playCompletionChime,
872
+ chimeInfo: function () {
873
+ return {
874
+ enabled: chimeEnabled,
875
+ attached: typeof chimeUnsubscribe === "function",
876
+ log: chimeLog.slice()
877
+ };
878
+ },
879
+ toggleChime: toggleCompletionSound
724
880
  };
725
881
  } catch (error) {
726
882
  /* debug hook is optional */
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.1.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": {