dsh-split-view 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +26 -22
  2. package/lib/client.js +38 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,61 +1,65 @@
1
- # dsh-split-view
1
+ # dsh-split-view(分屏插件)
2
2
 
3
- 把 DeepSeek Harness 主窗口变成终端式分屏,横竖随便拆,每一格都是一整个对话,分屏、关闭、最大化全程不重载。
3
+ 一个插件把 DeepSeek Harness 主窗口变成多个分屏,同时查看多个会话的状态。
4
4
 
5
- 你有没有过这种经历。一个 agent 正在跑任务,你想同时在旁边再起一个,可官方 web 界面一个窗口只能放一个会话。开新标签页吧,两边各跑各的,切来切去心累;等任务跑完想回头看一眼,又得在一堆标签里翻。你希望它像终端,旁边拆一格,各干各的,抬眼全在。
5
+ 有过【同时跑多个任务】经历的同学应该能感受这种痛苦:多个标签切来切去,很心累。
6
6
 
7
- dsh-split-view 就是干这个的。装上之后主窗口变成一个可拖拽的分屏容器,每一格里跑一个完整的 DSH 客户端。
7
+ dsh-split-view 分屏插件借鉴了终端分屏的思想。将 DSH 主窗口变成一个可拖拽的分屏容器,每个分屏里跑一个完整的 DSH 客户端。
8
8
 
9
9
  ## 长什么样
10
10
 
11
- 截图待补,先说行为。
11
+ ![分屏实录](demo.gif)
12
12
 
13
- - 横竖任意嵌套分屏,可拖动调整大小
14
- - 每个面板一条标题栏,左边实时显示当前会话标题,右边五个按钮,向右分屏、向下分屏、放大还原、刷新、关闭
15
- - 一键复制「会话 id + 会话标题」
13
+ 上面是日常操作:向右分屏、向下分屏、拖动分割条调整大小、双击标题栏放大、关闭。
16
14
 
17
- ## 安装
18
15
 
19
- npm 包还没发布,先从源码装。
16
+
17
+ ![两分屏](demo2.png)
18
+
19
+
20
+
21
+ ![三分屏](demo3.png)
22
+
23
+ 横竖任意嵌套。每个面板一条标题栏,左边实时显示当前会话标题,右边是向右分屏、向下分屏、放大还原、刷新、关闭五个按钮。点标题直接改名,双击放大,还能一键复制「会话 id + 会话标题」。
24
+
25
+ ## 安装
20
26
 
21
27
  ```sh
22
- dsh plugin --profile web add -w /path/to/dsh-split-view
28
+ dsh plugin --profile web add dsh-split-view
23
29
  dsh web # 重启生效
24
30
  ```
25
31
 
26
- `-w` 在第一次 add 时必须带。每个 profile 自带 `pnpm-workspace.yaml`,pnpm 会把它当成 workspace 根,裸加会报 `ERR_PNPM_ADDING_TO_ROOT`。
27
-
28
- 发布之后一条命令就行,`dsh plugin --profile web add dsh-split-view`(CLI 安装要重启 dsh web 生效)。也可以在插件市场(dshmarket)里搜索 dsh-split-view 一键安装:市场会把插件热挂载进运行中的实例,不用重启,页面刷新一次即可。
32
+ 本地开发从源码装:`dsh plugin --profile web add -w /path/to/dsh-split-view`。
29
33
 
30
34
  卸载用 `dsh plugin --profile web remove dsh-split-view`,重启恢复单窗口。插件本身如果出问题,boot 会停下来报错,不会静默白屏,所以卸载也就是紧急恢复的办法。
31
35
 
32
- ## 快捷键
36
+ ## 默认快捷键(支持在设置中自定义)
33
37
 
34
38
  | 键(macOS ⌘,Windows/Linux Ctrl) | 动作 |
35
39
  |---|---|
36
- | ⇧⌘H | 向右分屏 |
40
+ | ⇧⌘D | 向右分屏 |
37
41
  | ⇧⌘V | 向下分屏 |
38
42
  | ⇧⌘X | 关闭聚焦面板 |
39
43
  | ⇧⌘Enter | 最大化 / 还原聚焦面板 |
40
44
 
