@north-light/crouter 0.3.170 → 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.
Files changed (121) hide show
  1. package/dist/build-root.d.ts +1 -16
  2. package/dist/build-root.js +1 -25
  3. package/dist/builtin-memory/insights/capture.md +66 -0
  4. package/dist/builtin-memory/insights/init.md +45 -0
  5. package/dist/builtin-memory/internal/plugins.md +12 -11
  6. package/dist/cli.js +2 -6
  7. package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
  8. package/dist/clients/attach/__tests__/context-message.test.js +50 -3
  9. package/dist/clients/attach/__tests__/diagram.test.js +1 -1
  10. package/dist/clients/attach/chrome/header.d.ts +10 -0
  11. package/dist/clients/attach/chrome/header.js +35 -0
  12. package/dist/clients/attach/chrome/status-line.d.ts +3 -0
  13. package/dist/clients/attach/chrome/status-line.js +5 -0
  14. package/dist/clients/attach/chrome/widgets.js +7 -18
  15. package/dist/clients/attach/input/controller.d.ts +10 -0
  16. package/dist/clients/attach/input/controller.js +3 -0
  17. package/dist/clients/attach/overlays/help.d.ts +33 -0
  18. package/dist/clients/attach/overlays/help.js +204 -0
  19. package/dist/clients/attach/render/chat-view.d.ts +81 -9
  20. package/dist/clients/attach/render/chat-view.js +434 -90
  21. package/dist/clients/attach/render/context-message.js +3 -3
  22. package/dist/clients/attach/render/diagram.js +2 -2
  23. package/dist/clients/attach/render/measured-container.d.ts +94 -0
  24. package/dist/clients/attach/render/measured-container.js +307 -0
  25. package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
  26. package/dist/clients/attach/render/transcript-copy.js +81 -0
  27. package/dist/clients/attach/render/viewport.d.ts +29 -0
  28. package/dist/clients/attach/render/viewport.js +160 -0
  29. package/dist/clients/attach/session/bindings.d.ts +5 -1
  30. package/dist/clients/attach/session/bindings.js +16 -0
  31. package/dist/clients/attach/session/context.d.ts +5 -3
  32. package/dist/clients/attach/session/frame.d.ts +34 -0
  33. package/dist/clients/attach/session/frame.js +173 -0
  34. package/dist/clients/attach/session/frames.d.ts +2 -0
  35. package/dist/clients/attach/session/frames.js +3 -0
  36. package/dist/clients/attach/session/input-wiring.d.ts +10 -4
  37. package/dist/clients/attach/session/input-wiring.js +13 -13
  38. package/dist/clients/attach/session/keys.d.ts +4 -1
  39. package/dist/clients/attach/session/keys.js +9 -4
  40. package/dist/clients/attach/session/layout.d.ts +10 -8
  41. package/dist/clients/attach/session/layout.js +32 -31
  42. package/dist/clients/attach/session/mouse.d.ts +15 -0
  43. package/dist/clients/attach/session/mouse.js +61 -0
  44. package/dist/clients/attach/slash/dispatch.d.ts +10 -0
  45. package/dist/clients/attach/slash/dispatch.js +35 -2
  46. package/dist/clients/attach/viewer.js +605 -581
  47. package/dist/clients/web/dev-server.d.ts +4 -1
  48. package/dist/clients/web/dev-server.js +8 -19
  49. package/dist/clients/web/web-cmd.js +1 -1
  50. package/dist/commands/human/prompts.js +2 -1
  51. package/dist/commands/pkg/plugin-manage.js +143 -68
  52. package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
  53. package/dist/commands/sys/setup-core.js +21 -16
  54. package/dist/core/clipboard-text.d.ts +8 -0
  55. package/dist/core/clipboard-text.js +18 -0
  56. package/dist/core/command-manifests/registry.d.ts +1 -3
  57. package/dist/core/command-plugins/bundle.d.ts +29 -0
  58. package/dist/core/command-plugins/bundle.js +198 -0
  59. package/dist/core/command-plugins/discovery.d.ts +0 -8
  60. package/dist/core/command-plugins/discovery.js +2 -39
  61. package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
  62. package/dist/core/command-plugins/transport/http-fetch.js +17 -95
  63. package/dist/core/command.d.ts +1 -13
  64. package/dist/core/command.js +1 -16
  65. package/dist/core/config.js +2 -1
  66. package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
  67. package/dist/core/keybindings/attach-control.d.ts +3 -0
  68. package/dist/core/keybindings/attach-control.js +1 -0
  69. package/dist/core/keybindings/catalog.d.ts +2 -2
  70. package/dist/core/keybindings/catalog.js +8 -0
  71. package/dist/core/runtime/broker-protocol.d.ts +13 -1
  72. package/dist/core/runtime/broker.js +213 -5
  73. package/dist/core/runtime/canvas-extensions.d.ts +3 -0
  74. package/dist/core/runtime/canvas-extensions.js +3 -0
  75. package/dist/core/runtime/front-door.js +4 -4
  76. package/dist/core/runtime/kickoff.d.ts +1 -5
  77. package/dist/core/runtime/kickoff.js +1 -5
  78. package/dist/core/runtime/naming.d.ts +7 -0
  79. package/dist/core/runtime/naming.js +5 -2
  80. package/dist/core/runtime/node-read.js +15 -0
  81. package/dist/core/runtime/package-health.d.ts +6 -21
  82. package/dist/core/runtime/package-health.js +12 -123
  83. package/dist/core/runtime/stop-guard.d.ts +1 -1
  84. package/dist/core/runtime/stop-guard.js +3 -3
  85. package/dist/core/runtime/tool-group-summary.d.ts +19 -0
  86. package/dist/core/runtime/tool-group-summary.js +117 -0
  87. package/dist/core/termrender/code-doc.d.ts +12 -0
  88. package/dist/core/termrender/code-doc.js +91 -0
  89. package/dist/core/termrender/display.d.ts +12 -0
  90. package/dist/core/termrender/display.js +19 -0
  91. package/dist/core/termrender/termrender.d.ts +73 -0
  92. package/dist/core/termrender/termrender.js +795 -0
  93. package/dist/core/termrender/version.d.ts +1 -0
  94. package/dist/core/termrender/version.js +1 -0
  95. package/dist/core/user-settings.d.ts +14 -0
  96. package/dist/core/user-settings.js +20 -0
  97. package/dist/daemon/crtrd.js +5 -6
  98. package/dist/index.d.ts +2 -0
  99. package/dist/index.js +1 -0
  100. package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
  101. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
  102. package/dist/pi-extensions/canvas-stophook.js +2 -2
  103. package/dist/pi-extensions/canvas-structured-output.js +52 -8
  104. package/dist/pi-extensions/summary-tool.d.ts +33 -0
  105. package/dist/pi-extensions/summary-tool.js +62 -0
  106. package/dist/shared/generated-context.d.ts +19 -4
  107. package/dist/shared/generated-context.js +98 -7
  108. package/dist/shared/tool-groups.d.ts +23 -0
  109. package/dist/shared/tool-groups.js +60 -0
  110. package/dist/types.d.ts +11 -0
  111. package/dist/types.js +1 -0
  112. package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
  113. package/dist/web-client/index.html +1 -1
  114. package/dist/web-client/sw.js +1 -1
  115. package/package.json +3 -1
  116. package/runtime.lock.json +62 -3
  117. package/scripts/postinstall.mjs +14 -1
  118. package/dist/core/command-plugins/store.d.ts +0 -16
  119. package/dist/core/command-plugins/store.js +0 -64
  120. package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
  121. package/dist/core/runtime/fault-recovery-nudge.js +0 -3
