dsh-multi-chat 0.6.5 → 0.6.7

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.md CHANGED
@@ -9,6 +9,12 @@
9
9
  <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/dsh--plugin-community-brightgreen" alt="dsh-plugin"></a>
10
10
  </p>
11
11
 
12
+ <p align="center">
13
+ <a href="https://awesome-dsh-plugin.com/p/daetz-coder/dsh-multi-chat/"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome-dsh-plugin"></a>
14
+ <a href="https://dshfind.com/plugins/daetz-coder/dsh-multi-chat"><img src="https://dshfind.com/api/badge/daetz-coder/dsh-multi-chat" alt="dshfind"></a>
15
+ <a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://img.shields.io/badge/listed%20on-awesome--dsh--plugin-4d6bfe" alt="listed on awesome-dsh-plugin"></a>
16
+ </p>
17
+
12
18
  > **Run N conversations in DeepSeek Harness at once, watch every Agent's live progress side-by-side, and check in from your phone or tablet.** One browser tab goes from "one conversation at a time" to "a panoramic multi-conversation cockpit."
13
19
 
14
20
  Install a **multi-window wall** into the official [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) Web UI: a grid that shows N running DSH conversation instances simultaneously (each instance runs its own task), so every Agent's live progress, chat, and output are **visible at a glance** — no more hopping between endless tabs and windows.
package/README.zh.md CHANGED
@@ -9,6 +9,12 @@
9
9
  <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/dsh--plugin-community-brightgreen" alt="dsh-plugin"></a>
10
10
  </p>
11
11
 
12
+ <p align="center">
13
+ <a href="https://awesome-dsh-plugin.com/p/daetz-coder/dsh-multi-chat/"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome-dsh-plugin"></a>
14
+ <a href="https://dshfind.com/plugins/daetz-coder/dsh-multi-chat"><img src="https://dshfind.com/api/badge/daetz-coder/dsh-multi-chat" alt="dshfind"></a>
15
+ <a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://img.shields.io/badge/listed%20on-awesome--dsh--plugin-4d6bfe" alt="listed on awesome-dsh-plugin"></a>
16
+ </p>
17
+
12
18
  > **在 DeepSeek Harness 里同时开 N 个对话,并排盯住每一个 Agent 的实时进度,还能用手机/平板躺着看。** 一个浏览器,从「一次一个对话」升级成「全景多对话驾驶舱」。
13
19
 
14
20
  给 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 官方 Web 界面装上一面**多窗口墙**:在一张网格里同时显示 N 个正在运行的 DSH 对话实例(每个实例独立跑一个任务),所有 Agent 的实时进度、对话、输出**一眼尽收**,不用在无数标签页/窗口之间切来切去。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-multi-chat",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "DSH 多对话:在官方 DeepSeek Harness Web 界面里以多窗口网格并排运行、监控多个对话实例,一屏盯住所有 Agent 的实时进度;内置带口令认证的局域网网关,手机/平板也能看。npx dsh-multi-chat install|start|stop|gateway",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -21,6 +21,7 @@ window.__ModuleLoader__.load({
21
21
  "scan.from": "起始端口",
22
22
  "scan.to": "结束端口",
23
23
  "create": "新建窗口",
24
+ "create.pending": "启动中…",
24
25
  "create.done": "已创建窗口 :{port}",
25
26
  "create.failed": "创建失败:{error}",
26
27
  "create.unknown": "未知原因",
@@ -67,6 +68,7 @@ window.__ModuleLoader__.load({
67
68
  "scan.from": "Start port",
68
69
  "scan.to": "End port",
69
70
  "create": "New window",
71
+ "create.pending": "Starting…",
70
72
  "create.done": "Created window :{port}",
71
73
  "create.failed": "Create failed: {error}",
72
74
  "create.unknown": "unknown reason",
@@ -242,27 +244,27 @@ window.__ModuleLoader__.load({
242
244
  document.head.appendChild(tag);
243
245
  }
244
246
  var WallView_module_css_default = {
247
+ "empty": "ouhzTG_empty",
245
248
  "paneActions": "ouhzTG_paneActions",
246
- "linkBar": "ouhzTG_linkBar",
249
+ "paneHead": "ouhzTG_paneHead",
247
250
  "wall": "ouhzTG_wall",
248
- "action": "ouhzTG_action",
249
- "danger": "ouhzTG_danger",
250
- "paneBody": "ouhzTG_paneBody",
251
- "title": "ouhzTG_title",
252
- "hint": "ouhzTG_hint",
253
- "empty": "ouhzTG_empty",
254
- "zoomed": "ouhzTG_zoomed",
255
251
  "field": "ouhzTG_field",
256
- "paneHead": "ouhzTG_paneHead",
257
252
  "status": "ouhzTG_status",
258
- "paneTitle": "ouhzTG_paneTitle",
259
- "grid": "ouhzTG_grid",
253
+ "confirm": "ouhzTG_confirm",
260
254
  "dot": "ouhzTG_dot",
255
+ "title": "ouhzTG_title",
256
+ "linkText": "ouhzTG_linkText",
257
+ "danger": "ouhzTG_danger",
258
+ "paneBody": "ouhzTG_paneBody",
259
+ "controls": "ouhzTG_controls",
260
+ "action": "ouhzTG_action",
261
261
  "pane": "ouhzTG_pane",
262
- "confirm": "ouhzTG_confirm",
262
+ "grid": "ouhzTG_grid",
263
+ "hint": "ouhzTG_hint",
263
264
  "toolbar": "ouhzTG_toolbar",
264
- "controls": "ouhzTG_controls",
265
- "linkText": "ouhzTG_linkText"
265
+ "linkBar": "ouhzTG_linkBar",
266
+ "paneTitle": "ouhzTG_paneTitle",
267
+ "zoomed": "ouhzTG_zoomed"
266
268
  };
267
269
  //#endregion
268
270
  //#region src/client/WallView.tsx
@@ -552,7 +554,7 @@ window.__ModuleLoader__.load({
552
554
  onClick: () => {
553
555
  handleCreate();
554
556
  },
555
- children: t("create")
557
+ children: creating ? t("create.pending") : t("create")
556
558
  }),
557
559
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
558
560
  open: colsMenuOpen,
@@ -681,8 +683,8 @@ window.__ModuleLoader__.load({
681
683
  document.head.appendChild(tag);
682
684
  }
683
685
  var WallToggle_module_css_default = {
684
- "row": "bmjS6q_row",
685
- "label": "bmjS6q_label"
686
+ "label": "bmjS6q_label",
687
+ "row": "bmjS6q_row"
686
688
  };
687
689
  //#endregion
688
690
  //#region src/client/WallToggle.tsx
@@ -692,43 +692,74 @@ function resolveLauncher() {
692
692
  };
693
693
  }
694
694
  /**
695
- * Probe whether a local TCP port is already listening (no HTTP needed).
696
- * @param port - the port to check.
697
- * @returns whether something listens on it.
695
+ * Collect every distinct local TCP port that is listening, in ONE command
696
+ * (not one `netstat`/`lsof` per candidate, which the old free-port scan ran
697
+ * sequentially and could take many seconds on slow Windows boxes). Windows
698
+ * parses `netstat`; POSIX parses `lsof` `(LISTEN)` lines.
699
+ * @returns the set of busy ports.
698
700
  */
699
- async function isPortBusy(port) {
700
- try {
701
- return (await listeningPids(port)).length > 0;
702
- } catch {
703
- return true;
701
+ async function listeningPorts() {
702
+ const set = /* @__PURE__ */ new Set();
703
+ if (process.platform === "win32") {
704
+ const stdout = await execStdout("netstat", [
705
+ "-ano",
706
+ "-p",
707
+ "tcp"
708
+ ]);
709
+ for (const line of stdout.split(/\r?\n/)) {
710
+ const m = /^\s*TCP\s+([0-9.]+|\*|\[::\]):(\d+)\s+\S+\s+LISTENING\s+(\d+)\s*$/.exec(line);
711
+ if (m !== null) set.add(Number(m[2]));
712
+ }
713
+ return set;
714
+ }
715
+ const stdout = await execStdout("lsof", [
716
+ "-nP",
717
+ "-iTCP",
718
+ "-sTCP:LISTEN"
719
+ ]);
720
+ for (const line of stdout.split(/\r?\n/)) {
721
+ const m = /:(\d+)\s+\(LISTEN\)\s*$/.exec(line.trim());
722
+ if (m !== null) set.add(Number(m[1]));
704
723
  }
724
+ return set;
705
725
  }
706
726
  /**
707
727
  * Pick the first free port in [lo, hi] that is neither the serving port nor
708
- * already listening.
728
+ * already listening. The busy set is resolved once (a single command), then
729
+ * scanned in memory.
709
730
  * @param lo - first port of the range.
710
731
  * @param hi - last port of the range.
711
732
  * @param selfPort - the port serving this wall (never chosen).
712
733
  * @returns a free port, or undefined when the range is exhausted.
713
734
  */
714
735
  async function pickFreePort(lo, hi, selfPort) {
736
+ const busy = await listeningPorts();
715
737
  for (let port = lo; port <= hi; port++) {
716
738
  if (port === selfPort) continue;
717
- if (await isPortBusy(port)) continue;
739
+ if (busy.has(port)) continue;
718
740
  return port;
719
741
  }
720
742
  }
721
743
  /**
722
- * Spawn a new `dsh web` instance on a port and wait until it serves the DSH
723
- * shell (probe). Detached so it outlives this process. The child's stderr is
724
- * captured and quoted into every failure, so a crash or a bad bin surfaces a
725
- * concrete reason instead of a bare timeout.
744
+ * Spawn a new `dsh web` instance on a port and decide quickly whether it is
745
+ * viable. Detached so it outlives this process. This is intentionally NOT a
746
+ * full readiness gate: the wall polls liveness itself, so the create response
747
+ * returns fast and a still-booting instance simply shows a pane that lights up
748
+ * when the server finishes. A short bounded wait still catches immediate
749
+ * spawn failures (bad bin, ENOENT) and very fast boots.
750
+ *
751
+ * On a genuine launch failure the child is killed so no orphan lingers; on a
752
+ * slow-but-viable start the deadline returns ok:true and the child keeps
753
+ * booting in the background (the pane already mounted, liveness confirms when
754
+ * alive). The child's stderr is captured and quoted into every failure so a
755
+ * crash or a bad bin surfaces a concrete reason instead of a bare timeout.
726
756
  * @param launcher - how to spawn the dsh CLI.
727
757
  * @param port - the port for the new instance.
728
- * @param timeoutMs - how long to wait for readiness.
758
+ * @param timeoutMs - how long to wait before handing back ok:true.
759
+ * @param pollMs - readiness probe interval while waiting.
729
760
  * @returns ok plus the port, or ok:false with a reason.
730
761
  */
731
- async function startInstance(launcher, port, timeoutMs = 2e4) {
762
+ async function startInstance(launcher, port, timeoutMs = 3e3, pollMs = 300) {
732
763
  const child = spawn(launcher.file, [...launcher.args, String(port)], {
733
764
  detached: true,
734
765
  stdio: [
@@ -755,11 +786,16 @@ async function startInstance(launcher, port, timeoutMs = 2e4) {
755
786
  };
756
787
  const deadline = Date.now() + timeoutMs;
757
788
  for (;;) {
758
- if (spawnFailure.error !== null) return {
759
- ok: false,
760
- port,
761
- error: `new instance failed to start: ${spawnFailure.error.message}`
762
- };
789
+ if (spawnFailure.error !== null) {
790
+ try {
791
+ child.kill();
792
+ } catch {}
793
+ return {
794
+ ok: false,
795
+ port,
796
+ error: `new instance failed to start: ${spawnFailure.error.message}`
797
+ };
798
+ }
763
799
  if (child.exitCode !== null) return {
764
800
  ok: false,
765
801
  port,
@@ -770,11 +806,10 @@ async function startInstance(launcher, port, timeoutMs = 2e4) {
770
806
  port
771
807
  };
772
808
  if (Date.now() > deadline) return {
773
- ok: false,
774
- port,
775
- error: `instance did not become ready in time${detail()}`
809
+ ok: true,
810
+ port
776
811
  };
777
- await new Promise((resolve) => setTimeout(resolve, 400));
812
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
778
813
  }
779
814
  }
780
815
  /**
@@ -10,6 +10,7 @@ export declare const zh: {
10
10
  'scan.from': string;
11
11
  'scan.to': string;
12
12
  create: string;
13
+ 'create.pending': string;
13
14
  'create.done': string;
14
15
  'create.failed': string;
15
16
  'create.unknown': string;
@@ -24,6 +25,7 @@ export declare const zh: {
24
25
  'link.hint': string;
25
26
  'link.reachable': string;
26
27
  'link.unreachable': string;
28
+ 'link.token': string;
27
29
  columns: string;
28
30
  'columns.auto': string;
29
31
  refresh: string;
@@ -57,6 +59,7 @@ export declare const en: {
57
59
  'scan.from': string;
58
60
  'scan.to': string;
59
61
  create: string;
62
+ 'create.pending': string;
60
63
  'create.done': string;
61
64
  'create.failed': string;
62
65
  'create.unknown': string;
@@ -71,6 +74,7 @@ export declare const en: {
71
74
  'link.hint': string;
72
75
  'link.reachable': string;
73
76
  'link.unreachable': string;
77
+ 'link.token': string;
74
78
  columns: string;
75
79
  'columns.auto': string;
76
80
  refresh: string;
@@ -24,11 +24,15 @@ export interface CreateRow {
24
24
  export interface LinkRow {
25
25
  port: number;
26
26
  host: string;
27
- /** LAN URLs (`http://<lan-ip>:<port>/`), possibly empty. */
27
+ /** LAN URLs (`http://<lan-ip>:<gateway-port>/`), possibly empty. */
28
28
  lan: string[];
29
- /** Whether the instance is reachable off-loopback (binds 0.0.0.0). */
29
+ /** Whether the instance is reachable off-loopback (gateway running). */
30
30
  reachable: boolean;
31
- /** Human hint when not reachable (restart / tunnel guidance). */
31
+ /** The inline gateway's listen port (when started). */
32
+ gatewayPort?: number;
33
+ /** The login token (generated or configured). */
34
+ token?: string;
35
+ /** Human hint when not reachable. */
32
36
  hint?: string;
33
37
  }
34
38
  /** The inject face delivered to the wall view registration. */
@@ -26,6 +26,16 @@ export interface MultiWallConfig {
26
26
  * answers `{ lan: [publicUrl + '/'], reachable: true }`.
27
27
  */
28
28
  publicUrl?: string;
29
+ /**
30
+ * Gateway listen port for the inline phone-access gateway. `0` (default)
31
+ * means `targetPort + 5000`.
32
+ */
33
+ gatewayPort?: number;
34
+ /**
35
+ * Optional fixed login token for the inline gateway. Empty (default) means
36
+ * a random token is generated per gateway start (returned by /multi/api/link).
37
+ */
38
+ gatewayToken?: string;
29
39
  }
30
40
  /** Schema-validated config (the Loader resolves defaults for absent keys). */
31
41
  export declare const Config: z<Schemastery.ObjectS<{
@@ -33,11 +43,15 @@ export declare const Config: z<Schemastery.ObjectS<{
33
43
  scanTo: z<number, number>;
34
44
  ports: z<number[], number[]>;
35
45
  publicUrl: z<string, string>;
46
+ gatewayPort: z<number, number>;
47
+ gatewayToken: z<string, string>;
36
48
  }>, Schemastery.ObjectT<{
37
49
  scanFrom: z<number, number>;
38
50
  scanTo: z<number, number>;
39
51
  ports: z<number[], number[]>;
40
52
  publicUrl: z<string, string>;
53
+ gatewayPort: z<number, number>;
54
+ gatewayToken: z<string, string>;
41
55
  }>>;
42
56
  /** One stop result row from /multi/api/stop. */
43
57
  export interface StopRow {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-multi-wall",
3
3
  "description": "Multi-window wall surface: a sidebar footer action that opens a full-screen wall of every running DSH instance (one pane per port), inside the official web GUI. Additive slots only — no existing row or interaction logic is changed.",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },