@north-light/crouter 0.3.165 → 0.3.166

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.
@@ -69,7 +69,7 @@ The `<plugin-name>` directory IS the plugin. The manifest's `name` field must ma
69
69
  | `source` | recommended | Git URL where the plugin lives. Used by `crtr pkg plugin update --name <name>`. |
70
70
  | `owner` | optional | Author info. |
71
71
  | `commands` | optional | Plugin-root-relative path to a `commands.json` command manifest. It must appear with `transport`; together they make the plugin contribute `crtr` commands — see [Plugin commands](#plugin-commands). |
72
- | `transport` | optional | Required exactly when `commands` is present. `{ "kind": "exec", "executable": "bin/cmd.js" }` runs a local executable; `{ "kind": "http", "endpoint": "https://…", "authEnv": "TOKEN_NAME" }` calls a remote HTTP command surface. |
72
+ | `transport` | optional | Required exactly when `commands` is present. `{ "kind": "exec", "executable": "bin/cmd.js" }` runs executable leaves; a passthrough-only exec manifest may omit `executable`. `{ "kind": "http", "endpoint": "https://…", "authEnv": "TOKEN_NAME" }` calls a remote HTTP command surface. |
73
73
 
74
74
  ## Scopes
75
75
 
@@ -183,7 +183,7 @@ An HTTP-transport plugin replaces that declaration with:
183
183
  "transport": { "kind": "http", "endpoint": "https://example.com/v1/cli/manifest", "authEnv": "DEPLOY_TOKEN" }
184
184
  ```
185
185
 
186
- For `exec`, `executable` is plugin-root-relative, resolves inside the plugin root to a regular file, and carries the POSIX exec bit. For `http`, `endpoint` is an absolute endpoint and `authEnv` is optional. crtr stores only the environment variable **name**, never its credential; it reads the credential when fetching the manifest or invoking a leaf.
186
+ For `exec`, `executable` is required when the manifest has any executable leaf; a passthrough-only manifest omits it. When declared, it is plugin-root-relative, resolves inside the plugin root to a regular file, and carries the POSIX exec bit. For `http`, `endpoint` is an absolute endpoint and `authEnv` is optional. crtr stores only the environment variable **name**, never its credential; it reads the credential when fetching the manifest or invoking a leaf.
187
187
 
188
188
  Only an installed, **enabled** plugin's command manifest contributes. Discovery is per-invocation: enable, disable, update, and remove take effect on the next `crtr` call — no daemon restart or cache clearing.
189
189
 
@@ -246,7 +246,7 @@ For an exec leaf, crtr writes exactly one JSON request to the executable's stdin
246
246
  crtr validates command manifests statically; it never executes an exec binary to inspect its command surface:
247
247
 
248
248
  - `crtr pkg plugin show <name>` inventories the plugin manifest, command-manifest path, accepted top-level command names, and validation issues (each with received/expected/next).
249
- - `crtr sys doctor` validates the command manifest and transport declaration for every effective plugin, including executable-path checks for exec transport, and reports structured remediation (disable, update, remove). `--fix` never chmods or rewrites plugin content.
249
+ - `crtr sys doctor` validates the command manifest and transport declaration for every effective plugin, including executable-path checks when exec manifests declare executable leaves, and reports structured remediation (disable, update, remove). `--fix` never chmods or rewrites plugin content.
250
250
  - `crtr pkg plugin install` and `update` report accepted top-level command names and validation issues.
251
251
 
252
252
  Core always wins a path collision. A cross-plugin collision drops every claimant with a `command_collision` issue. A fixed manifest goes live on the next invocation; there is nothing to restart.
@@ -7,20 +7,49 @@ import { readFileSync } from 'node:fs';
7
7
  import { dirname, join } from 'node:path';
8
8
  import test from 'node:test';
9
9
  import { fileURLToPath } from 'node:url';
10
+ import { UserMessageComponent } from '@earendil-works/pi-coding-agent';
11
+ import { Container } from '@earendil-works/pi-tui';
12
+ import { applyTheme, attachMarkdownTheme } from '../config.js';
13
+ import { ChatView } from '../render/chat-view.js';
10
14
  import { ContextMessageComponent } from '../render/context-message.js';
15
+ import { styleAttachMarkdownHeadings } from '../render/markdown-headings.js';
11
16
  import { CONTEXT_NUDGE_CUSTOM_TYPE, REVIVE_KICKOFF_SENTINEL, generatedContextPresentation, } from '../../../shared/generated-context.js';
12
17
  const HERE = dirname(fileURLToPath(import.meta.url));
13
18
  const CHAT_VIEW = readFileSync(join(HERE, '..', 'render/chat-view.ts'), 'utf8');
14
19
  const identity = (s) => s;
20
+ applyTheme();
15
21
  test('collapsed by default: body text is not rendered until expanded', () => {
16
22
  const body = '<situational-context>\nthe user is mid-checkout\n</situational-context>';
17
23
  const collapsed = new ContextMessageComponent(body, false, identity, identity, 'situational context', ' hint');
18
- const collapsedLines = collapsed.render(80).join('\n');
24
+ const collapsedRender = collapsed.render(80);
25
+ const collapsedLines = collapsedRender.join('\n');
26
+ assert.equal(collapsedRender.length, 1, 'a collapsed row has no surrounding blank line');
19
27
  assert.match(collapsedLines, /\[situational context\]/);
20
28
  assert.doesNotMatch(collapsedLines, /mid-checkout/, 'collapsed state must never leak the body');
21
29
  collapsed.setExpanded(true);
22
- const expandedLines = collapsed.render(80).join('\n');
30
+ const expandedRender = collapsed.render(80);
31
+ const expandedLines = expandedRender.join('\n');
23
32
  assert.match(expandedLines, /mid-checkout/, 'expanded state (Ctrl+O) still shows the full body — the LLM-visible content is never lost, only the default view folds it');
33
+ const normalUserRender = new UserMessageComponent(styleAttachMarkdownHeadings(body), attachMarkdownTheme()).render(80);
34
+ assert.deepEqual(expandedRender.slice(0, -1), normalUserRender, 'expanded generated context uses the ordinary user-message surface');
35
+ assert.equal(expandedRender.at(-1), '', 'an expanded block leaves one blank line below its body');
36
+ });
37
+ test('generated rows add no leading spacer after prior transcript content', async () => {
38
+ const container = new Container();
39
+ const tui = { requestRender() { }, terminal: { rows: 40 } };
40
+ const view = new ChatView(tui, container, { toolOutputExpanded: false });
41
+ await view.applySnapshot({
42
+ messages: [
43
+ { role: 'assistant', content: [{ type: 'text', text: 'before' }], stopReason: 'stop' },
44
+ { role: 'user', content: `${REVIVE_KICKOFF_SENTINEL} — full disk bearings` },
45
+ ],
46
+ stats: {},
47
+ state: { isStreaming: false },
48
+ });
49
+ const lines = container.render(80).map((line) => line.replace(/\x1b\[[0-9;]*m/g, ''));
50
+ const generatedIndex = lines.findIndex((line) => line.includes('[crtr revive]'));
51
+ assert.notEqual(generatedIndex, -1);
52
+ assert.match(lines[generatedIndex - 1] ?? '', /before/, 'the collapsed row follows prior content directly');
24
53
  });
25
54
  test('the crtr-context label/hint keep their defaults when callers omit the new params', () => {
26
55
  const bearings = new ContextMessageComponent('some bearings body', false, identity, identity);
@@ -872,8 +872,6 @@ export class ChatView {
872
872
  if (generated !== null &&
873
873
  (message.role !== 'custom' || message.display === true)) {
874
874
  this.pushTranscriptText(generated.body);
875
- if (message.role === 'user' && this.chatChildCount() > 0)
876
- this.append(new Spacer(1));
877
875
  this.append(new ContextMessageComponent(generated.body, this.toolOutputExpanded, this.dimStyle, (s) => this.spinnerStyle(this.labelStyle(s)), generated.label, ` ${generated.summary} — ctrl+o to expand`));
878
876
  return;
879
877
  }
@@ -1,11 +1,11 @@
1
1
  import { type Component } from '@earendil-works/pi-tui';
2
2
  export declare class ContextMessageComponent implements Component {
3
- private readonly body;
4
3
  private readonly dim;
5
4
  private readonly label;
6
5
  private readonly labelText;
7
6
  private readonly collapsedHint;
8
7
  private expanded;
8
+ private readonly expandedBody;
9
9
  constructor(body: string, expanded: boolean, dim: (s: string) => string, label: (s: string) => string, labelText?: string, collapsedHint?: string);
10
10
  /** Ctrl+O global tool-output toggle (ChatView.isExpandable duck-type). */
11
11
  setExpanded(expanded: boolean): void;
@@ -2,48 +2,32 @@
2
2
  // crouter-generated context. Session bearings, situational updates, fresh-revive
3
3
  // kickoffs, context-size nudges, and node inbox digests retain their full model-visible body and
4
4
  // runtime role, while this display-only component presents them consistently:
5
- // collapsed → one dim line; expanded (Ctrl+O) → label + full wrapped body. It
6
- // implements `setExpanded`, so the global tool-display cycle folds these blocks
7
- // alongside tool output. Classification lives in shared/generated-context.ts;
8
- // this component only renders the presentation it is handed.
5
+ // collapsed → one dim line; expanded (Ctrl+O) → the ordinary user-message
6
+ // surface, regardless of the message's runtime role. It implements
7
+ // `setExpanded`, so the global tool-display cycle folds these blocks alongside
8
+ // tool output. Classification lives in shared/generated-context.ts; this
9
+ // component only renders the presentation it is handed.
10
+ import { UserMessageComponent } from '@earendil-works/pi-coding-agent';
9
11
  import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
12
+ import { attachMarkdownTheme } from '../config.js';
13
+ import { styleAttachMarkdownHeadings } from './markdown-headings.js';
10
14
  /** Default label/hint for the `crtr-context` bearings block. */
11
15
  const LABEL = 'crtr context';
12
16
  const COLLAPSED_HINT = ' orienting bearings — ctrl+o to expand';
13
- /** Char-based wrap that never emits an over-width line (pi-tui hard-crashes on
14
- * one). Visible-width aware so wide glyphs in the bearings tree count right. */
15
- function wrapLine(line, width) {
16
- if (line === '')
17
- return [''];
18
- const out = [];
19
- let cur = '';
20
- for (const ch of line) {
21
- if (cur !== '' && visibleWidth(cur + ch) > width) {
22
- out.push(cur);
23
- cur = ch;
24
- }
25
- else {
26
- cur += ch;
27
- }
28
- }
29
- if (cur !== '')
30
- out.push(cur);
31
- return out;
32
- }
33
17
  export class ContextMessageComponent {
34
- body;
35
18
  dim;
36
19
  label;
37
20
  labelText;
38
21
  collapsedHint;
39
22
  expanded;
23
+ expandedBody;
40
24
  constructor(body, expanded, dim, label, labelText = LABEL, collapsedHint = COLLAPSED_HINT) {
41
- this.body = body;
42
25
  this.dim = dim;
43
26
  this.label = label;
44
27
  this.labelText = labelText;
45
28
  this.collapsedHint = collapsedHint;
46
29
  this.expanded = expanded;
30
+ this.expandedBody = new UserMessageComponent(styleAttachMarkdownHeadings(body), attachMarkdownTheme());
47
31
  }
48
32
  /** Ctrl+O global tool-output toggle (ChatView.isExpandable duck-type). */
49
33
  setExpanded(expanded) {
@@ -63,11 +47,9 @@ export class ContextMessageComponent {
63
47
  return [this.label(truncateToWidth(labelTag, w, '…'))];
64
48
  return [this.label(labelTag) + this.dim(truncateToWidth(rest, w - labelW, '…'))];
65
49
  }
66
- const lines = [this.label(truncateToWidth(`[${this.labelText}]`, w, '…')), ''];
67
- for (const raw of this.body.split('\n')) {
68
- for (const seg of wrapLine(raw, w))
69
- lines.push(seg);
70
- }
50
+ const lines = this.expandedBody.render(w);
51
+ if (lines.at(-1) !== '')
52
+ lines.push('');
71
53
  return lines;
72
54
  }
73
55
  invalidate() {