@volter/supercode-terminal 0.2.56 → 0.2.58

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/index.d.ts CHANGED
@@ -252,7 +252,7 @@ export type {
252
252
  export type { TerminalInputDelivery, TmuxCommand, TmuxWindowSize };
253
253
 
254
254
  /** The bounded live composer text, or null for an unrecognized screen. */
255
- export function composerText(kind: string, screen: string | undefined, options?: { allowBusy?: boolean }): string | null;
255
+ export function composerText(kind: string, screen: string | undefined): string | null;
256
256
  export function restoreComposerInput(host: TmuxTerminalHost | PtyTerminalHost | PaneServerHost, sessionId: string, kind: string, text: string): Promise<{ restored: boolean; reason?: string; remaining?: string }>;
257
257
  /** Failed input is restored or returns an explicit cleanup obligation; never blindly resend. */
258
- export function deliverComposerInput(host: TmuxTerminalHost | PtyTerminalHost | PaneServerHost, sessionId: string, kind: string, text: string, options?: { deadlineMs?: number; signal?: AbortSignal }): Promise<{ delivered: boolean; reason?: string; phase?: 'before-paste' | 'before-submit'; busy?: { indicator: string; row: string }; restored?: boolean; cleanup_required?: boolean; cleanup_reason?: string; cleanup_remaining?: string; delivery?: TerminalInputDelivery }>;
258
+ export function deliverComposerInput(host: TmuxTerminalHost | PtyTerminalHost | PaneServerHost, sessionId: string, kind: string, text: string, options?: { deadlineMs?: number; signal?: AbortSignal }): Promise<{ delivered: boolean; reason?: string; restored?: boolean; cleanup_required?: boolean; cleanup_reason?: string; cleanup_remaining?: string; delivery?: TerminalInputDelivery }>;
package/index.mjs CHANGED
@@ -4,6 +4,7 @@ import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, renameSync
4
4
  import { homedir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { connect as netConnect } from 'node:net';
7
+ import { createRequire } from 'node:module';
7
8
  import { fileURLToPath } from 'node:url';
8
9
  import { createServer as defaultCreateServer } from 'node:http';
9
10
 
@@ -560,11 +561,18 @@ export class PtyTerminalHost {
560
561
  if ([...this.#sessions.values()].some((entry) => entry.name === name)) {
561
562
  throw new TerminalHostError('session_exists', `A pane named ${name} already exists.`);
562
563
  }
563
- const [{ default: pty }, { default: headless }, { default: serialize }] = await Promise.all([
564
- import('@homebridge/node-pty-prebuilt-multiarch'),
565
- import('@xterm/headless'),
566
- import('@xterm/addon-serialize'),
567
- ]);
564
+ let pty, headless, serialize;
565
+ try {
566
+ [{ default: pty }, { default: headless }, { default: serialize }] = await Promise.all([
567
+ import('@homebridge/node-pty-prebuilt-multiarch'),
568
+ import('@xterm/headless'),
569
+ import('@xterm/addon-serialize'),
570
+ ]);
571
+ } catch (error) {
572
+ // A process that failed this load may keep failing it after the package is installed (Node keeps what it read of
573
+ // a half-installed package): the pane server answers this code by ending itself, and the next one loads afresh.
574
+ throw new TerminalHostError('pane_runtime_missing', `The pane runtime could not load: ${errorMessage(error)}`);
575
+ }
568
576
  const env = { ...process.env, ...(input.env ?? {}) };
569
577
  const { file, args } = windowsLaunch(input.command.program, input.command.arguments ?? [], env);
570
578
  // The watchdog is running before the pane is, and the pane is on its list the moment it exists.
@@ -974,6 +982,10 @@ export class PaneServerHost {
974
982
  rmSync(staging, { recursive: true, force: true });
975
983
  cpSync(root, join(staging, 'node_modules', '@volter', 'supercode-terminal'), { recursive: true, filter: (path) => !path.slice(root.length).split(/[\\/]/).includes('node_modules') });
976
984
  for (const name of PANE_RUNTIME_PACKAGES) cpSync(packageDir(name), join(staging, 'node_modules', ...name.split('/')), { recursive: true });
985
+ // A copy taken of a half-installed package would stay broken under its version: it is complete only when every
986
+ // runtime package resolves to an entry that exists in it.
987
+ const fromCopy = createRequire(join(staging, 'node_modules', '@volter', 'supercode-terminal', 'package.json'));
988
+ for (const name of PANE_RUNTIME_PACKAGES) if (!existsSync(fromCopy.resolve(name))) throw new Error(`${name} has no entry in the copy`);
977
989
  writeFileSync(join(staging, '.complete'), `${version}\n`);
978
990
  rmSync(target, { recursive: true, force: true });
979
991
  renameSync(staging, target);
@@ -1738,11 +1750,10 @@ export function composerHoldsDraft(kind, screen) {
1738
1750
  /** Read only a bounded, live Claude/Codex composer, never a prompt in scrollback.
1739
1751
  * Unknown layouts and collapsed pastes are deliberately unconfirmed.
1740
1752
  */
1741
- export function composerText(kind, screen, { allowBusy = false } = {}) {
1753
+ export function composerText(kind, screen) {
1742
1754
  if (kind !== 'claude-code' && kind !== 'codex') return null;
1743
1755
  const lines = String(screen ?? '').split('\n').map(styledCells);
1744
1756
  const plain = lines.map((cells) => cells.map((cell) => cell.char).join(''));
1745
- if (!allowBusy && composerBusy(screen)) return null;
1746
1757
  const rule = /^\s*[─━]{4,}\s*$/u;
1747
1758
  const footer = /^\s*(?:.*\btab to queue (?:a )?message\b.*$|\? for shortcuts\b|⚠\s+\d+\s+warnings?\s+·\s+f2 to view\s*$|[^\n]+ · (?:~(?:[\\/]|(?=\s|$))|[\\/]|[A-Z]:[\\/])[^\n]*$|.*← for agents\s*$|.*\bcontext(?:\s+\d+%)?\s+left\b)/iu;
1748
1759
  const caret = kind === 'codex' ? /^\s*›(?:\s|$)/u : /^\s*[❯>](?:\s|$)/u;
@@ -1791,22 +1802,10 @@ function composerMatches(value, wanted) {
1791
1802
  return offsets.has(wanted.length);
1792
1803
  }
1793
1804
 
1794
- /** Busy evidence must come from the live UI bottom, not a fleet label or a
1795
- * quoted phrase in the transcript. */
1796
- function composerBusyEvidence(screen) {
1797
- const bottom=String(screen??'').split('\n').map(line=>styledCells(line).map(cell=>cell.char).join('')).filter(line=>line.trim()).slice(-8);
1798
- for (const row of bottom) {
1799
- const match=/\b(?:esc to (?:interrupt|cancel)|tab to queue (?:a )?message)\b/iu.exec(row);
1800
- if (match) return { indicator: match[0], row: row.trim().slice(0, 240) };
1801
- }
1802
- return null;
1803
- }
1804
- function composerBusy(screen) { return composerBusyEvidence(screen) !== null; }
1805
-
1806
1805
  /** Undo only text this input wrote, including when a turn started meanwhile.
1807
1806
  * Never Enter, Escape, or Ctrl-C: restoration must not submit or stop work. */
1808
1807
  export async function restoreComposerInput(host, sessionId, kind, text) {
1809
- const read=async()=>composerText(kind,await host.capture(sessionId,{preserveEscapes:true}),{allowBusy:true});
1808
+ const read=async()=>composerText(kind,await host.capture(sessionId,{preserveEscapes:true}));
1810
1809
  let remaining=await read();
1811
1810
  if(remaining==='')return {restored:true};
1812
1811
  if(!composerMatches(remaining,String(text).trim()))return {restored:false,reason:'composer-changed-or-unreadable'};
@@ -1833,7 +1832,7 @@ export async function restoreComposerInput(host, sessionId, kind, text) {
1833
1832
  * unreadable/concurrently changed composer returns an explicit repair obligation. */
1834
1833
  export async function deliverComposerInput(host, sessionId, kind, text, { deadlineMs = Date.now() + 10_000, signal } = {}) {
1835
1834
  const capture=()=>host.capture(sessionId,{preserveEscapes:true});
1836
- const read=async()=>composerText(kind,await capture(),{allowBusy:true});
1835
+ const read=async()=>composerText(kind,await capture());
1837
1836
  const expired=()=>signal?.aborted||Date.now()>=deadlineMs;
1838
1837
  let touched=false;
1839
1838
  const failed=async (reason, evidence={})=>{
@@ -1847,7 +1846,6 @@ export async function deliverComposerInput(host, sessionId, kind, text, { deadli
1847
1846
  if(!wanted||/[\x00-\x08\x0b-\x1f\x7f]/u.test(text))return failed('unsupported-text');
1848
1847
  for(let check=0;check<2;check++) {
1849
1848
  const screen=await capture(),before=composerText(kind,screen);
1850
- if(composerBusy(screen))return failed('busy-composer',{phase:'before-paste',busy:composerBusyEvidence(screen)});
1851
1849
  if(before!=='')return failed(before===null?'no-composer':'draft');
1852
1850
  if(check===0)await sleep(200);
1853
1851
  }
@@ -1864,14 +1862,13 @@ export async function deliverComposerInput(host, sessionId, kind, text, { deadli
1864
1862
  for(let attempt=0;attempt<3;attempt++) {
1865
1863
  if(expired())return failed('deadline');
1866
1864
  const screen=await capture();
1867
- if(composerBusy(screen))return failed('busy-composer',{phase:'before-submit',busy:composerBusyEvidence(screen)});
1868
1865
  if(!composerMatches(composerText(kind,screen),wanted))return failed('composer-changed');
1869
1866
  if(expired())return failed('deadline');
1870
1867
  await host.sendInput(sessionId,'\r',{submitMode:'single',deadlineMs});
1871
1868
  for(let poll=0;poll<10&&!expired();poll++) {
1872
1869
  await sleep(100);
1873
1870
  const current=await read();
1874
- if(current==='')return {delivered:true,delivery};
1871
+ if(current==='')return kind === 'claude-code' ? {delivered:true,delivery,path:'composer_submit'} : {delivered:false,reason:'native-transcript-confirmation-required',delivery};
1875
1872
  if(!composerMatches(current,wanted))return failed('submission-unconfirmed');
1876
1873
  }
1877
1874
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-terminal",
3
- "version": "0.2.56",
3
+ "version": "0.2.58",
4
4
  "type": "module",
5
5
  "description": "Optional leak-safe terminal and tmux host adapter for Volter Harness frontends",
6
6
  "exports": {
@@ -95,8 +95,12 @@
95
95
  "prepack": "npm run build",
96
96
  "build:substrate": "tsc -p tsconfig.substrate.json"
97
97
  },
98
- "peerDependencies": {
98
+ "dependencies": {
99
99
  "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1 || ^0.14.1",
100
+ "@xterm/addon-serialize": "^0.14.0",
101
+ "@xterm/headless": "^6.0.0"
102
+ },
103
+ "peerDependencies": {
100
104
  "@xterm/addon-fit": "^0.11.0",
101
105
  "@xterm/addon-search": "^0.16.0",
102
106
  "@xterm/addon-web-links": "^0.12.0",
@@ -104,14 +108,9 @@
104
108
  "preact": ">=10.19.0 <11",
105
109
  "react": ">=18 <20",
106
110
  "ws": "^8.21.0",
107
- "@volter/supercode-harness-sdk": "^0.3.18",
108
- "@xterm/headless": "^6.0.0",
109
- "@xterm/addon-serialize": "^0.14.0"
111
+ "@volter/supercode-harness-sdk": "^0.3.18"
110
112
  },
111
113
  "peerDependenciesMeta": {
112
- "@homebridge/node-pty-prebuilt-multiarch": {
113
- "optional": true
114
- },
115
114
  "@xterm/addon-fit": {
116
115
  "optional": true
117
116
  },
@@ -135,24 +134,15 @@
135
134
  },
136
135
  "@volter/supercode-harness-sdk": {
137
136
  "optional": true
138
- },
139
- "@xterm/headless": {
140
- "optional": true
141
- },
142
- "@xterm/addon-serialize": {
143
- "optional": true
144
137
  }
145
138
  },
146
139
  "devDependencies": {
147
- "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
148
140
  "@types/node": "^22.20.1",
149
141
  "@types/ws": "^8.18.1",
150
142
  "@volter/supercode-harness-sdk": "^0.3.18",
151
143
  "@xterm/addon-fit": "^0.11.0",
152
144
  "@xterm/addon-search": "^0.16.0",
153
- "@xterm/addon-serialize": "^0.14.0",
154
145
  "@xterm/addon-web-links": "^0.12.0",
155
- "@xterm/headless": "^6.0.0",
156
146
  "@xterm/xterm": "^6.0.0",
157
147
  "esbuild": "^0.25.0",
158
148
  "preact": "^10.19.0",
@@ -86,7 +86,14 @@ const server = createServer((connection) => {
86
86
  const { id, op, args = [] } = message;
87
87
  if (op === 'attach') { attach(id, args, message.stream); continue; }
88
88
  if (!OPS.has(op)) { refuse(id, { code: 'unsupported', message: `pane server: no operation ${op}` }); continue; }
89
- Promise.resolve().then(() => host[op](...args)).then((result) => answer(id, result), (error) => refuse(id, error));
89
+ Promise.resolve().then(() => host[op](...args)).then((result) => answer(id, result), async (error) => {
90
+ refuse(id, error);
91
+ // The pane runtime failed to load in this process: with no pane to keep, it ends once the answer is out, so
92
+ // the next request starts a pane server that loads the runtime afresh (one installed since this one started).
93
+ if (error?.code === 'pane_runtime_missing' && !(await host.listSessions().catch(() => [])).length) {
94
+ connection.end(() => { host.dispose(); process.exit(0); });
95
+ }
96
+ });
90
97
  }
91
98
  });
92
99
  // A client that goes (its daemon restarting) takes only its viewers with it: the panes stay.