@@ -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 { accessSync, constants, existsSync, readFileSync, realpathSync } from 'node:fs';
14
- import { fileURLToPath } from 'node:url';
13
+ import { existsSync, readFileSync } from 'node:fs';
15
14
  import { homedir } from 'node:os';
16
- import { delimiter, dirname, join, resolve } from 'node:path';
17
- const GLOBAL_HUMANLOOP_INSTALL_COMMAND = 'npm i -g @crouton-kit/humanloop@latest';
18
- /** Read the installed library's version rather than coupling this check to the
19
- * dependency range in crouter's package.json. The bundled copy is the floor
20
- * required by crouter's generated `hl inbox toggle` binding. */
21
- export function bundledHumanloopVersion() {
22
- try {
23
- // package.json is deliberately not an exported subpath. Resolve the public
24
- // ESM entry, then walk from dist/index.js to the package root.
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
- for (let index = 0; index < a.core.length; index++) {
51
- const difference = a.core[index] - b.core[index];
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 const STALL_REPROMPT = "You've stopped but you're not waiting on anyone and haven't finished. " +
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 `You must call the \`submit\` tool with a result matching the required schema before you can stop. You cannot finish or go dormant any other way while this request is pending.\n\nRequired schema:\n\n\`\`\`json\n${schema}\n\`\`\``;
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
+ };