41
- 这几个组合是挑出来的,⌘W 关页、⌘D 书签、⌘V 粘贴这些浏览器保留键页面拦不住。关闭键原本用过 ⌘⇧Q,会把浏览器锁死;换 ⌘⇧W,又被飞书的全局热键占了;最后落在 X,就当它是 ✕。都是默认值,可以在设置里改(见下节)。
42
45
 
43
- 焦点在 iframe 里时快捷键照样有效,面板内的 agent 捕获后转发给顶层。标题栏按钮做的是同一批动作,tooltip 里的写法按平台来,macOS 显示 ⌘⇧,其余显示 Ctrl+Shift+。点标题栏任意位置就是聚焦那个面板。面板超过 4 秒没就绪会挂一个「⚠ 未就绪 · 重载」角标,点一下单独重载它。
44
46
 
45
47
  ## 设置
46
48
 
47
- 在任意面板里打开设置,左侧导航有一项「分屏」。四组设置:
49
+ 在任意面板里打开设置,左侧导航有一项「分屏」。
48
50
 
49
- - **快捷键**:点某个动作的组合键进入录制,按新组合完成改绑,Esc 取消。录制时会拦掉不安全的组合——不带 ⌘/Ctrl 的、单个字母不带 Shift/Alt 的(浏览器保留区)、⌘⇧W/Q/T/N 这类有前科或被系统占用的、以及和别的动作撞车的,都会给出提示并拒绝。
51
+ 四组设置:
52
+
53
+ - **快捷键**:点某个动作的组合键进入录制,按新组合完成改绑,Esc 取消。绑什么由你决定:被浏览器或系统占用的组合、和别的动作撞车的组合照样能绑,只会给出提示;唯一的硬性要求是得带 ⌘/Ctrl。
50
54
  - **选中高亮色**:聚焦面板的描边和标题栏染色。预设色板、自定义取色器,外加「跟随主题」——跟随当前皮肤的品牌色 token,换皮肤自动跟着变。
51
55
  - **标题栏**:一个开关,隐藏所有面板的标题栏。注意标题栏上有复制会话信息、分屏、最大化、刷新、关闭按钮,双击放大、点标题重命名也走标题栏,隐藏后这些动作只能靠快捷键;点面板聚焦不受影响。
52
56
  - **恢复默认**:一键把上面全部还原,点之前会弹确认。
53
57
 
54
- 设置的存储边界和布局一致:真源在顶层窗口的 localStorage(键 `dsh-split:prefs`),因为画标题栏、描边、分屏容器的都是顶层。面板里的设置面板改完通过 postMessage 交给顶层校验、落盘,再广播回所有面板——跨 origin 的面板也走这条路,启动时还会拉一次全量。所以不管在哪个面板里改、改完立刻全局生效,刷新也不丢。
58
+
55
59
 
56
60
  ## 供其它插件调用
57
61
 
58
- 分屏能力整体暴露成了一个 Cordis 服务:面板操作、面板×会话操作(加载指定会话、发消息、改名、换模型、取消回合)、查询与订阅。面向集成方的完整契约——服务 API、URL 契约(给做原生壳的人)与已知限制——见 [api.md](./api.md)。
62
+ 分屏能力整体暴露成了一个 Cordis 服务:面板操作、面板×会话操作(加载指定会话、发消息、改名、换模型、取消回合)、查询与订阅。面向集成方的服务 API 见 [api.md](./api.md)。
59
63
 
60
64
  ## 协议
61
65
 
