@north-light/crouter 0.3.158 → 0.3.159

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.
@@ -3,6 +3,8 @@ name: internal/storage-tiers
3
3
  kind: knowledge
4
4
  description: Where crouter state belongs
5
5
  when-and-why-to-read: When locating crtr state or deciding where a new file belongs, this reference should be read so files are found or placed in the storage tier with the right ownership and durability.
6
+ system-prompt-visibility: name
7
+ file-read-visibility: none
6
8
  short-form: The two crtr storage tiers — scope root for durable user/repo content and canvas home for node-graph runtime state, node artifacts, and human tickets.
7
9
  ---
8
10
 
@@ -2,8 +2,3 @@ import { type BashJobStatus } from '../../../core/bash-jobs.js';
2
2
  /** Compact cockpit row: the running command (or job count) plus elapsed time.
3
3
  * `undefined` for an empty list — the caller omits the row entirely. */
4
4
  export declare function bashJobsWaitSummary(jobs: readonly BashJobStatus[], nowMs?: number): string | undefined;
5
- /** Readable drill-in document for the tmux detail surface. Watch-only — no
6
- * cancel line: the owning agent already received the exact `kill -- -<pgid>`
7
- * in its handoff notice, and persisting pgid to add a human stop affordance is
8
- * a separable future addition (measuring-stick restraint). */
9
- export declare function renderBashJobsDetails(jobs: readonly BashJobStatus[], nowMs?: number): string;
@@ -1,8 +1,8 @@
1
- // chrome/bash-jobs.ts — the attach-side formatter pair for live backgrounded bash
2
- // jobs (R3 of the Alt+C -> b bash handoff refinements design), mirroring
3
- // chrome/cron-wait.ts: a compact one-row cockpit summary and a read-only markdown
4
- // detail document. Pure — takes the already-scanned job list, never touches
5
- // disk itself (that's activeBackgroundBashJobs' job, in core/bash-jobs.ts).
1
+ // chrome/bash-jobs.ts — the attach-side formatter for live backgrounded bash jobs,
2
+ // mirroring chrome/cron-wait.ts: the compact one-row cockpit summary. Pure — takes
3
+ // the already-scanned job list, never touches disk itself (that's
4
+ // activeBackgroundBashJobs' job, in core/bash-jobs.ts). Enter on that row opens
5
+ // the Inspector's `jobs` section, which owns the drill-in rendering.
6
6
  import { formatBashElapsed } from '../../../core/bash-jobs.js';
7
7
  const CMD_CELL_CAP = 26;
8
8
  /** First line of `command`, whitespace-collapsed and capped to ~26 chars. */
@@ -22,22 +22,3 @@ export function bashJobsWaitSummary(jobs, nowMs = Date.now()) {
22
22
  const oldest = jobs[0]; // activeBackgroundBashJobs returns oldest-first
23
23
  return `${jobs.length} bash jobs · oldest ${formatBashElapsed(nowMs - oldest.startedAtMs)}`;
24
24
  }
25
- /** Readable drill-in document for the tmux detail surface. Watch-only — no
26
- * cancel line: the owning agent already received the exact `kill -- -<pgid>`
27
- * in its handoff notice, and persisting pgid to add a human stop affordance is
28
- * a separable future addition (measuring-stick restraint). */
29
- export function renderBashJobsDetails(jobs, nowMs = Date.now()) {
30
- const lines = [
31
- '# Background bash jobs',
32
- '',
33
- `${jobs.length} bash job${jobs.length === 1 ? '' : 's'} running in the background.`,
34
- ];
35
- for (const job of jobs) {
36
- lines.push('', `## ⚙ ${cmdCell(job.command)}`, '');
37
- lines.push(`- **Elapsed:** ${formatBashElapsed(nowMs - job.startedAtMs)} · started ${new Date(job.startedAtMs).toISOString()}`);
38
- lines.push(`- **Log:** \`${job.logPath}\``);
39
- lines.push(`- **Watch:** \`tail -f ${job.logPath}\``);
40
- lines.push('', '### Command', '', '```bash', job.command, '```');
41
- }
42
- return `${lines.join('\n')}\n`;
43
- }
@@ -11,8 +11,9 @@ export interface RosterHooks {
11
11
  * on close so a wait row that resolved while the popup was open disappears. */
12
12
  requestChromeRefresh: () => void;
13
13
  /** Open the viewer-owned Inspector popup (`surface inspect`) on a section —
14
- * the cron wait row routes to `schedule`; the native Inspector owns details. */
15
- openInspector: (section: 'overview' | 'schedule') => void;
14
+ * the cron wait row routes to `schedule` and the bash row to `jobs`; the
15
+ * native Inspector owns details for both. */
16
+ openInspector: (section: 'overview' | 'schedule' | 'jobs') => void;
16
17
  }
17
18
  export interface AttachRoster {
18
19
  /** Rebuild the rows from the canvas source, reconcile the selection, repaint. */
@@ -4,9 +4,11 @@
4
4
  // editor's visual bottom drops the cursor into the list; within it ↑/↓ move the
5
5
  // highlight, ↑ off the TOP row returns to the editor, ↓ CLAMPS at the bottom,
6
6
  // Enter opens the row (SWAPS this pane in place onto a node, or opens the wait
7
- // detail surface for a human/cron/bash row), Esc / any typed key returns to the
8
- // editor. Disabled entirely for a REMOTE attach: a remote graph id has no local
9
- // pane to swap into and no local jobs/crons to inspect.
7
+ // detail surface for a human/cron/bash row: a human row opens the inbox, a cron
8
+ // row the Inspector's schedule section, a bash row the Inspector's jobs section),
9
+ // Esc / any typed key returns to the editor. Disabled entirely for a REMOTE
10
+ // attach: a remote graph id has no local pane to swap into and no local
11
+ // jobs/crons to inspect.
10
12
  //
11
13
  // TWO EDITOR COUPLINGS ARE OWNED HERE, deliberately and exclusively:
12
14
  // • `editor.onCursorDownAtBottom` — the boundary handoff. The editor owns
@@ -23,18 +25,12 @@
23
25
  // graph, so `refresh()` rebuilds the cached rows asynchronously while arrow
24
26
  // movement only reverses the selected cached row and renders immediately.
25
27
  import { execFile, execFileSync } from 'node:child_process';
26
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
27
- import { tmpdir } from 'node:os';
28
- import { join } from 'node:path';
29
28
  import { Text, matchesKey } from '@earendil-works/pi-tui';
30
29
  import { cliClient } from '../../../commands/api-client.js';
31
30
  import { activeBackgroundBashJobs } from '../../../core/bash-jobs.js';
32
31
  import { contextDir } from '../../../core/canvas/paths.js';
33
32
  import { fillBar, fillWidth, REVERSE } from '../../../core/canvas/nav-render.js';
34
- import { inTmux, shellQuote } from '../../../core/runtime/placement-tmux.js';
35
- import { surfaceTmuxStyleArgs } from '../../../core/runtime/surface-bg.js';
36
33
  import { buildCanvasPanelLines } from './canvas-panels.js';
37
- import { renderBashJobsDetails } from './bash-jobs.js';
38
34
  /** Seed pane focus from the pane's REAL active state, not an unconditional
39
35
  * `true`. tmux only emits DECSET 1004 focus events on a TRANSITION, so a viewer
40
36
  * spawned into a non-active pane (a `split -d` beside another node — the
@@ -114,31 +110,6 @@ export function createRoster(s, hooks) {
114
110
  selKey = items[0].key;
115
111
  paint();
116
112
  };
117
- /** Open a read-only markdown detail popup in nvim, deleting the temp file when
118
- * the popup closes and re-running the chrome refresh (background-bash wait row). */
119
- const openDetailsPopup = (prefix, file, title, missingNvim, body) => {
120
- const dir = mkdtempSync(join(tmpdir(), prefix));
121
- const path = join(dir, file);
122
- writeFileSync(path, body, 'utf8');
123
- const cmd = `if command -v nvim >/dev/null 2>&1; then exec nvim -R -c 'setlocal filetype=markdown' -- ${shellQuote(path)}; else printf '${missingNvim}\\n'; printf 'Press Enter to close.'; read _; fi`;
124
- execFile('tmux', ['display-popup', '-E', '-T', title, '-w', '90%', '-h', '85%', ...surfaceTmuxStyleArgs(), cmd], () => {
125
- rmSync(dir, { recursive: true, force: true });
126
- hooks.requestChromeRefresh();
127
- });
128
- };
129
- const openBashJobsDetails = () => {
130
- const current = activeBackgroundBashJobs(contextDir(nodeId));
131
- if (current.length === 0) {
132
- hooks.setNotice('Background job already finished');
133
- hooks.requestChromeRefresh();
134
- return;
135
- }
136
- if (!inTmux()) {
137
- hooks.setNotice('Background job details require tmux');
138
- return;
139
- }
140
- openDetailsPopup('crtr-bash-jobs-', 'background-bash-jobs.md', ' background bash jobs ', 'nvim is required to inspect background bash jobs. Install Neovim and retry.', renderBashJobsDetails(current));
141
- };
142
113
  const openSelected = () => {
143
114
  const item = items.find((candidate) => candidate.key === selKey);
144
115
  if (item === undefined || remote)
@@ -158,8 +129,13 @@ export function createRoster(s, hooks) {
158
129
  return;
159
130
  }
160
131
  if (item.kind === 'bash') {
132
+ if (activeBackgroundBashJobs(contextDir(nodeId)).length === 0) {
133
+ hooks.setNotice('Background job already finished');
134
+ hooks.requestChromeRefresh();
135
+ return;
136
+ }
161
137
  exit();
162
- openBashJobsDetails();
138
+ hooks.openInspector('jobs');
163
139
  return;
164
140
  }
165
141
  const id = item.nodeId;