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 +6 -1
- package/README.md +11 -3
- package/lib/client.js +450 -4
- package/package.json +9 -4
package/README.en.md
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
# dsh-claude-workbench
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-claude-workbench) [](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
|

|
|
11
15
|

|
|
12
16
|

|
|
13
17
|

|
|
18
|
+

|
|
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
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-claude-workbench) [](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
|

|
|
11
17
|

|
|
@@ -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.
|
|
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,
|
|
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
|