@north-light/crouter 0.3.196 → 0.3.197

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.
@@ -10,7 +10,6 @@ import { extname, join } from 'node:path';
10
10
  import { randomBytes } from 'node:crypto';
11
11
  import { rmSync } from 'node:fs';
12
12
  import { readConfig } from '../../core/config.js';
13
- import { tmuxServerReachable } from '../../core/spawn.js';
14
13
  import { markStopSignal } from '../../core/runtime/stop-signals.js';
15
14
  import { atomicWriteJson } from '../../core/human/convention.js';
16
15
  import { ticketDir } from '../../core/human/root.js';
@@ -313,22 +312,17 @@ export const humanShow = defineLeaf({
313
312
  const path = input['path'];
314
313
  const windowArg = input['window'];
315
314
  const window = windowArg !== undefined ? windowArg : 'auto';
316
- // `human show` must never fail the caller: any display error degrades to
317
- // {pane_id:null, reason} with exit 0 (matches crouter renderer display semantics).
318
- let paneId;
315
+ // `human show` always reports display failures in its result rather than
316
+ // turning a passive screen surface into a failed agent turn.
319
317
  try {
320
- const r = display(path, { window, maxPanes: resolveMaxPanes() });
321
- paneId = r.paneId;
318
+ const result = display(path, { window, maxPanes: resolveMaxPanes() });
319
+ return result.paneId !== undefined
320
+ ? { pane_id: result.paneId, reason: null }
321
+ : { pane_id: null, reason: result.reason };
322
322
  }
323
- catch {
324
- paneId = undefined;
323
+ catch (error) {
324
+ const reason = error instanceof Error ? error.message : String(error);
325
+ return { pane_id: null, reason: `display failed: ${reason}` };
325
326
  }
326
- if (paneId !== undefined) {
327
- return { pane_id: paneId, reason: null };
328
- }
329
- const reason = tmuxServerReachable()
330
- ? 'renderer unavailable (termrender/uv missing)'
331
- : 'not in tmux';
332
- return { pane_id: null, reason };
333
327
  },
334
328
  });
@@ -6,7 +6,11 @@ export interface DisplayOpts {
6
6
  /** Pane budget per window before `'auto'` opens a new window. Default 3. */
7
7
  maxPanes?: number;
8
8
  }
