dsh-coding-sidebar 1.0.7 → 1.0.8

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 (69) hide show
  1. package/README.md +2 -2
  2. package/lib/client-editor.js +436 -227
  3. package/lib/client-registry.js +627 -7391
  4. package/lib/client-terminal.js +234 -182
  5. package/lib/client.js +634 -7398
  6. package/lib/index.js +367 -67
  7. package/lib/types/agent-pty.d.ts +62 -4
  8. package/lib/types/bundle-route.d.ts +1 -1
  9. package/lib/types/client/EditorHost.d.ts +1 -1
  10. package/lib/types/client/FileTree.d.ts +2 -2
  11. package/lib/types/client/TreePanel.d.ts +1 -1
  12. package/lib/types/client/chunk-loader.d.ts +1 -1
  13. package/lib/types/client/chunks/locale.d.ts +3 -0
  14. package/lib/types/client/conversation-draft.d.ts +85 -4
  15. package/lib/types/client/locales.d.ts +1 -20
  16. package/lib/types/client/selection-popup.d.ts +58 -0
  17. package/lib/types/client/service.d.ts +1 -1
  18. package/lib/types/client/terminal-font.d.ts +25 -2
  19. package/lib/types/context-types.d.ts +3 -1
  20. package/lib/types/index.d.ts +9 -0
  21. package/lib/types/prefs-shared.d.ts +7 -5
  22. package/lib/types/pty-manager.d.ts +53 -0
  23. package/lib/types/wire.d.ts +6 -2
  24. package/package.json +1 -1
  25. package/src/agent-pty.ts +221 -33
  26. package/src/bundle-route.ts +1 -1
  27. package/src/client/EditorHost.tsx +1 -1
  28. package/src/client/FileTree.tsx +4 -4
  29. package/src/client/Sidebar.tsx +41 -22
  30. package/src/client/TerminalView.tsx +13 -0
  31. package/src/client/TextEditor.tsx +27 -45
  32. package/src/client/TreePanel.tsx +16 -1
  33. package/src/client/chunk-loader.ts +1 -1
  34. package/src/client/chunks/locale.tsx +60 -0
  35. package/src/client/conversation-draft.ts +233 -7
  36. package/src/client/index.tsx +19 -8
  37. package/src/client/locales-ar.ts +5 -4
  38. package/src/client/locales-de.ts +5 -4
  39. package/src/client/locales-fr.ts +5 -4
  40. package/src/client/locales-hi.ts +5 -4
  41. package/src/client/locales-id.ts +5 -4
  42. package/src/client/locales-it.ts +5 -4
  43. package/src/client/locales-ja.ts +5 -4
  44. package/src/client/locales-ko.ts +5 -4
  45. package/src/client/locales-nl.ts +5 -4
  46. package/src/client/locales-pl.ts +5 -4
  47. package/src/client/locales-pt.ts +5 -4
  48. package/src/client/locales-ru.ts +5 -4
  49. package/src/client/locales-sv.ts +5 -4
  50. package/src/client/locales-th.ts +5 -4
  51. package/src/client/locales-tr.ts +5 -4
  52. package/src/client/locales-vi.ts +5 -4
  53. package/src/client/locales-zh-HK.ts +5 -4
  54. package/src/client/locales-zh-MO.ts +5 -4
  55. package/src/client/locales-zh-TW.ts +5 -4
  56. package/src/client/locales.ts +17 -51
  57. package/src/client/selection-popup.ts +155 -0
  58. package/src/client/service.ts +1 -1
  59. package/src/client/state.ts +14 -7
  60. package/src/client/terminal-font.ts +34 -3
  61. package/src/context-types.ts +3 -2
  62. package/src/git.ts +17 -1
  63. package/src/index.ts +39 -6
  64. package/src/open-external.ts +3 -4
  65. package/src/prefs-shared.ts +7 -5
  66. package/src/pty-manager.ts +176 -5
  67. package/src/sidechat-routes.ts +13 -1
  68. package/src/tools.ts +24 -6
  69. package/src/wire.ts +3 -0
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { mkdir, open, opendir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
3
- import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
4
4
  import { WebSocket, WebSocketServer } from "ws";
5
5
  import z from "schemastery";
6
6
  import { createHash, randomUUID } from "node:crypto";
@@ -13,6 +13,7 @@ import { homedir, userInfo } from "node:os";
13
13
  import { defineTool } from "@deepseek-ai/dsh-tools";
14
14
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
15
15
  import { snapshotSubagentDescriptor } from "@deepseek-ai/dsh-subagent";
16
+ import { SessionLogOffset } from "@deepseek-ai/dsh-session";
16
17
  //#region src/prefs-shared.ts
17
18
  /**
18
19
  * Shared "Side card" preference vocabulary (types + constants), consumed by
@@ -125,10 +126,12 @@ const PrefsSchema = z.object({
125
126
  var SidebarError = class extends Error {
126
127
  code;
127
128
  status;
128
- constructor(code, message, status = 400) {
129
+ meta;
130
+ constructor(code, message, status = 400, meta) {
129
131
  super(message);
130
132
  this.code = code;
131
133
  this.status = status;
134
+ this.meta = meta;
132
135
  }
133
136
  };
134
137
  /** Body size bound of one JSON request (defense against unbounded reads). */
@@ -632,7 +635,11 @@ function isTrustedApiRequest(request, trustedHosts) {
632
635
  * only allowlisted chunk names are servable (no path traversal).
633
636
  */
634
637
  /** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
635
- const CHUNK_NAMES = ["terminal", "editor"];
638
+ const CHUNK_NAMES = [
639
+ "terminal",
640
+ "editor",
641
+ "locale"
642
+ ];
636
643
  /** Directory of this host-half module (lib/ — the chunk scripts live next to it). */
637
644
  const LIB_DIR = dirname(fileURLToPath(import.meta.url));
638
645
  /** sha1 content hash shortened to 12 hex chars (same shape as the client-modules rev). */
@@ -747,7 +754,7 @@ function revealCommand(path, platform = process.platform) {
747
754
  };
748
755
  case "win32": return {
749
756
  command: "explorer.exe",
750
- args: ["/select,", path]
757
+ args: [`/select,${path}`]
751
758
  };
752
759
  default: return {
753
760
  command: "xdg-open",
@@ -1060,16 +1067,37 @@ function pathIdentity(path) {
1060
1067
  const absolute = resolve(path).replace(/[\\/]+$/, "");
1061
1068
  return process.platform === "win32" ? absolute.toLowerCase() : absolute;
1062
1069
  }
1070
+ /** Whether the current Git binary supports NUL-framed `worktree list` output.
1071
+ * Git < 2.36 rejects `-z`; cache the capability after the first attempt so
1072
+ * the SCM panel's polling does not repeatedly spawn a command known to fail. */
1073
+ let worktreeListSupportsZ;
1063
1074
  /** Raw usable checkout records, shared by inventory and target validation.
1064
1075
  * Prunable records point at missing paths and are deliberately excluded from
1065
1076
  * both the selector and the command-target allowlist. */
1066
1077
  async function listedWorktrees(cwd) {
1067
- return parseWorktreeList(await runGit(cwd, [
1078
+ let raw;
1079
+ if (worktreeListSupportsZ === false) raw = await runGit(cwd, [
1068
1080
  "worktree",
1069
1081
  "list",
1070
- "--porcelain",
1071
- "-z"
1072
- ])).filter((entry) => !entry.prunable);
1082
+ "--porcelain"
1083
+ ]);
1084
+ else try {
1085
+ raw = await runGit(cwd, [
1086
+ "worktree",
1087
+ "list",
1088
+ "--porcelain",
1089
+ "-z"
1090
+ ]);
1091
+ worktreeListSupportsZ = true;
1092
+ } catch {
1093
+ worktreeListSupportsZ = false;
1094
+ raw = await runGit(cwd, [
1095
+ "worktree",
1096
+ "list",
1097
+ "--porcelain"
1098
+ ]);
1099
+ }
1100
+ return parseWorktreeList(raw).filter((entry) => !entry.prunable);
1073
1101
  }
1074
1102
  /** All linked checkouts of the repository containing `cwd`, enriched with a
1075
1103
  * live change count. The current checkout is first so a single-worktree repo
@@ -1484,7 +1512,7 @@ var PtyManager = class {
1484
1512
  sessionId,
1485
1513
  tabId,
1486
1514
  cwd,
1487
- pty: this.nodePty.spawn(shell ?? this.shell, shellSpawnArgs(shellArgs ?? this.shellArgs), {
1515
+ pty: this.nodePty.spawn(resolveShellExecutable(shell ?? this.shell), shellSpawnArgs(shellArgs ?? this.shellArgs), {
1488
1516
  name: "xterm-256color",
1489
1517
  cols: Math.max(2, Math.floor(cols)),
1490
1518
  rows: Math.max(2, Math.floor(rows)),
@@ -1571,6 +1599,15 @@ var PtyManager = class {
1571
1599
  for (const key of [...this.sessions.keys()]) this.close(key);
1572
1600
  }
1573
1601
  };
1602
+ /** Read one Windows environment value case-insensitively. Real
1603
+ * `process.env` has case-insensitive lookup on Windows, but injected objects
1604
+ * and some embedders do not preserve that behavior. */
1605
+ function windowsEnv(env, name) {
1606
+ const direct = env[name];
1607
+ if (direct !== void 0) return direct;
1608
+ const lowered = name.toLowerCase();
1609
+ for (const [key, value] of Object.entries(env)) if (key.toLowerCase() === lowered) return value;
1610
+ }
1574
1611
  /**
1575
1612
  * Candidate directories that may contain a `pwsh.exe` on Windows: PATH
1576
1613
  * entries first, then the well-known machine/user install locations
@@ -1582,17 +1619,17 @@ var PtyManager = class {
1582
1619
  */
1583
1620
  function windowsPwshCandidateDirs(env) {
1584
1621
  const dirs = [];
1585
- const pathEntries = env.PATH;
1622
+ const pathEntries = windowsEnv(env, "PATH");
1586
1623
  if (pathEntries !== void 0) for (const entry of pathEntries.split(";")) {
1587
1624
  const trimmed = entry.trim();
1588
1625
  if (trimmed !== "") dirs.push(trimmed);
1589
1626
  }
1590
- for (const programFiles of [env.ProgramW6432, env.ProgramFiles]) {
1627
+ for (const programFiles of [windowsEnv(env, "ProgramW6432"), windowsEnv(env, "ProgramFiles")]) {
1591
1628
  if (programFiles === void 0 || programFiles.trim() === "") continue;
1592
1629
  dirs.push(join(programFiles, "PowerShell", "7"));
1593
1630
  dirs.push(join(programFiles, "PowerShell", "7-preview"));
1594
1631
  }
1595
- const localAppData = env.LOCALAPPDATA;
1632
+ const localAppData = windowsEnv(env, "LOCALAPPDATA");
1596
1633
  if (localAppData !== void 0 && localAppData.trim() !== "") {
1597
1634
  dirs.push(join(localAppData, "Microsoft", "PowerShell", "7"));
1598
1635
  dirs.push(join(localAppData, "Microsoft", "PowerShell", "7-preview"));
@@ -1602,6 +1639,63 @@ function windowsPwshCandidateDirs(env) {
1602
1639
  return [...new Set(dirs)];
1603
1640
  }
1604
1641
  /**
1642
+ * Resolve the configured shell executable before handing it to node-pty.
1643
+ *
1644
+ * Windows' native backend does not consistently apply the shell's PATHEXT
1645
+ * lookup to a bare value (`pwsh` / `cmd` can fail with the opaque
1646
+ * `File not found:` error), so perform the lookup ourselves: an explicit
1647
+ * path is accepted as-is when it exists (or with a PATHEXT suffix when the
1648
+ * user omitted `.exe`), and a bare name is searched through PATH, System32,
1649
+ * and PowerShell's known install directories.
1650
+ *
1651
+ * POSIX node-pty uses `execvp`, so a bare name would already follow PATH —
1652
+ * but a wrong name made the pty die with a bare
1653
+ * `[process exited with code N]`, so probe like Windows anyway: a path with
1654
+ * a separator must exist; a bare name is searched along PATH (the colon
1655
+ * form is fixed by the platform). A miss is a clear, actionable
1656
+ * `shell-not-found` error instead of a cryptic exit code.
1657
+ *
1658
+ * @param shell - the configured shell (settings page or yaml `config.shell`).
1659
+ * @param options - platform/env/exists injection points for tests.
1660
+ * @returns the executable path passed to node-pty.
1661
+ * @throws {SidebarError} `shell-not-found` when no candidate exists.
1662
+ */
1663
+ function resolveShellExecutable(shell, options = {}) {
1664
+ const configured = unquotePath(shell.trim());
1665
+ if (configured === "") return configured;
1666
+ const platform = options.platform ?? process.platform;
1667
+ const env = options.env ?? process.env;
1668
+ const exists = options.exists ?? existsSync;
1669
+ const notFound = () => new SidebarError("shell-not-found", `shell executable not found: "${configured}"`, 400, { shell: configured });
1670
+ if (platform === "win32") {
1671
+ const executableExts = (windowsEnv(env, "PATHEXT") ?? ".COM;.EXE").split(";").map((extension) => extension.trim()).filter((extension) => /^\.(?:com|exe)$/i.test(extension));
1672
+ if (executableExts.length === 0) executableExts.push(".EXE", ".COM");
1673
+ const names = win32.extname(configured) !== "" ? [configured] : executableExts.map((extension) => configured + extension.toLowerCase());
1674
+ const hasPath = win32.isAbsolute(configured) || /[\\/]/.test(configured);
1675
+ const candidates = [];
1676
+ if (hasPath) candidates.push(...names);
1677
+ else {
1678
+ const path = windowsEnv(env, "PATH");
1679
+ if (path !== void 0) for (const dir of path.split(";").map((entry) => entry.trim()).filter(Boolean)) for (const name of names) candidates.push(win32.join(dir, name));
1680
+ const systemRoot = windowsEnv(env, "SystemRoot");
1681
+ if (systemRoot !== void 0 && systemRoot.trim() !== "") for (const name of names) candidates.push(win32.join(systemRoot, "System32", name));
1682
+ if (/^pwsh(?:\.exe)?$/i.test(configured)) for (const dir of windowsPwshCandidateDirs(env)) candidates.push(win32.join(dir, "pwsh.exe"));
1683
+ }
1684
+ for (const candidate of [...new Set(candidates)]) if (exists(candidate)) return candidate;
1685
+ throw notFound();
1686
+ }
1687
+ if (configured.includes("/")) {
1688
+ if (!exists(configured)) throw notFound();
1689
+ return configured;
1690
+ }
1691
+ const path = env.PATH ?? "/usr/bin:/bin";
1692
+ for (const dir of path.split(":").map((entry) => entry.trim()).filter(Boolean)) {
1693
+ const candidate = join(dir, configured);
1694
+ if (exists(candidate)) return candidate;
1695
+ }
1696
+ throw notFound();
1697
+ }
1698
+ /**
1605
1699
  * The interactive shell for this platform, resolved like a terminal
1606
1700
  * emulator: an explicitly configured shell (the `shell` config field) wins,
1607
1701
  * then `$SHELL` on POSIX (deployment override), then the account's login
@@ -1663,6 +1757,20 @@ function shellSpawnArgs(configured = []) {
1663
1757
  if (configured.length > 0) return [...configured];
1664
1758
  return process.platform === "win32" ? [] : ["-l"];
1665
1759
  }
1760
+ /**
1761
+ * Strip ONE pair of surrounding quotes from a configured shell path. Users
1762
+ * paste Windows paths with spaces pre-quoted (`"C:\Program Files\…"`); the
1763
+ * quotes are shell-input syntax, not part of the path. Unpaired quotes and
1764
+ * shorter values stay verbatim.
1765
+ */
1766
+ function unquotePath(value) {
1767
+ if (value.length >= 2) {
1768
+ const first = value[0];
1769
+ const last = value[value.length - 1];
1770
+ if (first === "\"" && last === "\"" || first === "'" && last === "'") return value.slice(1, -1);
1771
+ }
1772
+ return value;
1773
+ }
1666
1774
  //#endregion
1667
1775
  //#region src/agent-pty.ts
1668
1776
  /**
@@ -1698,6 +1806,61 @@ function clampDims(cols, rows) {
1698
1806
  rows: clamp(rows)
1699
1807
  };
1700
1808
  }
1809
+ /**
1810
+ * node-pty's Windows terminal queues resize calls that arrive before the
1811
+ * ConPTY control socket's first data flush (`_deferNoArgs` in
1812
+ * windowsTerminal.js). If the pty exits before the queue flushes, the
1813
+ * deferred resize throws inside the socket's 'data' handler — uncatchable
1814
+ * by any caller and fatal to the host process. POSIX terminals have no such
1815
+ * queue (resize is synchronous), so the gate is armed on Windows only:
1816
+ * {@link tryResizePty} parks dims requested before the first output and
1817
+ * replays them after the flush, when node-pty executes resizes
1818
+ * synchronously (and the throw for an exited pty is catchable).
1819
+ */
1820
+ const ptyResizeGates = /* @__PURE__ */ new WeakMap();
1821
+ /**
1822
+ * Arm the Windows pre-ready resize gate for one freshly spawned pty.
1823
+ * No-op on POSIX and for injected ptys without `onData`.
1824
+ */
1825
+ function armPtyResizeGate(pty) {
1826
+ if (process.platform !== "win32") return;
1827
+ if (typeof pty.onData !== "function" || ptyResizeGates.has(pty)) return;
1828
+ const state = { sawData: false };
1829
+ ptyResizeGates.set(pty, state);
1830
+ pty.onData(() => {
1831
+ if (state.sawData) return;
1832
+ state.sawData = true;
1833
+ const dims = state.pending;
1834
+ state.pending = void 0;
1835
+ if (dims === void 0) return;
1836
+ setImmediate(() => {
1837
+ tryResizePty(pty, dims.cols, dims.rows);
1838
+ });
1839
+ });
1840
+ }
1841
+ /**
1842
+ * Best-effort resize for WebSocket-driven terminal views. Layout animation
1843
+ * can briefly produce unusable dimensions, and node-pty can reject a resize
1844
+ * after the socket setup's outer try/catch has returned. Ignore that one
1845
+ * frame so the host stays alive and a later valid measurement can retry.
1846
+ * Returns whether node-pty accepted the resize (or parked it for replay on
1847
+ * the first output — the Windows pre-ready window).
1848
+ */
1849
+ function tryResizePty(pty, cols, rows) {
1850
+ if (!Number.isFinite(cols) || !Number.isFinite(rows)) return false;
1851
+ const dims = clampDims(cols, rows);
1852
+ const gate = ptyResizeGates.get(pty);
1853
+ if (gate !== void 0 && !gate.sawData) {
1854
+ gate.pending = dims;
1855
+ return true;
1856
+ }
1857
+ try {
1858
+ pty.resize(dims.cols, dims.rows);
1859
+ return true;
1860
+ } catch {
1861
+ return false;
1862
+ }
1863
+ }
1701
1864
  /** Map a POSIX signal number to its conventional name (best-effort). */
1702
1865
  const SIGNAL_NAMES = {
1703
1866
  1: "SIGHUP",
@@ -1720,20 +1883,54 @@ function signalNameOf(signal) {
1720
1883
  if (signal === null || signal === void 0) return null;
1721
1884
  return SIGNAL_NAMES[signal] ?? `signal ${signal}`;
1722
1885
  }
1723
- /** Locate the first occurrence of `needle` in `transcript`, returning its line/column. */
1724
- function locateNeedle(transcript, needle) {
1886
+ /**
1887
+ * Compile a wait needle into a RegExp. The needle is treated as a JavaScript
1888
+ * regular expression; a pattern that fails to compile (e.g. an unbalanced
1889
+ * group typed as a literal) degrades to verbatim substring matching so
1890
+ * legacy literal needles keep working.
1891
+ */
1892
+ function compileNeedle(needle) {
1893
+ try {
1894
+ return new RegExp(needle);
1895
+ } catch {
1896
+ return null;
1897
+ }
1898
+ }
1899
+ /**
1900
+ * Locate the first occurrence of `needle` in `transcript`, returning its
1901
+ * line/column plus the actual matched text — for alternation patterns
1902
+ * (e.g. `(BUILD_OK|BUILD_FAIL)`) the match tells which alternative hit.
1903
+ * `re` is the precompiled form of `needle` (from {@link compileNeedle});
1904
+ * `null` means verbatim substring matching.
1905
+ */
1906
+ function locateNeedle(transcript, needle, re) {
1725
1907
  if (needle === "") return void 0;
1726
- const idx = transcript.indexOf(needle);
1727
- if (idx === -1) return void 0;
1908
+ let hit;
1909
+ if (re !== null) {
1910
+ re.lastIndex = 0;
1911
+ const m = re.exec(transcript);
1912
+ hit = m === null ? void 0 : {
1913
+ index: m.index,
1914
+ text: m[0]
1915
+ };
1916
+ } else {
1917
+ const idx = transcript.indexOf(needle);
1918
+ hit = idx === -1 ? void 0 : {
1919
+ index: idx,
1920
+ text: needle
1921
+ };
1922
+ }
1923
+ if (hit === void 0) return void 0;
1728
1924
  let line = 0;
1729
1925
  let lineStart = 0;
1730
- for (let i = 0; i < idx; i += 1) if (transcript.charCodeAt(i) === 10) {
1926
+ for (let i = 0; i < hit.index; i += 1) if (transcript.charCodeAt(i) === 10) {
1731
1927
  line += 1;
1732
1928
  lineStart = i + 1;
1733
1929
  }
1734
1930
  return {
1735
1931
  line,
1736
- column: idx - lineStart
1932
+ column: hit.index - lineStart,
1933
+ match: hit.text
1737
1934
  };
1738
1935
  }
1739
1936
  /** Snapshot projection of a handle (drops the pty reference and transcript). */
@@ -1748,6 +1945,11 @@ function snapshotOf(handle) {
1748
1945
  out.exitCode = handle.exitCode ?? null;
1749
1946
  out.exitSignal = signalNameOf(handle.exitSignal);
1750
1947
  }
1948
+ const active = handle.waits.at(-1);
1949
+ if (active !== void 0) out.waiting = {
1950
+ needle: active.needle,
1951
+ since: active.since
1952
+ };
1751
1953
  return out;
1752
1954
  }
1753
1955
  /**
@@ -1779,13 +1981,15 @@ var AgentPtyRegistry = class {
1779
1981
  create(sessionId, title, command, cwd, cols = 80, rows = 24, shell, shellArgs) {
1780
1982
  const uuid = randomUUID();
1781
1983
  const dims = clampDims(cols, rows);
1782
- const pty = this.nodePty.spawn(shell ?? this.shell, shellSpawnArgs(shellArgs ?? this.shellArgs), {
1984
+ const executable = resolveShellExecutable(shell ?? this.shell);
1985
+ const pty = this.nodePty.spawn(executable, shellSpawnArgs(shellArgs ?? this.shellArgs), {
1783
1986
  name: "xterm-256color",
1784
1987
  cols: dims.cols,
1785
1988
  rows: dims.rows,
1786
1989
  cwd,
1787
1990
  env: { ...process.env }
1788
1991
  });
1992
+ armPtyResizeGate(pty);
1789
1993
  const handle = {
1790
1994
  uuid,
1791
1995
  sessionId,
@@ -1794,7 +1998,8 @@ var AgentPtyRegistry = class {
1794
1998
  cwd,
1795
1999
  pty,
1796
2000
  transcript: "",
1797
- exited: false
2001
+ exited: false,
2002
+ waits: []
1798
2003
  };
1799
2004
  pty.onData((data) => {
1800
2005
  handle.transcript += data;
@@ -1878,7 +2083,7 @@ var AgentPtyRegistry = class {
1878
2083
  resize(uuid, cols, rows) {
1879
2084
  const handle = this.expect(uuid);
1880
2085
  const dims = clampDims(cols, rows);
1881
- if (!handle.exited) handle.pty.resize(dims.cols, dims.rows);
2086
+ if (!handle.exited) tryResizePty(handle.pty, dims.cols, dims.rows);
1882
2087
  return dims;
1883
2088
  }
1884
2089
  /**
@@ -1901,10 +2106,17 @@ var AgentPtyRegistry = class {
1901
2106
  * make event-driven wakeups unreliable. A 50ms poll is fast enough for
1902
2107
  * interactive use and simple enough to be obviously correct.
1903
2108
  * @param uuid - terminal to watch.
1904
- * @param needle - substring to search for (case-sensitive, verbatim).
2109
+ * @param needle - JavaScript regular expression to search for
2110
+ * (case-sensitive); a pattern that fails to compile falls back to
2111
+ * verbatim substring matching. May cover several outcomes at once
2112
+ * (e.g. `(BUILD_OK|BUILD_FAIL)` for build success vs failure) — the
2113
+ * returned `match` reports the text that actually matched, so callers
2114
+ * can tell which outcome hit.
1905
2115
  * @param timeoutMs - max wait; default 10000 (10s). Clamped to ≥100ms.
1906
2116
  * @param signal - caller-owned cancellation; aborts the wait re-throwing.
1907
- * @returns one of `found` / `timeout` / `exited`.
2117
+ * A wait can also be skipped by the user from the sidebar banner
2118
+ * (`skipWait`), which resolves it with `{kind:'skipped'}`.
2119
+ * @returns one of `found` / `timeout` / `exited` / `skipped`.
1908
2120
  */
1909
2121
  async waitFor(uuid, needle, timeoutMs = 1e4, signal) {
1910
2122
  if (needle === "") throw new SidebarError("bad-request", "needle must be a non-empty string", 400);
@@ -1912,49 +2124,85 @@ var AgentPtyRegistry = class {
1912
2124
  const timeout = Math.max(100, Math.floor(timeoutMs));
1913
2125
  const start = Date.now();
1914
2126
  const deadline = start + timeout;
2127
+ const re = compileNeedle(needle);
1915
2128
  if (handle.exited) return {
1916
2129
  kind: "exited",
1917
2130
  needle,
1918
2131
  exitCode: handle.exitCode ?? null,
1919
2132
  exitSignal: signalNameOf(handle.exitSignal)
1920
2133
  };
1921
- const firstHit = locateNeedle(handle.transcript, needle);
2134
+ const firstHit = locateNeedle(handle.transcript, needle, re);
1922
2135
  if (firstHit !== void 0) return {
1923
2136
  kind: "found",
1924
2137
  needle,
1925
2138
  line: firstHit.line,
1926
2139
  column: firstHit.column,
2140
+ match: firstHit.match,
1927
2141
  elapsedMs: Date.now() - start
1928
2142
  };
1929
- while (true) {
1930
- if (signal?.aborted) signal.throwIfAborted();
1931
- if (handle.exited) return {
1932
- kind: "exited",
1933
- needle,
1934
- exitCode: handle.exitCode ?? null,
1935
- exitSignal: signalNameOf(handle.exitSignal)
1936
- };
1937
- const hit = locateNeedle(handle.transcript, needle);
1938
- if (hit !== void 0) return {
1939
- kind: "found",
1940
- needle,
1941
- line: hit.line,
1942
- column: hit.column,
1943
- elapsedMs: Date.now() - start
1944
- };
1945
- if (Date.now() >= deadline) return {
1946
- kind: "timeout",
1947
- needle,
1948
- timeoutMs: timeout,
1949
- totalLines: handle.transcript.split("\n").length
1950
- };
1951
- await new Promise((resolve) => {
1952
- const t = setTimeout(resolve, 50);
1953
- if (typeof t === "object" && "unref" in t) t.unref();
1954
- });
2143
+ const record = {
2144
+ needle,
2145
+ since: start,
2146
+ skipped: false
2147
+ };
2148
+ handle.waits.push(record);
2149
+ this.notify();
2150
+ try {
2151
+ while (true) {
2152
+ if (signal?.aborted) signal.throwIfAborted();
2153
+ if (handle.exited) return {
2154
+ kind: "exited",
2155
+ needle,
2156
+ exitCode: handle.exitCode ?? null,
2157
+ exitSignal: signalNameOf(handle.exitSignal)
2158
+ };
2159
+ if (record.skipped) return {
2160
+ kind: "skipped",
2161
+ needle
2162
+ };
2163
+ const hit = locateNeedle(handle.transcript, needle, re);
2164
+ if (hit !== void 0) return {
2165
+ kind: "found",
2166
+ needle,
2167
+ line: hit.line,
2168
+ column: hit.column,
2169
+ match: hit.match,
2170
+ elapsedMs: Date.now() - start
2171
+ };
2172
+ if (Date.now() >= deadline) return {
2173
+ kind: "timeout",
2174
+ needle,
2175
+ timeoutMs: timeout,
2176
+ totalLines: handle.transcript.split("\n").length
2177
+ };
2178
+ await new Promise((resolve) => {
2179
+ const t = setTimeout(resolve, 50);
2180
+ if (typeof t === "object" && "unref" in t) t.unref();
2181
+ });
2182
+ }
2183
+ } finally {
2184
+ const index = handle.waits.indexOf(record);
2185
+ if (index !== -1) handle.waits.splice(index, 1);
2186
+ this.notify();
1955
2187
  }
1956
2188
  }
1957
2189
  /**
2190
+ * Mark every active wait on one terminal as skipped (the sidebar banner's
2191
+ * skip button). Each waiting poll loop observes its record's flag within
2192
+ * one 50ms tick and returns `{kind:'skipped'}`. Idempotent: 0 when nothing
2193
+ * is waiting (a stale banner racing a wait that already resolved).
2194
+ * @returns the number of waits that transitioned to skipped.
2195
+ */
2196
+ skipWait(uuid) {
2197
+ const handle = this.expect(uuid);
2198
+ let count = 0;
2199
+ for (const record of handle.waits) if (!record.skipped) {
2200
+ record.skipped = true;
2201
+ count += 1;
2202
+ }
2203
+ return count;
2204
+ }
2205
+ /**
1958
2206
  * Send a POSIX signal to a terminal's foreground process.
1959
2207
  *
1960
2208
  * Two delivery paths, by signal kind:
@@ -2327,7 +2575,7 @@ function registerTools(ctx, registry, resolveCwd, readShellOverrides) {
2327
2575
  }));
2328
2576
  register(defineTool({
2329
2577
  name: "terminal_wait_for",
2330
- description: "Block until a substring appears in a terminal's retained transcript, or until the timeout elapses, or until the terminal exits — whichever happens first. Use this to synchronize on command completion cues ( e.g. a shell prompt, \"done\", \"Listening on\", \"Build successful\" ) without busy-polling terminal_read. The wait scans the FULL retained transcript (up to ~1 MiB) on every poll, so a needle that scrolled past the most recent chunk is still a match. Returns `found` with the line/column of the first occurrence, `timeout` if the needle did not appear in time, or `exited` if the terminal process died before the needle appeared. Default timeout is 10 seconds; raise it for long-running commands ( dev servers, test suites ). The wait is cooperative: a tool-call cancel ( or agent turn end ) aborts it immediately.",
2578
+ description: "Block until a pattern appears in a terminal's retained transcript, or until the timeout elapses, or until the terminal exits — whichever happens first. Use this to synchronize on command completion cues ( e.g. a shell prompt, \"done\", \"Listening on\", \"Build successful\" ) without busy-polling terminal_read. The needle is a JavaScript regular expression ( a pattern that fails to compile falls back to verbatim substring matching ). One needle may cover MULTIPLE outcomes — e.g. wait on `(BUILD_OK|BUILD_FAIL)` or `Build (succeeded|failed)` returns as soon as EITHER marker appears, and the found result's `match` field tells which alternative hit ( build success vs failure ). The wait scans the FULL retained transcript (up to ~1 MiB) on every poll, so a needle that scrolled past the most recent chunk is still a match. Returns `found` with the line/column and the matched text, `timeout` if the needle did not appear in time, or `exited` if the terminal process died before the needle appeared. Default timeout is 10 seconds; raise it for long-running commands ( dev servers, test suites ). The wait is cooperative: a tool-call cancel ( or agent turn end ) aborts it immediately. The user can skip the wait from the sidebar ( a banner on the terminal's tab shows the needle and a skip button ) — the tool then returns `skipped`.",
2331
2579
  parameters: {
2332
2580
  uuid: {
2333
2581
  type: "string",
@@ -2337,7 +2585,7 @@ function registerTools(ctx, registry, resolveCwd, readShellOverrides) {
2337
2585
  needle: {
2338
2586
  type: "string",
2339
2587
  required: true,
2340
- description: "Substring to wait for (case-sensitive, verbatim). Must be non-empty."
2588
+ description: "JavaScript regular expression to wait for (case-sensitive); a pattern that fails to compile falls back to verbatim substring matching. May cover several outcomes in one wait ( e.g. `(BUILD_OK|BUILD_FAIL)` for build success/failure ) — check `match` in the found result to see which one hit. Must be non-empty."
2341
2589
  },
2342
2590
  timeout_ms: {
2343
2591
  type: "number",
@@ -2369,6 +2617,11 @@ function registerTools(ctx, registry, resolveCwd, readShellOverrides) {
2369
2617
  required: true,
2370
2618
  description: "0-based column index within that line where the match starts."
2371
2619
  },
2620
+ match: {
2621
+ type: "string",
2622
+ required: true,
2623
+ description: "The text that actually matched — for multi-outcome patterns ( e.g. `(BUILD_OK|BUILD_FAIL)` ) this tells which alternative matched."
2624
+ },
2372
2625
  elapsedMs: {
2373
2626
  type: "integer",
2374
2627
  required: true,
@@ -2423,18 +2676,40 @@ function registerTools(ctx, registry, resolveCwd, readShellOverrides) {
2423
2676
  description: "Exit signal name, if killed by a signal."
2424
2677
  }
2425
2678
  }
2679
+ },
2680
+ {
2681
+ type: "object",
2682
+ additionalProperties: false,
2683
+ properties: {
2684
+ kind: {
2685
+ type: "string",
2686
+ required: true,
2687
+ const: "skipped"
2688
+ },
2689
+ needle: {
2690
+ type: "string",
2691
+ required: true
2692
+ }
2693
+ }
2426
2694
  }
2427
2695
  ] },
2428
2696
  render: (_args, value) => {
2429
2697
  const v = value;
2430
- if (v.kind === "found") return [{
2431
- type: "text",
2432
- text: `Found "${v.needle}" at line ${v.line}, column ${v.column} (after ${v.elapsedMs}ms).`
2433
- }];
2698
+ if (v.kind === "found") {
2699
+ const matched = v.match !== void 0 && v.match !== "" ? `, matched "${v.match}"` : "";
2700
+ return [{
2701
+ type: "text",
2702
+ text: `Found "${v.needle}" at line ${v.line}, column ${v.column}${matched} (after ${v.elapsedMs}ms).`
2703
+ }];
2704
+ }
2434
2705
  if (v.kind === "timeout") return [{
2435
2706
  type: "text",
2436
2707
  text: `Timed out after ${v.timeoutMs}ms waiting for "${v.needle}". Call terminal_read to inspect the transcript.`
2437
2708
  }];
2709
+ if (v.kind === "skipped") return [{
2710
+ type: "text",
2711
+ text: `Skipped by user while waiting for "${v.needle}" — the wait ended early. Call terminal_read to inspect the transcript and decide how to proceed.`
2712
+ }];
2438
2713
  const exitInfo = v.exitCode !== void 0 && v.exitCode !== null ? ` (exit code ${v.exitCode})` : "";
2439
2714
  return [{
2440
2715
  type: "text",
@@ -3505,12 +3780,13 @@ function buildSidechatApi(ctx) {
3505
3780
  meta: {
3506
3781
  ...parentSession.header.cwd === void 0 ? {} : { cwd: parentSession.header.cwd },
3507
3782
  parentSession: parentSession.id,
3508
- isSeeded: seed.length > 0,
3783
+ isSeeded: true,
3509
3784
  origin: "subagent",
3510
3785
  delegationDepth: (parentSession.header.delegationDepth ?? 0) + 1,
3511
3786
  ...agentPreset === void 0 ? {} : { agentPreset }
3512
3787
  },
3513
3788
  seed,
3789
+ inheritedEventCount: SessionLogOffset(seed.length),
3514
3790
  agentOptions: { ...parent.options },
3515
3791
  setup,
3516
3792
  signal: AbortSignal.timeout(CREATE_TIMEOUT_MS)
@@ -4340,6 +4616,33 @@ async function attachAgentList(registry, ws, req) {
4340
4616
  }
4341
4617
  }
4342
4618
  /**
4619
+ * The WS close reason for a failed terminal attach. A missing configured
4620
+ * shell gets a SHORT machine-readable marker (`shell-not-found:<name>`,
4621
+ * capped by BYTES — a WS close reason allows at most 123 bytes, which `ws`
4622
+ * validates with `Buffer.byteLength`) that the client maps to a localized,
4623
+ * actionable banner; every other failure keeps the raw message (the
4624
+ * model-side tool errors read it verbatim).
4625
+ */
4626
+ function wsCloseReasonOf(error) {
4627
+ if (error instanceof SidebarError && error.code === "shell-not-found") return `shell-not-found:${truncateUtf8Bytes(shellDisplayName(String(error.meta?.shell ?? "")), 100)}`;
4628
+ return error instanceof Error ? error.message : String(error);
4629
+ }
4630
+ /**
4631
+ * Truncate to at most `maxBytes` UTF-8 bytes without splitting a code point.
4632
+ * A character-count `slice` does not bound the WS close reason: `ws` measures
4633
+ * `Buffer.byteLength` against its 123-byte cap, and the resulting throw would
4634
+ * replace the very error the reason describes.
4635
+ */
4636
+ function truncateUtf8Bytes(value, maxBytes) {
4637
+ if (Buffer.byteLength(value) <= maxBytes) return value;
4638
+ let truncated = "";
4639
+ for (const character of value) {
4640
+ if (Buffer.byteLength(truncated + character) > maxBytes) break;
4641
+ truncated += character;
4642
+ }
4643
+ return truncated;
4644
+ }
4645
+ /**
4343
4646
  * Wire one terminal socket to its pty: replay transcript, pump both ways.
4344
4647
  * Two attach modes share the wire protocol:
4345
4648
  * - `?uuid=...` attaches to an agent-owned terminal (created by the
@@ -4385,6 +4688,7 @@ async function attachTerminal(ctx, ptyManager, agentPtyRegistry, ws, req, resolv
4385
4688
  const cwd = await sessionCwdOf(ctx, sessionId, url.searchParams.get("cwd") ?? void 0);
4386
4689
  const overrides = shellOverridesOf(getSettings);
4387
4690
  const handle = ptyManager.open(sessionId, tabId, cwd, 80, 24, overrides.shell, overrides.shellArgs);
4691
+ armPtyResizeGate(handle.pty);
4388
4692
  if (handle.transcript !== "") ws.send(handle.transcript);
4389
4693
  const onData = (data) => {
4390
4694
  if (ws.readyState === WebSocket.OPEN && ws.bufferedAmount < 4194304) ws.send(data);
@@ -4410,10 +4714,8 @@ async function attachTerminal(ctx, ptyManager, agentPtyRegistry, ws, req, resolv
4410
4714
  return;
4411
4715
  }
4412
4716
  if (handle.exited) return;
4413
- if (control !== null && control.type === "resize" && typeof control.cols === "number" && typeof control.rows === "number") {
4414
- const dims = clampDims(control.cols, control.rows);
4415
- handle.pty.resize(dims.cols, dims.rows);
4416
- } else handle.pty.write(text);
4717
+ if (control !== null && control.type === "resize" && typeof control.cols === "number" && typeof control.rows === "number") tryResizePty(handle.pty, control.cols, control.rows);
4718
+ else handle.pty.write(text);
4417
4719
  });
4418
4720
  ws.on("close", () => {
4419
4721
  dataSub.dispose();
@@ -4421,7 +4723,7 @@ async function attachTerminal(ctx, ptyManager, agentPtyRegistry, ws, req, resolv
4421
4723
  if (!ptyManager.isParked(handle.key)) ptyManager.scheduleClose(handle.key, resolved.reconnectGraceMs);
4422
4724
  });
4423
4725
  } catch (error) {
4424
- ws.close(1011, error instanceof Error ? error.message : String(error));
4726
+ ws.close(1011, wsCloseReasonOf(error));
4425
4727
  }
4426
4728
  }
4427
4729
  /**
@@ -4453,10 +4755,8 @@ function pumpAgentTerminal(registry, handle, ws) {
4453
4755
  registry.close(handle.uuid);
4454
4756
  return;
4455
4757
  }
4456
- if (control !== null && control.type === "resize" && typeof control.cols === "number" && typeof control.rows === "number") {
4457
- const dims = clampDims(control.cols, control.rows);
4458
- handle.pty.resize(dims.cols, dims.rows);
4459
- } else if (control === null) handle.pty.write(text);
4758
+ if (control !== null && control.type === "resize" && typeof control.cols === "number" && typeof control.rows === "number") tryResizePty(handle.pty, control.cols, control.rows);
4759
+ else if (control === null) handle.pty.write(text);
4460
4760
  });
4461
4761
  ws.on("close", () => {
4462
4762
  dataSub.dispose();
@@ -4464,4 +4764,4 @@ function pumpAgentTerminal(registry, handle, ws) {
4464
4764
  });
4465
4765
  }
4466
4766
  //#endregion
4467
- export { Config, apply, inject, mediaTypeForPath, name };
4767
+ export { Config, apply, inject, mediaTypeForPath, name, wsCloseReasonOf };