@north-light/crouter 0.3.169 → 0.3.171
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/dist/build-root.d.ts +1 -16
- package/dist/build-root.js +1 -25
- package/dist/builtin-memory/insights/capture.md +66 -0
- package/dist/builtin-memory/insights/init.md +45 -0
- package/dist/builtin-memory/internal/plugins.md +12 -11
- package/dist/cli.js +2 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
- package/dist/clients/attach/__tests__/context-message.test.js +50 -3
- package/dist/clients/attach/__tests__/diagram.test.js +1 -1
- package/dist/clients/attach/chrome/header.d.ts +10 -0
- package/dist/clients/attach/chrome/header.js +35 -0
- package/dist/clients/attach/chrome/status-line.d.ts +3 -0
- package/dist/clients/attach/chrome/status-line.js +5 -0
- package/dist/clients/attach/chrome/widgets.js +7 -18
- package/dist/clients/attach/input/controller.d.ts +10 -0
- package/dist/clients/attach/input/controller.js +3 -0
- package/dist/clients/attach/overlays/help.d.ts +33 -0
- package/dist/clients/attach/overlays/help.js +204 -0
- package/dist/clients/attach/render/chat-view.d.ts +81 -9
- package/dist/clients/attach/render/chat-view.js +434 -90
- package/dist/clients/attach/render/context-message.js +3 -3
- package/dist/clients/attach/render/diagram.js +2 -2
- package/dist/clients/attach/render/measured-container.d.ts +94 -0
- package/dist/clients/attach/render/measured-container.js +307 -0
- package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
- package/dist/clients/attach/render/transcript-copy.js +81 -0
- package/dist/clients/attach/render/viewport.d.ts +29 -0
- package/dist/clients/attach/render/viewport.js +160 -0
- package/dist/clients/attach/session/bindings.d.ts +5 -1
- package/dist/clients/attach/session/bindings.js +16 -0
- package/dist/clients/attach/session/context.d.ts +5 -3
- package/dist/clients/attach/session/frame.d.ts +34 -0
- package/dist/clients/attach/session/frame.js +173 -0
- package/dist/clients/attach/session/frames.d.ts +2 -0
- package/dist/clients/attach/session/frames.js +3 -0
- package/dist/clients/attach/session/input-wiring.d.ts +10 -4
- package/dist/clients/attach/session/input-wiring.js +13 -13
- package/dist/clients/attach/session/keys.d.ts +4 -1
- package/dist/clients/attach/session/keys.js +9 -4
- package/dist/clients/attach/session/layout.d.ts +10 -8
- package/dist/clients/attach/session/layout.js +32 -31
- package/dist/clients/attach/session/mouse.d.ts +15 -0
- package/dist/clients/attach/session/mouse.js +61 -0
- package/dist/clients/attach/slash/dispatch.d.ts +10 -0
- package/dist/clients/attach/slash/dispatch.js +35 -2
- package/dist/clients/attach/viewer.js +605 -581
- package/dist/clients/web/dev-server.d.ts +4 -1
- package/dist/clients/web/dev-server.js +8 -19
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/human/prompts.js +16 -6
- package/dist/commands/pkg/plugin-manage.js +143 -68
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
- package/dist/commands/sys/setup-core.js +21 -16
- package/dist/core/__tests__/human-deliver.test.js +62 -2
- package/dist/core/clipboard-text.d.ts +8 -0
- package/dist/core/clipboard-text.js +18 -0
- package/dist/core/command-manifests/registry.d.ts +1 -3
- package/dist/core/command-plugins/bundle.d.ts +29 -0
- package/dist/core/command-plugins/bundle.js +198 -0
- package/dist/core/command-plugins/discovery.d.ts +0 -8
- package/dist/core/command-plugins/discovery.js +2 -39
- package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
- package/dist/core/command-plugins/transport/http-fetch.js +17 -95
- package/dist/core/command.d.ts +9 -13
- package/dist/core/command.js +12 -17
- package/dist/core/config.js +2 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +8 -0
- package/dist/core/runtime/broker-protocol.d.ts +13 -1
- package/dist/core/runtime/broker.js +213 -5
- package/dist/core/runtime/canvas-extensions.d.ts +3 -0
- package/dist/core/runtime/canvas-extensions.js +3 -0
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/kickoff.d.ts +1 -5
- package/dist/core/runtime/kickoff.js +1 -5
- package/dist/core/runtime/naming.d.ts +7 -0
- package/dist/core/runtime/naming.js +5 -2
- package/dist/core/runtime/node-read.js +15 -0
- package/dist/core/runtime/package-health.d.ts +6 -21
- package/dist/core/runtime/package-health.js +12 -123
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/core/runtime/tool-group-summary.d.ts +19 -0
- package/dist/core/runtime/tool-group-summary.js +117 -0
- package/dist/core/termrender/code-doc.d.ts +12 -0
- package/dist/core/termrender/code-doc.js +91 -0
- package/dist/core/termrender/display.d.ts +12 -0
- package/dist/core/termrender/display.js +19 -0
- package/dist/core/termrender/termrender.d.ts +73 -0
- package/dist/core/termrender/termrender.js +795 -0
- package/dist/core/termrender/version.d.ts +1 -0
- package/dist/core/termrender/version.js +1 -0
- package/dist/core/user-settings.d.ts +14 -0
- package/dist/core/user-settings.js +20 -0
- package/dist/daemon/crtrd.js +5 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
- package/dist/pi-extensions/canvas-stophook.js +2 -2
- package/dist/pi-extensions/canvas-structured-output.js +52 -8
- package/dist/pi-extensions/summary-tool.d.ts +33 -0
- package/dist/pi-extensions/summary-tool.js +62 -0
- package/dist/shared/generated-context.d.ts +19 -4
- package/dist/shared/generated-context.js +98 -7
- package/dist/shared/tool-groups.d.ts +23 -0
- package/dist/shared/tool-groups.js +60 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +3 -1
- package/runtime.lock.json +62 -3
- package/scripts/postinstall.mjs +14 -1
- package/dist/core/command-plugins/store.d.ts +0 -16
- package/dist/core/command-plugins/store.js +0 -64
- package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
- package/dist/core/runtime/fault-recovery-nudge.js +0 -3
|
@@ -5,6 +5,13 @@ export declare function sanitizeSessionName(raw: string): string;
|
|
|
5
5
|
/** Local fallback: derive a name straight from the prompt (no pi call). Drops
|
|
6
6
|
* stop-words, takes the first few content words. */
|
|
7
7
|
export declare function slugFromPrompt(prompt: string): string;
|
|
8
|
+
/** The namer's model: the `light` rung of the DEFAULT provider's ladder, with
|
|
9
|
+
* any thinking suffix stripped (we pass `--thinking off` explicitly — naming is
|
|
10
|
+
* a trivial classification that never needs a reasoning budget). Reading the
|
|
11
|
+
* ladder rather than pinning a concrete model id keeps naming working for a
|
|
12
|
+
* user who never configured Anthropic: an OpenAI default provider resolves to
|
|
13
|
+
* its own light rung. `CRTR_NAME_MODEL` still overrides. */
|
|
14
|
+
export declare function lightModel(overrideEnv?: string): string;
|
|
8
15
|
/** A generated session name: the kebab-case handle plus the Nerd Font glyph the
|
|
9
16
|
* namer chose for it. `icon` is '' when the model returned nothing usable —
|
|
10
17
|
* every surface renders the name alone in that case. */
|
|
@@ -119,13 +119,16 @@ export function slugFromPrompt(prompt) {
|
|
|
119
119
|
* ladder rather than pinning a concrete model id keeps naming working for a
|
|
120
120
|
* user who never configured Anthropic: an OpenAI default provider resolves to
|
|
121
121
|
* its own light rung. `CRTR_NAME_MODEL` still overrides. */
|
|
122
|
-
function
|
|
123
|
-
const override = process.env[
|
|
122
|
+
export function lightModel(overrideEnv) {
|
|
123
|
+
const override = overrideEnv === undefined ? undefined : process.env[overrideEnv];
|
|
124
124
|
if (override !== undefined && override.trim() !== '')
|
|
125
125
|
return override.trim();
|
|
126
126
|
const ladders = modelLadders();
|
|
127
127
|
return stripThinkingSuffix(ladders[defaultProvider(ladders)].light);
|
|
128
128
|
}
|
|
129
|
+
function nameModel() {
|
|
130
|
+
return lightModel('CRTR_NAME_MODEL');
|
|
131
|
+
}
|
|
129
132
|
/** The pi argv for a headless TEXT request (issue titles). Stripped down (no
|
|
130
133
|
* tools, session, context files, extensions, skills, templates, themes) so
|
|
131
134
|
* it's fast and side-effect free. */
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// and were imported into the handlers from there; Stage B-3 relocated them here
|
|
9
9
|
// so the CLI leaves' import graph reaches no canvas state store (spec §1/§10).
|
|
10
10
|
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
11
|
+
import { readJsonIfExists } from '../fs-utils.js';
|
|
11
12
|
import { tmpdir } from 'node:os';
|
|
12
13
|
import { basename, join } from 'node:path';
|
|
13
14
|
import { InputError } from '../io.js';
|
|
@@ -17,6 +18,7 @@ import { SessionManager } from './broker-sdk.js';
|
|
|
17
18
|
import { piAgentDir } from './package-health.js';
|
|
18
19
|
import { BUILTIN_SLASH_COMMANDS } from './pi-vendored.js';
|
|
19
20
|
import { cycleAwareMessages } from './session-cycles.js';
|
|
21
|
+
import { normalizeToolGroupSummaryRecord } from '../../shared/tool-groups.js';
|
|
20
22
|
import { ModelRegistry, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
|
21
23
|
import { PoolCredentialStore } from '../pool-credential-store.js';
|
|
22
24
|
/** A session `.jsonl` records only `{ provider, modelId }` — pi never persists a
|
|
@@ -131,6 +133,15 @@ function readIfPresent(path) {
|
|
|
131
133
|
return null;
|
|
132
134
|
}
|
|
133
135
|
}
|
|
136
|
+
function readToolGroupSummaries(nodeId) {
|
|
137
|
+
try {
|
|
138
|
+
const raw = readJsonIfExists(join(jobDir(nodeId), 'tool-summaries.json'));
|
|
139
|
+
return normalizeToolGroupSummaryRecord(raw);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
134
145
|
/** Reconstruct a persisted session without launching its broker. Backs the
|
|
135
146
|
* machine-readable snapshot endpoint and the human transcript render. */
|
|
136
147
|
export async function readNodeSnapshot(nodeId) {
|
|
@@ -175,6 +186,10 @@ export async function readNodeSnapshot(nodeId) {
|
|
|
175
186
|
// process state (last setStatus/setWidget/setTitle) and is not persisted,
|
|
176
187
|
// so an offline read has none.
|
|
177
188
|
display: { statuses: {}, widgets: {}, title: undefined },
|
|
189
|
+
// Sidecar summaries survive broker exit so a dormant transcript receives
|
|
190
|
+
// the same completed-group data as a live welcome snapshot. Malformed or
|
|
191
|
+
// absent sidecars are deliberately ignored: tool calls remain visible.
|
|
192
|
+
toolGroupSummaries: readToolGroupSummaries(nodeId),
|
|
178
193
|
};
|
|
179
194
|
return {
|
|
180
195
|
nodeId,
|
|
@@ -4,27 +4,12 @@ export interface MissingPackage {
|
|
|
4
4
|
/** Absolute path it resolved to (which does not exist). */
|
|
5
5
|
resolved: string;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* dependency range in crouter's package.json. The bundled copy is the floor
|
|
14
|
-
* required by crouter's generated `hl inbox toggle` binding. */
|
|
15
|
-
export declare function bundledHumanloopVersion(): string | null;
|
|
16
|
-
/** Read the PATH-visible `hl` package directly. Spawning `npm ls -g` here
|
|
17
|
-
* makes every front-door boot pay for npm's global dependency-tree scan; a
|
|
18
|
-
* linked development install can make that scan take many seconds. The real
|
|
19
|
-
* executable points into the package for both ordinary and npm-linked installs,
|
|
20
|
-
* so walking upward to its package.json is authoritative for the CLI crtr will
|
|
21
|
-
* actually invoke. */
|
|
22
|
-
export declare function globalHumanloopVersion(): string | null;
|
|
23
|
-
export declare function globalHumanloopHealth(): GlobalHumanloopHealth;
|
|
24
|
-
/** A non-fatal action-oriented diagnostic for a missing, unreadable, or stale
|
|
25
|
-
* global `hl` executable. */
|
|
26
|
-
export declare function globalHumanloopWarning(): string | null;
|
|
27
|
-
export declare function globalHumanloopInstallCommand(): string;
|
|
7
|
+
/** A non-fatal, action-oriented diagnostic for an unprovisioned managed
|
|
8
|
+
* renderer — the one remaining out-of-tree dependency. Deliberately calls
|
|
9
|
+
* `isRendererReady()`, NOT `ensureRenderer()`: this runs on the front-door boot
|
|
10
|
+
* path and at crtrd startup, where a `uv pip install` (up to 180s) must never
|
|
11
|
+
* happen. Readiness is a handful of fs stats, no subprocess. */
|
|
12
|
+
export declare function rendererWarning(): string | null;
|
|
28
13
|
/**
|
|
29
14
|
* The pi agent config dir (`~/.pi/agent`, or `$PI_CODING_AGENT_DIR`) — a local
|
|
30
15
|
* reimplementation of pi's `getAgentDir()`. Reimplemented (not imported) ON
|
|
@@ -10,131 +10,20 @@
|
|
|
10
10
|
// vanishes with no diagnostic. This turns that silent failure into a loud,
|
|
11
11
|
// actionable warning. The fix it points at is `crtr sys setup`, which re-resolves
|
|
12
12
|
// package paths to the repo's current location.
|
|
13
|
-
import {
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
14
|
import { homedir } from 'node:os';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const entryPath = fileURLToPath(import.meta.resolve('@crouton-kit/humanloop'));
|
|
26
|
-
const packageJsonPath = join(dirname(entryPath), '..', 'package.json');
|
|
27
|
-
const value = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
28
|
-
return typeof value.version === 'string'
|
|
29
|
-
? value.version
|
|
30
|
-
: null;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/** Compare ordinary npm semvers without loading a semver dependency on the
|
|
37
|
-
* foreground/daemon startup path. Unparseable versions deliberately fail the
|
|
38
|
-
* health check: setup can repair them and boot only warns. */
|
|
39
|
-
function compareVersions(left, right) {
|
|
40
|
-
const parse = (version) => {
|
|
41
|
-
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
|
|
42
|
-
if (match === null)
|
|
43
|
-
return null;
|
|
44
|
-
return { core: [Number(match[1]), Number(match[2]), Number(match[3])], prerelease: match[4]?.split('.') ?? [] };
|
|
45
|
-
};
|
|
46
|
-
const a = parse(left);
|
|
47
|
-
const b = parse(right);
|
|
48
|
-
if (a === null || b === null)
|
|
15
|
+
import { join, resolve } from 'node:path';
|
|
16
|
+
import { isRendererReady } from '../termrender/termrender.js';
|
|
17
|
+
/** A non-fatal, action-oriented diagnostic for an unprovisioned managed
|
|
18
|
+
* renderer — the one remaining out-of-tree dependency. Deliberately calls
|
|
19
|
+
* `isRendererReady()`, NOT `ensureRenderer()`: this runs on the front-door boot
|
|
20
|
+
* path and at crtrd startup, where a `uv pip install` (up to 180s) must never
|
|
21
|
+
* happen. Readiness is a handful of fs stats, no subprocess. */
|
|
22
|
+
export function rendererWarning() {
|
|
23
|
+
if (isRendererReady())
|
|
49
24
|
return null;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (difference !== 0)
|
|
53
|
-
return difference;
|
|
54
|
-
}
|
|
55
|
-
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
56
|
-
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
57
|
-
}
|
|
58
|
-
for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index++) {
|
|
59
|
-
const aPart = a.prerelease[index];
|
|
60
|
-
const bPart = b.prerelease[index];
|
|
61
|
-
if (aPart === undefined || bPart === undefined)
|
|
62
|
-
return aPart === bPart ? 0 : aPart === undefined ? -1 : 1;
|
|
63
|
-
if (aPart === bPart)
|
|
64
|
-
continue;
|
|
65
|
-
const aNumber = /^\d+$/.test(aPart);
|
|
66
|
-
const bNumber = /^\d+$/.test(bPart);
|
|
67
|
-
if (aNumber && bNumber)
|
|
68
|
-
return Number(aPart) - Number(bPart);
|
|
69
|
-
if (aNumber !== bNumber)
|
|
70
|
-
return aNumber ? -1 : 1;
|
|
71
|
-
return aPart < bPart ? -1 : 1;
|
|
72
|
-
}
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
/** Read the PATH-visible `hl` package directly. Spawning `npm ls -g` here
|
|
76
|
-
* makes every front-door boot pay for npm's global dependency-tree scan; a
|
|
77
|
-
* linked development install can make that scan take many seconds. The real
|
|
78
|
-
* executable points into the package for both ordinary and npm-linked installs,
|
|
79
|
-
* so walking upward to its package.json is authoritative for the CLI crtr will
|
|
80
|
-
* actually invoke. */
|
|
81
|
-
export function globalHumanloopVersion() {
|
|
82
|
-
const executable = (process.env['PATH'] ?? '')
|
|
83
|
-
.split(delimiter)
|
|
84
|
-
.filter(Boolean)
|
|
85
|
-
.map((entry) => join(entry, 'hl'))
|
|
86
|
-
.find((candidate) => {
|
|
87
|
-
try {
|
|
88
|
-
accessSync(candidate, constants.X_OK);
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
if (executable === undefined)
|
|
96
|
-
return null;
|
|
97
|
-
try {
|
|
98
|
-
let cursor = dirname(realpathSync(executable));
|
|
99
|
-
for (;;) {
|
|
100
|
-
const packageJsonPath = join(cursor, 'package.json');
|
|
101
|
-
if (existsSync(packageJsonPath)) {
|
|
102
|
-
const value = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
103
|
-
const manifest = value;
|
|
104
|
-
if (manifest.name === '@crouton-kit/humanloop')
|
|
105
|
-
return typeof manifest.version === 'string' ? manifest.version : null;
|
|
106
|
-
}
|
|
107
|
-
const parent = dirname(cursor);
|
|
108
|
-
if (parent === cursor)
|
|
109
|
-
return null;
|
|
110
|
-
cursor = parent;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
export function globalHumanloopHealth() {
|
|
118
|
-
const bundledVersion = bundledHumanloopVersion();
|
|
119
|
-
const globalVersion = globalHumanloopVersion();
|
|
120
|
-
const comparison = bundledVersion !== null && globalVersion !== null
|
|
121
|
-
? compareVersions(globalVersion, bundledVersion)
|
|
122
|
-
: null;
|
|
123
|
-
return { bundledVersion, globalVersion, current: comparison !== null && comparison >= 0 };
|
|
124
|
-
}
|
|
125
|
-
/** A non-fatal action-oriented diagnostic for a missing, unreadable, or stale
|
|
126
|
-
* global `hl` executable. */
|
|
127
|
-
export function globalHumanloopWarning() {
|
|
128
|
-
const { bundledVersion, globalVersion, current } = globalHumanloopHealth();
|
|
129
|
-
if (current)
|
|
130
|
-
return null;
|
|
131
|
-
const floor = bundledVersion ?? 'the bundled version';
|
|
132
|
-
const found = globalVersion === null ? 'missing or unprobeable' : globalVersion;
|
|
133
|
-
return `crtr: global \`hl\` CLI is ${found}; crouter requires at least bundled humanloop ${floor} for its inbox toggle.\n` +
|
|
134
|
-
` Fix: run \`${GLOBAL_HUMANLOOP_INSTALL_COMMAND}\`.`;
|
|
135
|
-
}
|
|
136
|
-
export function globalHumanloopInstallCommand() {
|
|
137
|
-
return GLOBAL_HUMANLOOP_INSTALL_COMMAND;
|
|
25
|
+
return 'crtr: the pinned termrender renderer is not provisioned — decks, reviews, and inline diagrams render as plaintext.\n' +
|
|
26
|
+
' Fix: run `crtr sys setup` (requires `uv`: curl -LsSf https://astral.sh/uv/install.sh | sh).';
|
|
138
27
|
}
|
|
139
28
|
/** Expand a leading `~` against `homeDir` (pi's own convention). */
|
|
140
29
|
function expandTilde(pathValue, homeDir) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { STALL_REPROMPT } from '../../shared/generated-context.js';
|
|
1
2
|
export interface StopSignals {
|
|
2
3
|
/** Did the node call `push --final` (finish) this turn? */
|
|
3
4
|
pushedFinal: boolean;
|
|
@@ -12,7 +13,6 @@ export type StopAction = {
|
|
|
12
13
|
reason: 'stalled';
|
|
13
14
|
message: string;
|
|
14
15
|
};
|
|
15
|
-
export declare const STALL_REPROMPT: string;
|
|
16
16
|
/** Decide what to do when a node stops. Pure given the canvas + this turn's
|
|
17
17
|
* signals — the stophook supplies the signals and enacts the action. */
|
|
18
18
|
export declare function evaluateStop(nodeId: string, signals: StopSignals): StopAction;
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
// • otherwise → a TERMINAL node with nothing live to wait for and no
|
|
19
19
|
// final pushed. Re-prompt it to finish or escalate.
|
|
20
20
|
import { hasActiveLiveSubscription, hasLiveMessageWait, hasPendingCancelOnWakeCron, getNode } from '../canvas/index.js';
|
|
21
|
+
import { formatStructuredOutputReprompt, STALL_REPROMPT } from '../../shared/generated-context.js';
|
|
21
22
|
import { hasPendingStructuredOutput, readOutputRequest } from './structured-output.js';
|
|
22
|
-
export
|
|
23
|
-
"Pipe the result to `crtr push final` through a single-quoted heredoc if the work is done, or use `crtr human ask` if you are blocked or need the user.";
|
|
23
|
+
export { STALL_REPROMPT } from '../../shared/generated-context.js';
|
|
24
24
|
function structuredOutputReprompt(nodeId) {
|
|
25
25
|
let schema = '{}';
|
|
26
26
|
try {
|
|
@@ -31,7 +31,7 @@ function structuredOutputReprompt(nodeId) {
|
|
|
31
31
|
catch {
|
|
32
32
|
/* keep a deterministic reprompt even if the schema file was corrupted */
|
|
33
33
|
}
|
|
34
|
-
return
|
|
34
|
+
return formatStructuredOutputReprompt(schema);
|
|
35
35
|
}
|
|
36
36
|
/** Decide what to do when a node stops. Pure given the canvas + this turn's
|
|
37
37
|
* signals — the stophook supplies the signals and enacts the action. */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ToolGroupSummary } from '../../shared/tool-groups.js';
|
|
2
|
+
export type ToolGroupPiece = {
|
|
3
|
+
type: 'tool';
|
|
4
|
+
name: string;
|
|
5
|
+
args: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'result';
|
|
8
|
+
name: string;
|
|
9
|
+
result: unknown;
|
|
10
|
+
isError: boolean;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'text';
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
15
|
+
/** Serialize only the causal material around one group, with fixed per-piece
|
|
16
|
+
* and total budgets so a verbose tool result cannot dominate the light-model call. */
|
|
17
|
+
export declare function serializeToolGroup(previousBoundaryProse: string, pieces: readonly ToolGroupPiece[]): string;
|
|
18
|
+
/** Generate a structured summary object, or null on every failure. */
|
|
19
|
+
export declare function headlessToolGroupSummary(segment: string): Promise<ToolGroupSummary | null>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Broker-side, best-effort tool-group summarization. The only successful output
|
|
2
|
+
// path is the constrained submit_summary tool; failures deliberately yield no
|
|
3
|
+
// substitute object so callers leave the normal folded calls visible.
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { SUMMARY_TOOL_PATH } from './canvas-extensions.js';
|
|
9
|
+
import { lightModel } from './naming.js';
|
|
10
|
+
import { bundledPiSubprocessEnv, resolveBundledPiCliPath } from './pi-cli.js';
|
|
11
|
+
import { normalizeToolGroupSummary } from '../../shared/tool-groups.js';
|
|
12
|
+
const BOUNDARY_CAP = 600;
|
|
13
|
+
const TOOL_ARGS_CAP = 300;
|
|
14
|
+
const RESULT_CAP = 400;
|
|
15
|
+
const TEXT_CAP = 1_000;
|
|
16
|
+
const SEGMENT_CAP = 10_000;
|
|
17
|
+
const SUMMARY_TIMEOUT_MS = 45_000;
|
|
18
|
+
function capped(value, cap) {
|
|
19
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
20
|
+
return normalized.length <= cap ? normalized : `${normalized.slice(0, Math.max(0, cap - 1)).trimEnd()}…`;
|
|
21
|
+
}
|
|
22
|
+
function printable(value, cap) {
|
|
23
|
+
try {
|
|
24
|
+
return capped(typeof value === 'string' ? value : JSON.stringify(value), cap);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return capped(String(value), cap);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Serialize only the causal material around one group, with fixed per-piece
|
|
31
|
+
* and total budgets so a verbose tool result cannot dominate the light-model call. */
|
|
32
|
+
export function serializeToolGroup(previousBoundaryProse, pieces) {
|
|
33
|
+
const lines = [`Previous visible message: ${capped(previousBoundaryProse, BOUNDARY_CAP) || '(none)'}`, 'Between-message activity:'];
|
|
34
|
+
for (const piece of pieces) {
|
|
35
|
+
if (piece.type === 'tool')
|
|
36
|
+
lines.push(`- tool ${piece.name}(${printable(piece.args, TOOL_ARGS_CAP)})`);
|
|
37
|
+
else if (piece.type === 'result')
|
|
38
|
+
lines.push(`- ${piece.isError ? 'error' : 'result'} ${piece.name}: ${printable(piece.result, RESULT_CAP)}`);
|
|
39
|
+
else
|
|
40
|
+
lines.push(`- context: ${capped(piece.text, TEXT_CAP)}`);
|
|
41
|
+
}
|
|
42
|
+
return capped(lines.join('\n'), SEGMENT_CAP);
|
|
43
|
+
}
|
|
44
|
+
// Tool mechanics, identifiers, and inbox bookkeeping produce a machine log rather
|
|
45
|
+
// than the glanceable human recap this surface needs, so the prompt asks only for
|
|
46
|
+
// very short outcomes while the structured fields retain the underlying stats.
|
|
47
|
+
const SUMMARY_SYSTEM_PROMPT = 'Create a structured recap of this coding-agent tool activity. ' +
|
|
48
|
+
'Prefer 1–3 bullets and use at most 5 only for distinct meaningful outcomes. ' +
|
|
49
|
+
'Each bullet must be an extremely readable plain-English outcome of 3–10 words, without markdown markers. ' +
|
|
50
|
+
'Describe what changed or was accomplished, not the tools, commands, or investigation steps; for code work, say what capability was added to a one- or two-phrase plain-English part of the system, such as "Added scheduling controls to the worker service." ' +
|
|
51
|
+
'Never include IDs, hashes, paths, filenames, command names, tool names, raw values, or report text. ' +
|
|
52
|
+
'Omit inbox receipt, delivery, and queueing; node-response and report bookkeeping; help lookups; status checks; and summary-generation activity. ' +
|
|
53
|
+
'A node response may support a substantive outcome, but its arrival or resummarization is never itself an outcome. ' +
|
|
54
|
+
'If no meaningful outcome remains after those omissions, submit the single bullet "No meaningful changes." ' +
|
|
55
|
+
'Count nodes created and distinct files created, modified, or deleted only when evidenced by the supplied tool arguments or results; otherwise use zero, even when the corresponding mechanics are omitted from bullets. ' +
|
|
56
|
+
'Do not invent success when a result shows failure. ' +
|
|
57
|
+
'Call submit_summary exactly once with bullets, nodesSpawned, and filesEdited, then stop.';
|
|
58
|
+
function summaryArgs(segment) {
|
|
59
|
+
return [
|
|
60
|
+
'-p',
|
|
61
|
+
'--no-session',
|
|
62
|
+
'--no-context-files',
|
|
63
|
+
'--no-extensions',
|
|
64
|
+
'--no-skills',
|
|
65
|
+
'--no-prompt-templates',
|
|
66
|
+
'--no-themes',
|
|
67
|
+
'-e', SUMMARY_TOOL_PATH,
|
|
68
|
+
'--tools', 'submit_summary',
|
|
69
|
+
'--mode', 'text',
|
|
70
|
+
'--thinking', 'off',
|
|
71
|
+
'--model', lightModel(),
|
|
72
|
+
'--system-prompt', SUMMARY_SYSTEM_PROMPT,
|
|
73
|
+
`<tool-group>\n${segment}\n</tool-group>\n\nSummarize the activity above.`,
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
/** Generate a structured summary object, or null on every failure. */
|
|
77
|
+
export function headlessToolGroupSummary(segment) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
let dir = null;
|
|
80
|
+
try {
|
|
81
|
+
dir = mkdtempSync(join(tmpdir(), 'crtr-tool-summary-'));
|
|
82
|
+
const resultFile = join(dir, 'summary.json');
|
|
83
|
+
const done = (value) => {
|
|
84
|
+
if (dir !== null) {
|
|
85
|
+
try {
|
|
86
|
+
rmSync(dir, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
catch { /* temp cleanup */ }
|
|
89
|
+
}
|
|
90
|
+
resolve(value);
|
|
91
|
+
};
|
|
92
|
+
const child = execFile(process.execPath, [resolveBundledPiCliPath(), ...summaryArgs(segment)], {
|
|
93
|
+
encoding: 'utf8',
|
|
94
|
+
timeout: SUMMARY_TIMEOUT_MS,
|
|
95
|
+
env: { ...bundledPiSubprocessEnv(), CRTR_SUMMARY_RESULT_FILE: resultFile },
|
|
96
|
+
}, () => {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(readFileSync(resultFile, 'utf8'));
|
|
99
|
+
done(normalizeToolGroupSummary(parsed));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
done(null);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
child.stdin?.end();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
if (dir !== null) {
|
|
109
|
+
try {
|
|
110
|
+
rmSync(dir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
catch { /* temp cleanup */ }
|
|
113
|
+
}
|
|
114
|
+
resolve(null);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RenderedDoc } from './termrender.js';
|
|
2
|
+
export declare function isMarkdownFile(file: string): boolean;
|
|
3
|
+
/** Fence info string for a source file — `text` when the extension is unknown,
|
|
4
|
+
* which every highlighter degrades to plain text on. */
|
|
5
|
+
export declare function fenceLanguageFor(file: string): string;
|
|
6
|
+
/** Wrap source text as a fenced block. The fence is grown past the longest
|
|
7
|
+
* backtick run in the content, so a file that itself contains ``` can't close
|
|
8
|
+
* its own block. */
|
|
9
|
+
export declare function codeFenceDocument(content: string, language: string): string;
|
|
10
|
+
/** Render a source file as a syntax-highlighted code panel whose spans are
|
|
11
|
+
* source lines — the code-file counterpart of `renderReviewDoc`. */
|
|
12
|
+
export declare function codeRenderedDoc(content: string, language: string, width: number): RenderedDoc;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { renderMarkdownWithMap } from './termrender.js';
|
|
3
|
+
// ── Source files as review documents ─────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// A review ticket may point at a .ts/.py/.sql source file, not just a .md
|
|
6
|
+
// artifact. Every surface still wants ONE rendering story, so a source file is
|
|
7
|
+
// presented as a single fenced code block: termrender (Pygments) and the
|
|
8
|
+
// browser (highlight.js) then syntax-highlight it for free, and the fence's
|
|
9
|
+
// language comes from the file extension.
|
|
10
|
+
//
|
|
11
|
+
// The one cost is a line shift: the fence's opening line makes source line N
|
|
12
|
+
// land on line N+1 of the wrapped document. `codeRenderedDoc` folds that back
|
|
13
|
+
// out so every span/anchor the surfaces expose is a real SOURCE line number.
|
|
14
|
+
/** Extension → fence info string. Names are the ones Pygments (termrender) and
|
|
15
|
+
* highlight.js (browser) both accept, so one map serves every surface. */
|
|
16
|
+
const FENCE_BY_EXT = {
|
|
17
|
+
'.ts': 'typescript', '.mts': 'typescript', '.cts': 'typescript', '.tsx': 'tsx',
|
|
18
|
+
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'jsx',
|
|
19
|
+
'.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.java': 'java',
|
|
20
|
+
'.c': 'c', '.h': 'c', '.cc': 'cpp', '.cpp': 'cpp', '.hpp': 'cpp', '.cs': 'csharp',
|
|
21
|
+
'.swift': 'swift', '.kt': 'kotlin', '.php': 'php', '.lua': 'lua', '.pl': 'perl',
|
|
22
|
+
'.sh': 'bash', '.bash': 'bash', '.zsh': 'bash', '.fish': 'fish',
|
|
23
|
+
'.sql': 'sql', '.json': 'json', '.jsonc': 'json', '.yaml': 'yaml', '.yml': 'yaml',
|
|
24
|
+
'.toml': 'toml', '.ini': 'ini', '.xml': 'xml', '.html': 'html', '.htm': 'html',
|
|
25
|
+
'.css': 'css', '.scss': 'scss', '.less': 'less', '.vim': 'vim', '.el': 'lisp',
|
|
26
|
+
'.ex': 'elixir', '.exs': 'elixir', '.erl': 'erlang', '.hs': 'haskell',
|
|
27
|
+
'.scala': 'scala', '.dart': 'dart', '.r': 'r', '.jl': 'julia', '.zig': 'zig',
|
|
28
|
+
'.tf': 'terraform', '.proto': 'protobuf', '.graphql': 'graphql', '.gql': 'graphql',
|
|
29
|
+
'.dockerfile': 'dockerfile', '.diff': 'diff', '.patch': 'diff', '.csv': 'text',
|
|
30
|
+
'.txt': 'text', '.log': 'text', '.env': 'bash', '.mk': 'makefile',
|
|
31
|
+
};
|
|
32
|
+
/** Markdown extensions render as a DOCUMENT (headings, tables, Mermaid); every
|
|
33
|
+
* other text file renders as code. */
|
|
34
|
+
const MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown', '.mdown', '.mkd']);
|
|
35
|
+
export function isMarkdownFile(file) {
|
|
36
|
+
return MARKDOWN_EXTENSIONS.has(extname(file).toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
/** Fence info string for a source file — `text` when the extension is unknown,
|
|
39
|
+
* which every highlighter degrades to plain text on. */
|
|
40
|
+
export function fenceLanguageFor(file) {
|
|
41
|
+
const ext = extname(file).toLowerCase();
|
|
42
|
+
if (ext === '') {
|
|
43
|
+
// Extensionless but conventionally-named files still have obvious types.
|
|
44
|
+
const base = file.slice(file.lastIndexOf('/') + 1).toLowerCase();
|
|
45
|
+
if (base === 'dockerfile')
|
|
46
|
+
return 'dockerfile';
|
|
47
|
+
if (base === 'makefile')
|
|
48
|
+
return 'makefile';
|
|
49
|
+
return 'text';
|
|
50
|
+
}
|
|
51
|
+
return FENCE_BY_EXT[ext] ?? 'text';
|
|
52
|
+
}
|
|
53
|
+
/** Wrap source text as a fenced block. The fence is grown past the longest
|
|
54
|
+
* backtick run in the content, so a file that itself contains ``` can't close
|
|
55
|
+
* its own block. */
|
|
56
|
+
export function codeFenceDocument(content, language) {
|
|
57
|
+
let longest = 0;
|
|
58
|
+
for (const run of content.matchAll(/`+/g))
|
|
59
|
+
longest = Math.max(longest, run[0].length);
|
|
60
|
+
const fence = '`'.repeat(Math.max(3, longest + 1));
|
|
61
|
+
const body = content.endsWith('\n') ? content.slice(0, -1) : content;
|
|
62
|
+
return `${fence}${language}\n${body}\n${fence}\n`;
|
|
63
|
+
}
|
|
64
|
+
/** Shift a rendered wrapped-document map back onto SOURCE line numbers: the
|
|
65
|
+
* opening fence occupies line 1, so document line N is source line N-1.
|
|
66
|
+
*
|
|
67
|
+
* Rows that touch a fence line are the panel's border/title chrome, not
|
|
68
|
+
* source: they lose BOTH their span and their block row so nothing anchors to
|
|
69
|
+
* them — otherwise the top border would be a step in j/k that selects the
|
|
70
|
+
* whole file. */
|
|
71
|
+
function foldFenceOffset(doc, sourceLineCount) {
|
|
72
|
+
const closingFenceLine = sourceLineCount + 2;
|
|
73
|
+
const clamp = (line) => Math.max(1, Math.min(line - 1, sourceLineCount));
|
|
74
|
+
const chrome = doc.spans.map((span) => span !== null && (span[0] <= 1 || span[1] >= closingFenceLine));
|
|
75
|
+
return {
|
|
76
|
+
lines: doc.lines,
|
|
77
|
+
rows: doc.rows.map((row, index) => (chrome[index] === true ? null : row)),
|
|
78
|
+
spans: doc.spans.map((span, index) => {
|
|
79
|
+
if (span === null || chrome[index] === true)
|
|
80
|
+
return null;
|
|
81
|
+
return [clamp(span[0]), clamp(span[1])];
|
|
82
|
+
}),
|
|
83
|
+
blocks: doc.blocks.map((block) => ({ type: block.type, start: clamp(block.start), end: clamp(block.end) })),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Render a source file as a syntax-highlighted code panel whose spans are
|
|
87
|
+
* source lines — the code-file counterpart of `renderReviewDoc`. */
|
|
88
|
+
export function codeRenderedDoc(content, language, width) {
|
|
89
|
+
const sourceLineCount = content.split('\n').length;
|
|
90
|
+
return foldFenceOffset(renderMarkdownWithMap(codeFenceDocument(content, language), width), sourceLineCount);
|
|
91
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Options for `display()` — the live-watch tmux pane surface. The pane always
|
|
2
|
+
* watches the file and live-updates on edits; there is no non-watched mode. */
|
|
3
|
+
export interface DisplayOpts {
|
|
4
|
+
/** `'auto'` (default) splits until the pane budget, then opens a new window. */
|
|
5
|
+
window?: 'auto' | 'split' | 'new';
|
|
6
|
+
/** Pane budget per window before `'auto'` opens a new window. Default 3. */
|
|
7
|
+
maxPanes?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function countPanesInCurrentWindow(): number;
|
|
10
|
+
export declare function display(path: string, opts?: DisplayOpts): {
|
|
11
|
+
paneId?: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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}'], {
|
|
6
|
+
encoding: 'utf8',
|
|
7
|
+
});
|
|
8
|
+
if (result.status !== 0)
|
|
9
|
+
return 0;
|
|
10
|
+
return result.stdout.split('\n').filter((line) => line.trim() !== '').length;
|
|
11
|
+
}
|
|
12
|
+
export function display(path, opts) {
|
|
13
|
+
const window = (opts?.window === 'split' || opts?.window === 'new') ? opts.window : 'auto';
|
|
14
|
+
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 });
|
|
19
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memoized, self-healing. Guarantees at most one authoritative renderer
|
|
3
|
+
* lifecycle per process: trust a valid stamp outright (zero subprocess spawns),
|
|
4
|
+
* else run ONE provision/verify/publish transition under the exclusive lock.
|
|
5
|
+
* There is no permanent legacy-verifier fallback — the spawn-based verify
|
|
6
|
+
* (`binaryOk` + `installedVersion`) is a step INSIDE that single transition,
|
|
7
|
+
* always followed by publishing the stamp, never a lasting alternate path.
|
|
8
|
+
*
|
|
9
|
+
* Single degradation path: `uv` absent → one stderr remediation line + plaintext.
|
|
10
|
+
* win32 → plaintext (no renderer).
|
|
11
|
+
*
|
|
12
|
+
* Invoked at postinstall AND lazily on the first render/check/display call,
|
|
13
|
+
* so `npm ci --ignore-scripts` consumers still self-heal on first use.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ensureRenderer(): void;
|
|
16
|
+
/** Cheap predicate — true when the pinned managed binary is verified ready. Does not install or spawn. */
|
|
17
|
+
export declare function isRendererReady(): boolean;
|
|
18
|
+
/** Render markdown to terminal lines via the pinned binary; plaintext fallback. */
|
|
19
|
+
export declare function renderMarkdown(md: string, width: number): string[];
|
|
20
|
+
export interface RenderedDoc {
|
|
21
|
+
/** Rendered ANSI rows — byte-identical to the plain `doc render` output. */
|
|
22
|
+
lines: string[];
|
|
23
|
+
/** Per-row index into `blocks`; null for the blank separator rows between blocks. */
|
|
24
|
+
rows: (number | null)[];
|
|
25
|
+
/** Per-row finest 1-indexed inclusive source-line range the renderer knows
|
|
26
|
+
* (a bullet, a table row, a code line…); null for separator rows and rows
|
|
27
|
+
* of unmapped blocks. Same length as `lines`. */
|
|
28
|
+
spans: ([number, number] | null)[];
|
|
29
|
+
/** Per top-level block: its termrender block type plus 1-indexed inclusive
|
|
30
|
+
* source-line bounds. Never empty. */
|
|
31
|
+
blocks: {
|
|
32
|
+
type: string;
|
|
33
|
+
start: number;
|
|
34
|
+
end: number;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
/** Render markdown with a row→source-line map via the pinned binary
|
|
38
|
+
* (`doc render --line-map`); plaintext paragraph-block fallback. */
|
|
39
|
+
export declare function renderMarkdownWithMap(md: string, width: number): RenderedDoc;
|
|
40
|
+
/**
|
|
41
|
+
* The shared block-aware mapped render: prose blocks keep the readability cap
|
|
42
|
+
* (`proseWidth`), while diagram blocks are re-rendered at the pane's full
|
|
43
|
+
* `paneWidth` and spliced back in by block index. Both surfaces (terminal
|
|
44
|
+
* review and the ask deck) go through this, so "which blocks may be wide" is
|
|
45
|
+
* decided once, from the renderer's own block types, never by heuristics on
|
|
46
|
+
* the emitted rows.
|
|
47
|
+
*
|
|
48
|
+
* Row order, block list and source spans are the narrow render's; only the
|
|
49
|
+
* rows OF a wide block are replaced, so anchoring stays source-based and
|
|
50
|
+
* unchanged.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderMarkdownBlockAware(md: string, proseWidth: number, paneWidth: number): RenderedDoc;
|
|
53
|
+
/** Block-aware render as plain rows, for surfaces that do not anchor. */
|
|
54
|
+
export declare function renderMarkdownBlockAwareLines(md: string, proseWidth: number, paneWidth: number): string[];
|
|
55
|
+
/** Validate markdown via `termrender doc check`. */
|
|
56
|
+
export declare function checkMarkdown(md: string): {
|
|
57
|
+
ok: true;
|
|
58
|
+
} | {
|
|
59
|
+
ok: false;
|
|
60
|
+
error: string;
|
|
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
|
+
};
|