package/lib/client.js CHANGED
@@ -427,13 +427,16 @@ function collectSyncedStorage() {
427
427
  // tab, ⌘D bookmark, ⌘V paste…); ⌘⇧Q locked the browser and ⌘⇧W is
428
428
  // taken by Feishu's global hotkey — X reads as ✕. Mod+Shift+letter is
429
429
  // the space a page can actually capture. ⌘⇧Enter keeps maximize/restore.
430
+ // split-right sits on D so the whole chord is a one-left-hand press;
431
+ // note ⌘⇧D is reserved by some browsers ("bookmark all tabs") but is
432
+ // capturable in chrome-less shells such as the native WKWebView app.
430
433
  /** localStorage key holding user preferences (persisted by the top window). */
431
434
  const PREFS_KEY = "dsh-split:prefs";
432
435
  /** Locale namespace of the settings section and its rows. */
433
436
  const SETTINGS_NS = "settings.splitView";
434
437
  const ACTION_IDS = ["split-right", "split-down", "close", "maximize"];
435
438
  const DEFAULT_SHORTCUTS = Object.freeze({
436
- "split-right": "Mod+Shift+H",
439
+ "split-right": "Mod+Shift+D",
437
440
  "split-down": "Mod+Shift+V",
438
441
  "close": "Mod+Shift+X",
439
442
  "maximize": "Mod+Shift+Enter"
@@ -456,7 +459,7 @@ function collectSyncedStorage() {
456
459
  return event.key.toLowerCase();
457
460
  }
458
461
 
459
- /** Parse a serialized combo ("Mod+Shift+H"). Mod (matched as ⌘ OR Ctrl on
462
+ /** Parse a serialized combo ("Mod+Shift+D"). Mod (matched as ⌘ OR Ctrl on
460
463
  * every platform — the same unification the original fixed map used) is
461
464
  * mandatory; returns {shift, alt, key} or null. */
462
465
  function parseCombo(text) {
@@ -502,7 +505,7 @@ function collectSyncedStorage() {
502
505
  return null;
503
506
  }
504
507
 
505
- /** Platform-aware label for tooltips and the settings row (⌘⇧H / Ctrl+Shift+H). */
508
+ /** Platform-aware label for tooltips and the settings row (⌘⇧D / Ctrl+Shift+D). */
506
509
  function comboLabel(text) {
507
510
  const parsed = parseCombo(text);
508
511
  if (!parsed) return "—";
@@ -511,19 +514,22 @@ function collectSyncedStorage() {
511
514
  return "Ctrl+" + (parsed.shift ? "Shift+" : "") + (parsed.alt ? "Alt+" : "") + key;
512
515
  }
513
516
 
514
- /** Recording-time validation. Returns a locale error key (resolved under
515
- * "shortcuts.*" by the caller) or null when the combo is safe to bind. */
516
- function validateRecorded(parsed, map, actionId) {
517
- // Bare ⌘/Ctrl+letter is browser-reserved territory (⌘W closes the
518
- // tab, ⌘D bookmarks…) — the page never sees those events.
519
- if (parsed.key.length === 1 && !parsed.shift && !parsed.alt) return "err.letter";
517
+ /** Recording-time advisory. Returns a locale warning key (resolved under
518
+ * "shortcuts.*" by the caller) or null. Every combo is bindable — the
519
+ * caller always binds — the warning only tells the user the binding may
520
+ * be shadowed by the browser, the OS, or another action; the choice is
521
+ * theirs. */
522
+ function recordedWarning(parsed, map, actionId) {
523
+ // Bare ⌘/Ctrl+letter is usually browser-reserved (⌘W closes the
524
+ // tab, ⌘D bookmarks…) — the page may never see those events.
525
+ if (parsed.key.length === 1 && !parsed.shift && !parsed.alt) return "warn.letter";
520
526
  // Combinations with a painful history or a browser/system reservation
521
527
  // (⌘⇧Q locked the browser, ⌘⇧W closes all windows / is Feishu's
522
528
  // global hotkey, ⌘⇧T reopens tabs, ⌘⇧N opens private windows).
523
- if (parsed.shift && !parsed.alt && (parsed.key === "w" || parsed.key === "q" || parsed.key === "t" || parsed.key === "n")) return "err.danger";
529
+ if (parsed.shift && !parsed.alt && (parsed.key === "w" || parsed.key === "q" || parsed.key === "t" || parsed.key === "n")) return "warn.danger";
524
530
  for (const other of ACTION_IDS) {
525
531
  if (other === actionId) continue;
526
- if (combosEqual(parseCombo(map[other]), parsed)) return "err.dup";
532
+ if (combosEqual(parseCombo(map[other]), parsed)) return "warn.dup";
527
533
  }
528
534
  return null;
529
535
  }
@@ -534,12 +540,10 @@ function collectSyncedStorage() {
534
540
  const source = raw && typeof raw === "object" ? raw : {};
535
541
  const rawShortcuts = source.shortcuts && typeof source.shortcuts === "object" ? source.shortcuts : {};
536
542
  const shortcuts = {};
537
- const claimed = [];
538
543
  for (const cmd of ACTION_IDS) {
539
544
  let parsed = parseCombo(rawShortcuts[cmd]);
540
- if (!parsed || claimed.some((c) => combosEqual(c, parsed))) parsed = parseCombo(DEFAULT_SHORTCUTS[cmd]);
545
+ if (!parsed) parsed = parseCombo(DEFAULT_SHORTCUTS[cmd]);
541
546
  shortcuts[cmd] = comboToText(parsed);
542
- claimed.push(parsed);
543
547
  }
544
548
  let focusColor = DEFAULT_PREFS.focusColor;
545
549
  if (source.focusColor === "theme") focusColor = "theme";
@@ -568,8 +572,6 @@ function collectSyncedStorage() {
568
572
  if (text === undefined) continue;
569
573
  const parsed = parseCombo(text);
570
574
  if (!parsed) continue;
571
- const collides = ACTION_IDS.some((other) => other !== cmd && combosEqual(parseCombo(next.shortcuts[other]), parsed));
572
- if (collides) continue;
573
575
  const nextText = comboToText(parsed);
574
576
  if (next.shortcuts[cmd] !== nextText) {
575
577
  next.shortcuts[cmd] = nextText;
@@ -610,11 +612,10 @@ function collectSyncedStorage() {
610
612
  "shortcuts.record": "按新的组合键…",
611
613
  "shortcuts.recordHint": "按 Esc 取消",
612
614
  "shortcuts.reset": "恢复默认",
613
- "shortcuts.hint": "组合键需包含 ⌘/Ctrl;单个字母或数字需搭配 Shift 或 Alt,否则会被浏览器保留。个别组合可能仍被浏览器或系统全局热键占用,遇到无效换一个即可。",
614
615
  "shortcuts.err.noMod": "组合键需包含 ⌘ 或 Ctrl",
615
- "shortcuts.err.letter": "字母/数字键需搭配 Shift 或 Alt,否则会被浏览器保留",
616
- "shortcuts.err.danger": "该组合被浏览器或系统保留(例如会关掉所有窗口),请换一个",
617
- "shortcuts.err.dup": "该组合已被其他动作使用",
616
+ "shortcuts.warn.letter": "已绑定。单个字母/数字不带 Shift 或 Alt 的组合(如 ⌘D)通常被浏览器保留,页面可能捕捉不到",
617
+ "shortcuts.warn.danger": "已绑定。该组合可能被浏览器或系统全局热键占用(如 ⌘⇧W 会关掉所有窗口),没反应就换一个",
618
+ "shortcuts.warn.dup": "已绑定。该组合与另一个动作的快捷键相同,按下时列表里靠前的动作响应",
618
619
  "color.title": "选中高亮色",
619
620
  "color.theme": "跟随主题",
620
621
  "color.pick": "自定义…",
@@ -639,11 +640,10 @@ function collectSyncedStorage() {
639
640
  "shortcuts.record": "Press a new combo…",
640
641
  "shortcuts.recordHint": "Esc to cancel",
641
642
  "shortcuts.reset": "Reset to defaults",
642
- "shortcuts.hint": "Combos need ⌘/Ctrl; a bare letter or digit also needs Shift or Alt, otherwise the browser keeps it. A few combos may still be taken by the browser or global hotkeys — pick another if one does nothing.",
643
643
  "shortcuts.err.noMod": "The combo needs ⌘ or Ctrl",
644
- "shortcuts.err.letter": "Letters/digits need Shift or Alt, otherwise the browser keeps them",
645
- "shortcuts.err.danger": "That combo is reserved by the browser or the OS (it would e.g. close all windows) — pick another",
646
- "shortcuts.err.dup": "That combo is already bound to another action",
644
+ "shortcuts.warn.letter": "Bound. A bare letter/digit without Shift or Alt (e.g. ⌘D) is usually kept by the browser, so the page may never see it",
645
+ "shortcuts.warn.danger": "Bound. This combo may be taken by the browser or an OS global hotkey (e.g. ⌘⇧W closes all windows) — switch if it does nothing",
646
+ "shortcuts.warn.dup": "Bound. This combo is shared with another action; the action listed earlier responds on keypress",
647
647
  "color.title": "Focus Highlight Color",
648
648
  "color.theme": "Follow theme",
649
649
  "color.pick": "Custom…",
@@ -995,6 +995,7 @@ function collectSyncedStorage() {
995
995
  title: { color: "var(--dsw-alias-label-primary)", fontSize: "14px", fontWeight: 400, lineHeight: "22px" },
996
996
  hint: { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px", lineHeight: "18px" },
997
997
  error: { color: "var(--dsw-alias-state-error-primary,#e5484d)", fontSize: "12px", lineHeight: "18px" },
998
+ warning: { color: "var(--dsw-alias-state-warning-primary,#b8860b)", fontSize: "12px", lineHeight: "18px" },
998
999
  actionRow: { display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" },
999
1000
  kvRow: { display: "flex", alignItems: "center", gap: "10px" },
1000
1001
  kvLabel: { color: "var(--dsw-alias-label-secondary)", fontSize: "13px", lineHeight: "20px" },
@@ -1109,12 +1110,16 @@ function collectSyncedStorage() {
1109
1110
  }
1110
1111
 
1111
1112
  /** Shortcut row: one recording chip per action. Click a chip, press the
1112
- * new combination (Esc cancels); reserved/duplicate combos are rejected
1113
- * with an inline error instead of binding. */
1113
+ * new combination (Esc cancels). Every combo is bindable; combos that
1114
+ * the browser/OS is likely to keep, or that collide with another
1115
+ * action, are accepted with an inline warning instead of rejected.
1116
+ * Only a missing ⌘/Ctrl stays a hard error (the runtime never fires
1117
+ * without it). */
1114
1118
  function ShortcutsRow({ t, useStore, setShortcut }) {
1115
1119
  const prefs = useStore((s) => s.prefs);
1116
1120
  const [recording, setRecording] = React.useState(null);
1117
1121
  const [error, setError] = React.useState(null);
1122
+ const [warning, setWarning] = React.useState(null);
1118
1123
  const prefsRef = React.useRef(prefs);
1119
1124
  prefsRef.current = prefs;
1120
1125
  React.useEffect(() => {
@@ -1134,14 +1139,11 @@ function collectSyncedStorage() {
1134
1139
  return;
1135
1140
  }
1136
1141
  const parsed = { shift: event.shiftKey, alt: event.altKey, key };
1137
- const errKey = validateRecorded(parsed, prefsRef.current.shortcuts, recording);
1138
- if (errKey !== null) {
1139
- setError(t("shortcuts." + errKey));
1140
- return;
1141
- }
1142
1142
  setShortcut(recording, comboToText(parsed));
1143
1143
  setRecording(null);
1144
1144
  setError(null);
1145
+ const warnKey = recordedWarning(parsed, prefsRef.current.shortcuts, recording);
1146
+ setWarning(warnKey === null ? null : t("shortcuts." + warnKey));
1145
1147
  };
1146
1148
  window.addEventListener("keydown", onKey, true);
1147
1149
  return () => window.removeEventListener("keydown", onKey, true);
@@ -1159,15 +1161,17 @@ function collectSyncedStorage() {
1159
1161
  "aria-pressed": recording === cmd ? "true" : undefined,
1160
1162
  onClick: () => {
1161
1163
  setError(null);
1164
+ setWarning(null);
1162
1165
  setRecording(recording === cmd ? null : cmd);
1163
1166
  }
1164
1167
  }, recording === cmd ? t("shortcuts.record") : comboLabel(prefs.shortcuts[cmd]))
1165
1168
  )),
1166
1169
  React.createElement("div", { style: settingsStyles.actionRow },
1167
- error !== null ? React.createElement("span", { style: settingsStyles.error }, error) : null,
1170
+ error !== null
1171
+ ? React.createElement("span", { style: settingsStyles.error }, error)
1172
+ : warning !== null && recording === null ? React.createElement("span", { style: settingsStyles.warning }, warning) : null,
1168
1173
  recording !== null ? React.createElement("span", { style: settingsStyles.hint }, t("shortcuts.recordHint")) : null
1169
- ),
1170
- React.createElement("div", { style: settingsStyles.hint }, t("shortcuts.hint"))
1174
+ )
1171
1175
  );
1172
1176
  }
1173
1177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-split-view",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Multi-session split view for the DeepSeek Harness web GUI: the top window shadows the root slot with a resizable split container; each pane is an iframe running a full DSH client pinned to its own session, with per-pane title bars, LRU hibernation and a dual-loopback origin pool.",
5
5
  "license": "MIT",
6
6
  "type": "module",