9
- export declare function countPanesInCurrentWindow(): number;
10
- export declare function display(path: string, opts?: DisplayOpts): {
11
- paneId?: string;
9
+ export type DisplayResult = {
10
+ paneId: string;
11
+ reason?: never;
12
+ } | {
13
+ paneId?: never;
14
+ reason: string;
12
15
  };
16
+ export declare function display(path: string, opts?: DisplayOpts): DisplayResult;
@@ -1,19 +1,55 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { displayInPane } from './termrender.js';
3
- export function countPanesInCurrentWindow() {
4
- // -t '' targets the current window of the current session.
5
- const result = spawnSync('tmux', ['list-panes', '-F', '#{pane_id}'], {
2
+ import { resolve } from 'node:path';
3
+ import { breakPane, closePane, currentTmux, paneOfNode, splitWindow, } from '../runtime/placement-tmux.js';
4
+ import { documentWatchCommand } from './termrender.js';
5
+ function targetPane() {
6
+ const callerPane = process.env['TMUX_PANE'];
7
+ if (callerPane !== undefined && callerPane !== '')
8
+ return callerPane;
9
+ const nodeId = process.env['CRTR_NODE_ID'];
10
+ if (nodeId !== undefined && nodeId !== '') {
11
+ const viewerPane = paneOfNode(nodeId);
12
+ if (viewerPane !== null)
13
+ return viewerPane;
14
+ }
15
+ return currentTmux()?.pane ?? null;
16
+ }
17
+ function countPanes(target) {
18
+ const result = spawnSync('tmux', ['list-panes', '-t', target, '-F', '#{pane_id}'], {
6
19
  encoding: 'utf8',
7
20
  });
8
21
  if (result.status !== 0)
9
- return 0;
22
+ return null;
10
23
  return result.stdout.split('\n').filter((line) => line.trim() !== '').length;
11
24
  }
12
25
  export function display(path, opts) {
13
26
  const window = (opts?.window === 'split' || opts?.window === 'new') ? opts.window : 'auto';
14
27
  const maxPanes = (opts?.maxPanes !== undefined && opts.maxPanes > 0) ? opts.maxPanes : 3;
15
- const newWindow = window === 'new' ||
16
- (window === 'auto' && countPanesInCurrentWindow() >= maxPanes);
17
- // The pane always watches the file — displayed docs are live by definition.
18
- return displayInPane(path, { newWindow });
28
+ const command = documentWatchCommand(resolve(path));
29
+ if (command === null)
30
+ return { reason: 'renderer unavailable' };
31
+ const target = targetPane();
32
+ if (target === null)
33
+ return { reason: 'no pane on screen' };
34
+ let newWindow = window === 'new';
35
+ if (window === 'auto') {
36
+ const paneCount = countPanes(target);
37
+ if (paneCount === null)
38
+ return { reason: `tmux could not inspect target pane ${target}` };
39
+ newWindow = paneCount >= maxPanes;
40
+ }
41
+ const pane = splitWindow(target, {
42
+ cwd: process.cwd(),
43
+ env: {},
44
+ command,
45
+ });
46
+ if (pane === null)
47
+ return { reason: `tmux could not split target pane ${target}` };
48
+ if (!newWindow)
49
+ return { paneId: pane };
50
+ const opened = breakPane(pane);
51
+ if (opened !== null)
52
+ return { paneId: opened.pane };
53
+ closePane(pane);
54
+ return { reason: 'tmux could not move the document pane into a new window' };
19
55
  }
@@ -59,15 +59,6 @@ export declare function checkMarkdown(md: string): {
59
59
  ok: false;
60
60
  error: string;
61
61
  };
62
- export interface DisplayInPaneOpts {
63
- /** Open in a new tmux window instead of splitting the current one. */
64
- newWindow?: boolean;
65
- }
66
- /**
67
- * Spawn termrender into a live tmux pane. The pane-budget policy (whether to
68
- * split vs open a new window) is decided by the caller (`src/surfaces/
69
- * display.ts`); this is the thin managed-binary spawn it delegates to.
70
- */
71
- export declare function displayInPane(path: string, opts?: DisplayInPaneOpts): {
72
- paneId?: string;
73
- };
62
+ /** Shell command for a pane that live-renders `path` with crouter's pinned
63
+ * termrender. Null means the managed renderer could not be provisioned. */
64
+ export declare function documentWatchCommand(path: string): string | null;
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, unlinkSyn
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import stringWidth from 'string-width';
6
+ import { shellQuote } from '../../shared/shell-quote.js';
6
7
  import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
7
8
  import { TERMRENDER_VERSION } from './version.js';
8
9
  // ── The sole org-wide termrender binding ─────────────────────────────────────
@@ -703,43 +704,11 @@ export function checkMarkdown(md) {
703
704
  }
704
705
  return { ok: true };
705
706
  }
706
- /**
707
- * Spawn termrender into a live tmux pane. The pane-budget policy (whether to
708
- * split vs open a new window) is decided by the caller (`src/surfaces/
709
- * display.ts`); this is the thin managed-binary spawn it delegates to.
710
- */
711
- export function displayInPane(path, opts = {}) {
707
+ /** Shell command for a pane that live-renders `path` with crouter's pinned
708
+ * termrender. Null means the managed renderer could not be provisioned. */
709
+ export function documentWatchCommand(path) {
712
710
  ensureRenderer();
713
711
  if (rendererState !== 'ready')
714
- return {};
715
- // Always watch: a displayed pane is a live view of the file by definition.
716
- const args = ['pane', 'open', path, '--watch'];
717
- args.push('--window', opts.newWindow ? 'new' : 'split');
718
- const result = spawnSync(VENV_BIN, args, {
719
- encoding: 'utf-8',
720
- stdio: ['pipe', 'pipe', 'pipe'],
721
- });
722
- // A spawn fault (binary broke) invalidates readiness; a non-zero exit (e.g.
723
- // tmux refused, bad path) is not an environment fault and must not.
724
- if (result.error) {
725
- invalidateRenderer('pane open');
726
- return {};
727
- }
728
- if (result.status !== 0)
729
- return {};
730
- // `encoding: 'utf-8'` makes spawnSync return stdout as a string.
731
- const rawStdout = result.stdout;
732
- if (!rawStdout)
733
- return {};
734
- let parsed = null;
735
- try {
736
- parsed = JSON.parse(rawStdout.trim());
737
- }
738
- catch {
739
- return {};
740
- }
741
- if (parsed && typeof parsed.pane_id === 'string' && parsed.pane_id) {
742
- return { paneId: parsed.pane_id };
743
- }
744
- return {};
712
+ return null;
713
+ return `${shellQuote(VENV_BIN)} doc watch ${shellQuote(path)}`;
745
714
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.196",
3
+ "version": "0.3.197",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.196",
3
+ "version": "0.3.197",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.196",
9
+ "version": "0.3.197",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {