@north-light/crouter 0.3.164 → 0.3.165

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 (155) hide show
  1. package/dist/build-root.d.ts +6 -6
  2. package/dist/build-root.js +12 -13
  3. package/dist/builtin-memory/00-runtime-base.md +3 -4
  4. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  5. package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
  6. package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
  7. package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
  8. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
  9. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
  10. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
  11. package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
  12. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
  13. package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
  14. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
  15. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
  16. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
  17. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
  18. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
  20. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
  21. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
  22. package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
  23. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
  24. package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
  25. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
  26. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
  27. package/dist/builtin-memory/advisor/council.md +40 -0
  28. package/dist/builtin-memory/design.md +8 -11
  29. package/dist/builtin-memory/development.md +6 -9
  30. package/dist/builtin-memory/internal/INDEX.md +4 -5
  31. package/dist/builtin-memory/internal/agent-shaping.md +5 -7
  32. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
  33. package/dist/builtin-memory/internal/marketplaces.md +13 -14
  34. package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
  35. package/dist/builtin-memory/internal/plugins.md +61 -45
  36. package/dist/builtin-memory/planning.md +4 -7
  37. package/dist/builtin-memory/spec.md +9 -12
  38. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
  39. package/dist/cli.js +1 -1
  40. package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
  41. package/dist/clients/attach/__tests__/context-message.test.js +31 -20
  42. package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
  43. package/dist/clients/attach/render/chat-view.d.ts +20 -20
  44. package/dist/clients/attach/render/chat-view.js +42 -54
  45. package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
  46. package/dist/clients/attach/render/condensed-history.js +60 -0
  47. package/dist/clients/attach/render/context-message.js +9 -14
  48. package/dist/clients/attach/session/bindings.d.ts +5 -3
  49. package/dist/clients/attach/session/bindings.js +7 -13
  50. package/dist/clients/attach/session/keys.d.ts +2 -0
  51. package/dist/clients/attach/session/keys.js +36 -37
  52. package/dist/clients/attach/session/profile-files.d.ts +8 -0
  53. package/dist/clients/attach/session/profile-files.js +157 -0
  54. package/dist/clients/attach/viewer.js +525 -525
  55. package/dist/commands/node.js +2 -2
  56. package/dist/commands/pkg/plugin-inspect.js +6 -7
  57. package/dist/commands/pkg/plugin-manage.d.ts +1 -1
  58. package/dist/commands/pkg/plugin-manage.js +131 -19
  59. package/dist/commands/pkg/plugin.js +2 -2
  60. package/dist/commands/pkg.js +6 -11
  61. package/dist/commands/profile/env.js +3 -3
  62. package/dist/commands/sys/config.js +17 -76
  63. package/dist/commands/sys/doctor.js +5 -91
  64. package/dist/commands/sys/setup-wizard.d.ts +9 -11
  65. package/dist/commands/sys/setup-wizard.js +47 -81
  66. package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
  67. package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
  68. package/dist/core/__tests__/command-plugins.test.js +36 -16
  69. package/dist/core/__tests__/review-model-floor.test.js +2 -2
  70. package/dist/core/__tests__/tmux-surface.test.js +10 -1
  71. package/dist/core/command-manifests/manifest.d.ts +24 -0
  72. package/dist/core/{configured-clis → command-manifests}/manifest.js +21 -25
  73. package/dist/core/command-manifests/registry.d.ts +1 -2
  74. package/dist/core/command-manifests/registry.js +2 -2
  75. package/dist/core/command-manifests/schema.d.ts +11 -13
  76. package/dist/core/command-manifests/schema.js +53 -192
  77. package/dist/core/command-plugins/compose.d.ts +0 -6
  78. package/dist/core/command-plugins/compose.js +28 -75
  79. package/dist/core/command-plugins/discovery.d.ts +25 -58
  80. package/dist/core/command-plugins/discovery.js +152 -259
  81. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  82. package/dist/core/command-plugins/endpoint.js +48 -0
  83. package/dist/core/command-plugins/store.d.ts +16 -0
  84. package/dist/core/command-plugins/store.js +64 -0
  85. package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
  86. package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
  87. package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
  88. package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
  89. package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
  90. package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
  91. package/dist/core/command.d.ts +8 -9
  92. package/dist/core/command.js +6 -9
  93. package/dist/core/config.js +6 -10
  94. package/dist/core/env-name.d.ts +6 -0
  95. package/dist/core/env-name.js +9 -0
  96. package/dist/core/io.d.ts +1 -1
  97. package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
  98. package/dist/core/keybindings/attach-control.d.ts +37 -0
  99. package/dist/core/keybindings/attach-control.js +38 -0
  100. package/dist/core/keybindings/catalog.d.ts +5 -4
  101. package/dist/core/keybindings/catalog.js +16 -8
  102. package/dist/core/keybindings/index.d.ts +1 -0
  103. package/dist/core/keybindings/index.js +1 -0
  104. package/dist/core/keybindings/types.d.ts +1 -1
  105. package/dist/core/preview-registry.js +41 -74
  106. package/dist/core/runtime/bearings.d.ts +2 -5
  107. package/dist/core/runtime/bearings.js +2 -5
  108. package/dist/core/runtime/front-door.d.ts +1 -1
  109. package/dist/core/runtime/front-door.js +2 -2
  110. package/dist/core/runtime/kickoff.d.ts +3 -3
  111. package/dist/core/runtime/kickoff.js +4 -3
  112. package/dist/core/runtime/situational-context.d.ts +1 -1
  113. package/dist/core/runtime/situational-context.js +1 -1
  114. package/dist/core/runtime/spawn.js +9 -9
  115. package/dist/core/runtime/tmux.js +29 -2
  116. package/dist/core/scope.d.ts +0 -5
  117. package/dist/core/scope.js +0 -10
  118. package/dist/core/user-settings.d.ts +193 -0
  119. package/dist/core/user-settings.js +252 -0
  120. package/dist/index.d.ts +4 -0
  121. package/dist/index.js +3 -0
  122. package/dist/pi-extensions/canvas-stophook.js +3 -2
  123. package/dist/shared/generated-context.d.ts +34 -0
  124. package/dist/shared/generated-context.js +98 -0
  125. package/dist/types.d.ts +23 -16
  126. package/dist/types.js +3 -14
  127. package/dist/web-client/assets/{index-NIuSCOHM.js → index-BMTOGuOZ.js} +19 -19
  128. package/dist/web-client/assets/index-DvA6Zw-R.css +2 -0
  129. package/dist/web-client/index.html +2 -2
  130. package/dist/web-client/sw.js +1 -1
  131. package/docs/public-api.md +1 -0
  132. package/package.json +1 -1
  133. package/runtime.lock.json +2 -2
  134. package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
  135. package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
  136. package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
  137. package/dist/builtin-memory/internal/workflow-codification.md +0 -82
  138. package/dist/builtin-memory/product.md +0 -80
  139. package/dist/clients/attach/render/frozen-history.js +0 -100
  140. package/dist/commands/pkg/cli-inspect.d.ts +0 -17
  141. package/dist/commands/pkg/cli-inspect.js +0 -190
  142. package/dist/commands/pkg/cli-manage.d.ts +0 -3
  143. package/dist/commands/pkg/cli-manage.js +0 -206
  144. package/dist/commands/pkg/cli.d.ts +0 -1
  145. package/dist/commands/pkg/cli.js +0 -14
  146. package/dist/core/configured-clis/cache.d.ts +0 -16
  147. package/dist/core/configured-clis/cache.js +0 -57
  148. package/dist/core/configured-clis/compose.d.ts +0 -14
  149. package/dist/core/configured-clis/compose.js +0 -60
  150. package/dist/core/configured-clis/discovery.d.ts +0 -47
  151. package/dist/core/configured-clis/discovery.js +0 -173
  152. package/dist/core/configured-clis/manifest.d.ts +0 -24
  153. package/dist/core/configured-clis/registration.d.ts +0 -40
  154. package/dist/core/configured-clis/registration.js +0 -201
  155. package/dist/web-client/assets/index-CqLKj8Xu.css +0 -2
@@ -26,21 +26,17 @@ import { attachMarkdownTheme } from '../config.js';
26
26
  import { ContextMessageComponent } from './context-message.js';
27
27
  import { CRTR_OUTPUT_CUSTOM_TYPE, CrtrOutputMessageComponent, createCrtrBashToolDefinition, } from './crtr-output.js';
28
28
  import { CRTR_CYCLE_DIVIDER_CUSTOM_TYPE } from '../../../core/runtime/session-cycles.js';
29
- import { SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../../../core/runtime/situational-context.js';
29
+ import { generatedContextPresentation } from '../../../shared/generated-context.js';
30
30
  import { transformMessageDiagrams } from './diagram.js';
31
31
  import { styleAttachMarkdownHeadings, styleAttachMessageMarkdown, styleAttachSummaryMarkdown, } from './markdown-headings.js';
32
32
  import { createCrtrEditToolDefinition } from './edit-diff.js';
33
33
  import { FoldedToolCallController, createCrtrPlainBashToolDefinition, createCrtrReadToolDefinition, createCrtrWriteToolDefinition, } from './tool-calls.js';
34
34
  import { ringCompletionBell } from '../chrome/completion-bell.js';
35
35
  import { extractFilePaths } from '../visible-paths.js';
36
- import { FrozenHistoryComponent, keepWhenCondensed, liveRegionStart } from './frozen-history.js';
36
+ import { keepWhenCondensed, liveRegionStart } from './condensed-history.js';
37
37
  import { ViewerAssistantMessageComponent } from './assistant-message.js';
38
38
  import { readConfig, updateRawConfigAtomically } from '../../../core/config.js';
39
39
  import { DEFAULT_CONDENSED_HISTORY, DEFAULT_LIVE_CYCLES } from '../../../types.js';
40
- /** customType stamped on the injected `<crtr-context>` bearings block (mirrors
41
- * CONTEXT_INTRO_CUSTOM_TYPE in canvas-context-intro.ts — kept as a local literal
42
- * so the viewer never imports the in-process extension). */
43
- const CONTEXT_INTRO_CUSTOM_TYPE = 'crtr-context';
44
40
  /** Theme-derived chrome colors (spinner / dim / error) are supplied by the
45
41
  * attach command via `ChatViewOptions.palette` (built from the live theme in
46
42
  * config.ts). pi does not re-export the raw `theme` instance, so when no
@@ -257,8 +253,8 @@ export class ChatView {
257
253
  /** Activity/status area below history. Holds working/compaction/retry loaders. */
258
254
  statusContainer = new Container();
259
255
  /** Where `append()` puts a component. Normally `historyContainer`; retargeted to
260
- * a scratch container while a frozen block is being built, so the very same
261
- * message-rendering path serves both. */
256
+ * a nested container while condensed history is being built, so the same
257
+ * message-rendering path serves both regions. */
262
258
  appendTarget = this.historyContainer;
263
259
  /** Trailing cycles kept at full fidelity (config `live_cycles`). */
264
260
  liveCycles;
@@ -372,18 +368,18 @@ export class ChatView {
372
368
  const renderedPendingTools = new Map();
373
369
  const messages = snapshot.messages;
374
370
  const lastIndex = messages.length - 1;
375
- // Cycles older than the trailing `live_cycles` are condensed to prose and
376
- // frozen into one flat line block BEFORE the live region is built, so the loop
377
- // below only ever constructs components for the cycles you are actually
378
- // working in. `liveStart` is 0 — condense nothing — until the node has yielded
379
- // past the configured window, so a fresh node renders exactly as it always did.
371
+ // Cycles older than the trailing `live_cycles` are condensed into one nested
372
+ // container before the live region is built. It retains only configured prose
373
+ // and cycle seams, avoiding old tool components while still reflowing on pane
374
+ // resize. `liveStart` is 0 until the node has yielded past the configured
375
+ // window, so a fresh node renders at full fidelity.
380
376
  const liveStart = liveRegionStart(messages, this.liveCycles);
381
377
  if (liveStart > 0) {
382
- const frozen = await this.buildFrozenBlock(messages.slice(0, liveStart), generation);
378
+ const condensed = await this.buildCondensedBlock(messages.slice(0, liveStart), generation);
383
379
  if (generation !== this.snapshotGeneration)
384
380
  return;
385
- if (frozen)
386
- this.append(frozen);
381
+ if (condensed)
382
+ this.append(condensed);
387
383
  }
388
384
  for (let i = liveStart; i < messages.length; i++) {
389
385
  // Yield to the event loop every SNAPSHOT_YIELD_EVERY messages so queued
@@ -483,20 +479,20 @@ export class ChatView {
483
479
  this.replayDeferredFrames();
484
480
  }
485
481
  /**
486
- * Render the condensed form of the already-finished cycles ONCE and hand back a
487
- * frozen line block (or undefined when nothing survived condensation).
482
+ * Build the condensed form of the already-finished cycles as a nested component
483
+ * container, or return undefined when nothing survives condensation.
488
484
  *
489
- * Reuses `addMessageToChat` verbatim by retargeting `append()` at a scratch
490
- * container, so condensed history is styled exactly like live history — the same
491
- * renderer, with the tool traffic filtered out and the result flattened to text.
492
- * Yields on the same cadence as the live build so a very long history still lets
493
- * queued keystrokes through, and abandons itself when a newer welcome supersedes
494
- * this generation.
485
+ * Reuses `addMessageToChat` by retargeting `append()` at the nested container, so
486
+ * condensed history uses the same renderer as live history with tool traffic
487
+ * filtered out. The outer history cache keeps stable-width frames cheap, while
488
+ * preserving the components lets pane-width changes reflow their content. The
489
+ * build yields on the live cadence and abandons itself when a newer welcome
490
+ * supersedes this generation.
495
491
  */
496
- async buildFrozenBlock(messages, generation) {
497
- const scratch = new Container();
492
+ async buildCondensedBlock(messages, generation) {
493
+ const condensed = new Container();
498
494
  const previousTarget = this.appendTarget;
499
- this.appendTarget = scratch;
495
+ this.appendTarget = condensed;
500
496
  try {
501
497
  let built = 0;
502
498
  for (const message of messages) {
@@ -518,10 +514,7 @@ export class ChatView {
518
514
  finally {
519
515
  this.appendTarget = previousTarget;
520
516
  }
521
- const width = this.renderWidth();
522
- const lines = scratch.render(width);
523
- scratch.clear();
524
- return lines.length > 0 ? new FrozenHistoryComponent(lines, width) : undefined;
517
+ return condensed.children.length > 0 ? condensed : undefined;
525
518
  }
526
519
  /** Defer a live broker frame when a snapshot build is in flight. Returns true
527
520
  * (and enqueues the replay closure) while building so the caller returns
@@ -564,8 +557,8 @@ export class ChatView {
564
557
  // -------------------------------------------------------------------------
565
558
  /** Ctrl+O / app.tools.expand — flip global tool-output expansion and apply it to
566
559
  * every rendered component that supports it. Returns the new expanded state so
567
- * the caller can surface a notice. Condensed cycles hold no tool components and
568
- * are frozen as text, so they are unaffected by design. */
560
+ * the caller can surface a notice. Condensed cycles hold no tool components, so
561
+ * they are unaffected by design. */
569
562
  toggleToolsExpanded() {
570
563
  this.toolOutputExpanded = !this.toolOutputExpanded;
571
564
  this.applyToolDisplay();
@@ -870,6 +863,20 @@ export class ChatView {
870
863
  // Rebuild path (ports addMessageToChat, interactive-mode.js:2483).
871
864
  // -------------------------------------------------------------------------
872
865
  addMessageToChat(message) {
866
+ // Crouter-generated context retains its runtime role (user vs custom) so
867
+ // delivery to the model is unchanged, but every attached view presents it
868
+ // through one compact disclosure contract. The shared classifier uses the
869
+ // producers' exact custom types/sentinels, so ordinary user prompts cannot
870
+ // accidentally acquire this treatment and saved transcripts gain it too.
871
+ const generated = generatedContextPresentation(message);
872
+ if (generated !== null &&
873
+ (message.role !== 'custom' || message.display === true)) {
874
+ this.pushTranscriptText(generated.body);
875
+ if (message.role === 'user' && this.chatChildCount() > 0)
876
+ this.append(new Spacer(1));
877
+ 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
+ return;
879
+ }
873
880
  switch (message.role) {
874
881
  case 'bashExecution': {
875
882
  this.pushTranscriptText(message.output ?? '');
@@ -888,25 +895,6 @@ export class ChatView {
888
895
  case 'custom': {
889
896
  if (message.display) {
890
897
  this.pushTranscriptText(this.userMessageText(message));
891
- // The <crtr-context> bearings block folds to a one-liner under Ctrl+O
892
- // (the broker runs its renderer extension, so reimplement it natively).
893
- if (message.customType === CONTEXT_INTRO_CUSTOM_TYPE) {
894
- this.append(new ContextMessageComponent(this.userMessageText(message), this.toolOutputExpanded, this.dimStyle,
895
- // Bold + active accent, so the [crtr context] label reads as a
896
- // distinct color against the dim rest of the line.
897
- (s) => this.spinnerStyle(this.labelStyle(s))));
898
- break;
899
- }
900
- // The hidden ambient `<situational-context>` update (canvas-inbox-watcher's
901
- // ~non-birth wake path). Rendered collapsed exactly like `crtr-context`
902
- // (same component, distinct label) so an existing-node ambient update is
903
- // never more prominent in the transcript than the bearings block it rides
904
- // alongside on fresh birth — the LLM sees the full content either way; only
905
- // the viewer's display treatment differs.
906
- if (message.customType === SITUATIONAL_CONTEXT_CUSTOM_TYPE) {
907
- this.append(new ContextMessageComponent(this.userMessageText(message), this.toolOutputExpanded, this.dimStyle, (s) => this.spinnerStyle(this.labelStyle(s)), 'situational context', ' ambient context update — ctrl+o to expand'));
908
- break;
909
- }
910
898
  if (message.customType === CRTR_OUTPUT_CUSTOM_TYPE) {
911
899
  this.append(new CrtrOutputMessageComponent(this.userMessageText(message), this.toolOutputExpanded, this.crtrOutputTheme()));
912
900
  break;
@@ -1047,7 +1035,7 @@ export class ChatView {
1047
1035
  return component;
1048
1036
  }
1049
1037
  /** Append conversation content above the independent live status area — or into
1050
- * the scratch container while a frozen block is under construction. */
1038
+ * the nested container while condensed history is under construction. */
1051
1039
  append(child) {
1052
1040
  this.appendTarget.addChild(child);
1053
1041
  }
@@ -1058,7 +1046,7 @@ export class ChatView {
1058
1046
  return Math.max(40, process.stdout.columns ?? 80);
1059
1047
  }
1060
1048
  /** Number of rendered conversation components in the container currently being
1061
- * appended to (history, or the frozen block's scratch container). */
1049
+ * appended to (live history or the nested condensed region). */
1062
1050
  chatChildCount() {
1063
1051
  return this.appendTarget.children.length;
1064
1052
  }
@@ -1,4 +1,3 @@
1
- import { type Component } from '@earendil-works/pi-tui';
2
1
  import type { CondensedHistoryMode } from '../../../types.js';
3
2
  /** A message from the broker snapshot, structurally. */
4
3
  type Message = {
@@ -6,7 +5,7 @@ type Message = {
6
5
  customType?: string;
7
6
  };
8
7
  export declare function isCycleDivider(message: Message): boolean;
9
- /** Does this message survive condensation into the frozen block? */
8
+ /** Does this message survive into the condensed region? */
10
9
  export declare function keepWhenCondensed(message: Message, mode: CondensedHistoryMode): boolean;
11
10
  /**
12
11
  * Index into `messages` at which the LIVE (fully rendered) region begins.
@@ -19,20 +18,4 @@ export declare function keepWhenCondensed(message: Message, mode: CondensedHisto
19
18
  * single-yield node renders exactly as it always did.
20
19
  */
21
20
  export declare function liveRegionStart(messages: readonly Message[], liveCycles: number): number;
22
- /**
23
- * A settled block of pre-rendered transcript lines. Renders in O(1) at the width
24
- * it was frozen at, and clips (never reflows) when the pane is narrower. Ctrl+O /
25
- * Ctrl+T do not reach it: it holds no components to expand, only text.
26
- */
27
- export declare class FrozenHistoryComponent implements Component {
28
- private readonly lines;
29
- private readonly frozenWidth;
30
- /** Last clip result, so a stable narrow pane clips once rather than per frame. */
31
- private clipped;
32
- constructor(lines: readonly string[], frozenWidth: number);
33
- get lineCount(): number;
34
- /** Nothing to recompute — the block is text, already laid out. */
35
- invalidate(): void;
36
- render(width: number): string[];
37
- }
38
21
  export {};
@@ -0,0 +1,60 @@
1
+ // condensed-history.ts — lightweight rendering of a node's OLD cycles.
2
+ //
3
+ // A node that has yielded several times carries every prior cycle in its welcome
4
+ // snapshot (cycleAwareMessages, session-cycles.ts). Cycles older than the trailing
5
+ // `live_cycles` (config, default 2) are condensed to cycle dividers plus whichever
6
+ // messages `condensed_history` (config, default `none`) admits. Tool calls and
7
+ // results are excluded; across six measured sessions they accounted for 62.9% of
8
+ // rendered lines and 69% of transcript build time.
9
+ //
10
+ // Retained messages stay as components inside one nested container. Settled lines
11
+ // are cached at a stable pane width, while a width change rerenders the nested
12
+ // content so old prose wraps with the rest of the transcript.
13
+ //
14
+ // The full detail of a condensed cycle remains in the node's session file, and
15
+ // `/tree` navigates the real branches.
16
+ import { CRTR_CYCLE_DIVIDER_CUSTOM_TYPE } from '../../../core/runtime/session-cycles.js';
17
+ /** Roles admitted by each `condensed_history` mode. `toolResult`,
18
+ * `bashExecution`, and every non-divider `custom` message (crtr output,
19
+ * injected context blocks) are never kept, nor are the tool-call components an
20
+ * assistant message would otherwise spawn — only its prose. The cycle dividers
21
+ * survive every mode, so `none` leaves the seams and nothing else. */
22
+ const ROLES_BY_MODE = {
23
+ none: new Set(),
24
+ user: new Set(['user']),
25
+ agent: new Set(['assistant', 'compactionSummary', 'branchSummary']),
26
+ both: new Set(['user', 'assistant', 'compactionSummary', 'branchSummary']),
27
+ };
28
+ export function isCycleDivider(message) {
29
+ return message.role === 'custom' && message.customType === CRTR_CYCLE_DIVIDER_CUSTOM_TYPE;
30
+ }
31
+ /** Does this message survive into the condensed region? */
32
+ export function keepWhenCondensed(message, mode) {
33
+ if (isCycleDivider(message))
34
+ return true;
35
+ return (ROLES_BY_MODE[mode] ?? ROLES_BY_MODE.none).has(message.role ?? '');
36
+ }
37
+ /**
38
+ * Index into `messages` at which the LIVE (fully rendered) region begins.
39
+ *
40
+ * The snapshot is `[cycle 0][divider][cycle 1][divider]…[current cycle]`, so a
41
+ * transcript with D dividers holds D+1 cycles. Keeping the trailing
42
+ * `liveCycles` of them means everything up to and including divider
43
+ * `D - liveCycles` is condensed. Returns 0 — condense nothing — when the node
44
+ * has not yielded often enough to have older cycles, so a fresh or
45
+ * single-yield node renders exactly as it always did.
46
+ */
47
+ export function liveRegionStart(messages, liveCycles) {
48
+ const keep = Math.max(1, Math.floor(liveCycles));
49
+ const dividers = [];
50
+ for (let i = 0; i < messages.length; i++) {
51
+ if (isCycleDivider(messages[i]))
52
+ dividers.push(i);
53
+ }
54
+ const cycles = dividers.length + 1;
55
+ if (cycles <= keep)
56
+ return 0;
57
+ // The last divider that closes a condensed cycle; the live region opens just
58
+ // after it (the divider itself stays in the condensed region as the seam).
59
+ return dividers[cycles - keep - 1] + 1;
60
+ }
@@ -1,18 +1,13 @@
1
- // render/context-message.ts — the viewer's foldable renderer for hidden-context custom
2
- // messages: the `<crtr-context>` bearings block (customType `crtr-context`) and
3
- // the ambient `<situational-context>` update (customType
4
- // `crtr-situational-context`). Both are broker-injected (canvas-context-intro,
5
- // canvas-inbox-watcher), so the VIEWER has no message renderer for either and
6
- // would otherwise show the full block always. This reimplements the bearings
7
- // renderer natively (src/pi-extensions/canvas-context-intro.ts:
8
- // `renderContextMessage`) and reuses the same collapsed/expand shape for
9
- // situational context so both render consistently: collapsed → one dim line;
10
- // expanded (Ctrl+O) → label + full wrapped body. It implements `setExpanded`,
11
- // so the Ctrl+O tool-display cycle (ChatView.cycleToolDisplay →
12
- // isExpandable) folds it alongside tool output.
1
+ // render/context-message.ts — the terminal viewer's foldable renderer for
2
+ // crouter-generated context. Session bearings, situational updates, fresh-revive
3
+ // kickoffs, context-size nudges, and node inbox digests retain their full model-visible body and
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.
13
9
  import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
14
- /** Default label/hint for the `crtr-context` bearings block. Mirrors the
15
- * extension's customType stamp. */
10
+ /** Default label/hint for the `crtr-context` bearings block. */
16
11
  const LABEL = 'crtr context';
17
12
  const COLLAPSED_HINT = ' orienting bearings — ctrl+o to expand';
18
13
  /** Char-based wrap that never emits an over-width line (pi-tui hard-crashes on
@@ -1,9 +1,11 @@
1
- import { type BindingId, type BindingResolution } from '../../../core/keybindings/index.js';
2
- export type AttachBindingId = Extract<BindingId, `crtr.attach.${string}` | 'crtr.mode.cycle'>;
1
+ import { type AttachBindingId, type BindingId, type BindingResolution } from '../../../core/keybindings/index.js';
2
+ export type { AttachBindingId } from '../../../core/keybindings/index.js';
3
3
  export type AttachBindings = BindingResolution<BindingId>;
4
+ export type AttachActions = Record<AttachBindingId, () => void>;
4
5
  export { inboxOpenHint } from '../../../core/keybindings/index.js';
5
6
  export declare function matchesAttachBinding(bindings: AttachBindings, id: AttachBindingId, data: string): boolean;
6
- export declare function dispatchAttachBinding(bindings: AttachBindings, data: string, actions: Record<AttachBindingId, () => void>): boolean;
7
+ export declare function dispatchAttachAction(id: AttachBindingId, actions: AttachActions): void;
8
+ export declare function dispatchAttachBinding(bindings: AttachBindings, data: string, actions: AttachActions): boolean;
7
9
  export declare function detachHint(bindings: AttachBindings): string | undefined;
8
10
  /** Re-resolve the user's binding catalog and republish it to every consumer:
9
11
  * the graph overlay's own snapshot and (in tmux) the crouter-owned tmux
@@ -5,24 +5,18 @@
5
5
  // tmux installer binds from (`core/keybindings`), never from ad-hoc key
6
6
  // literals, so a user's rebind is honored by the viewer and tmux alike.
7
7
  import { matchesKey } from '@earendil-works/pi-tui';
8
- import { formatBinding, matchesPiTuiInput, } from '../../../core/keybindings/index.js';
8
+ import { ATTACH_CONTROL_BINDINGS, formatBinding, matchesPiTuiInput, } from '../../../core/keybindings/index.js';
9
9
  export { inboxOpenHint } from '../../../core/keybindings/index.js';
10
10
  export function matchesAttachBinding(bindings, id, data) {
11
11
  return matchesPiTuiInput(bindings, id, data, (input, gesture) => matchesKey(input, gesture));
12
12
  }
13
+ export function dispatchAttachAction(id, actions) {
14
+ actions[id]();
15
+ }
13
16
  export function dispatchAttachBinding(bindings, data, actions) {
14
- for (const id of [
15
- 'crtr.attach.detach',
16
- 'crtr.attach.clear-or-detach',
17
- 'crtr.mode.cycle',
18
- 'crtr.attach.graph.toggle',
19
- 'crtr.attach.model-ladder.next',
20
- 'crtr.attach.model-ladder.previous',
21
- 'crtr.attach.command.inspect',
22
- 'crtr.attach.file-review',
23
- ]) {
24
- if (matchesAttachBinding(bindings, id, data)) {
25
- actions[id]();
17
+ for (const { actionId } of ATTACH_CONTROL_BINDINGS) {
18
+ if (matchesAttachBinding(bindings, actionId, data)) {
19
+ dispatchAttachAction(actionId, actions);
26
20
  return true;
27
21
  }
28
22
  }
@@ -17,6 +17,8 @@ export interface KeyHooks {
17
17
  inspectLoadedCommand: () => void;
18
18
  /** The file-review chord — open the transcript-file picker + review surface. */
19
19
  openFileReview: () => void;
20
+ /** Search the current node's local profile project files. */
21
+ openProfileFiles: () => void;
20
22
  }
21
23
  /** Install the listener; returns the remover, which teardown MUST call. */
22
24
  export declare function installGlobalKeys(ctx: Pick<AttachSession, 'tui' | 'editor'>, hooks: KeyHooks): () => void;
@@ -3,10 +3,11 @@
3
3
  // A TUI input listener fires BEFORE the focused component and before any dialog
4
4
  // overlay, so the configured attach actions work regardless of what has focus.
5
5
  // That privilege is also the hazard: anything consumed here never reaches the
6
- // editor. Only three things may claim a chunk — terminal focus reports, the
7
- // roster while it holds the cursor, and a configured crtr binding — and each
8
- // returns to the editor untouched when it does not apply.
9
- import { dispatchAttachBinding } from './bindings.js';
6
+ // editor. Only terminal focus reports, a private crouter control input, the
7
+ // roster while it holds the cursor, and a configured crtr binding may claim a
8
+ // chunk; each returns ordinary editor input untouched when it does not apply.
9
+ import { extractAttachControlInput } from '../../../core/keybindings/index.js';
10
+ import { dispatchAttachAction, dispatchAttachBinding } from './bindings.js';
10
11
  import { FOCUS_IN, FOCUS_OUT } from './focus-events.js';
11
12
  /** Press the clear-or-detach binding twice inside this window to detach. */
12
13
  const CLEAR_OR_DETACH_MS = 1000;
@@ -14,47 +15,45 @@ const CLEAR_OR_DETACH_MS = 1000;
14
15
  export function installGlobalKeys(ctx, hooks) {
15
16
  const { tui, editor } = ctx;
16
17
  let lastClearOrDetachAt = 0;
18
+ const actions = {
19
+ 'crtr.attach.detach': hooks.detach,
20
+ 'crtr.attach.clear-or-detach': () => {
21
+ const now = Date.now();
22
+ if (now - lastClearOrDetachAt < CLEAR_OR_DETACH_MS) {
23
+ lastClearOrDetachAt = 0;
24
+ hooks.detach();
25
+ return;
26
+ }
27
+ lastClearOrDetachAt = now;
28
+ editor.setText('');
29
+ hooks.setNotice(`Editor cleared — press ${hooks.detachHint()} again to detach`);
30
+ tui.requestRender();
31
+ },
32
+ 'crtr.mode.cycle': hooks.cycleMode,
33
+ 'crtr.attach.graph.toggle': hooks.toggleGraph,
34
+ // A plain broker frame, so this works locally and remotely alike; the broker gates observers on controller role.
35
+ 'crtr.attach.model-ladder.next': () => hooks.cycleModelLadder('forward'),
36
+ 'crtr.attach.model-ladder.previous': () => hooks.cycleModelLadder('backward'),
37
+ 'crtr.attach.command.inspect': hooks.inspectLoadedCommand,
38
+ 'crtr.attach.file-review': hooks.openFileReview,
39
+ 'crtr.attach.profile-files': hooks.openProfileFiles,
40
+ };
17
41
  return tui.addInputListener((data) => {
18
- // Terminal focus reporting (DECSET 1004, enabled after tui.start; tmux
19
- // forwards it per-pane with `focus-events on`). Pane focus drives the
20
- // editor's fake cursor — solid block focused, hollow outline blurred — so a
21
- // wall of attach panes shows exactly which one keys go to. The events are
22
- // stripped rather than blanket-consumed in case the chunk coalesced them
23
- // with real keystrokes.
42
+ // Terminal focus reporting (DECSET 1004, enabled after tui.start; tmux forwards it per-pane with `focus-events on`). Pane focus drives the editor's fake cursor — solid block focused, hollow outline blurred — so a wall of attach panes shows exactly which one keys go to. The events are stripped rather than blanket-consumed in case the chunk coalesced them with real keystrokes.
24
43
  if (data.includes(FOCUS_IN) || data.includes(FOCUS_OUT)) {
25
44
  hooks.setPaneFocused(data.lastIndexOf(FOCUS_IN) > data.lastIndexOf(FOCUS_OUT));
26
45
  tui.requestRender();
27
46
  const rest = data.replaceAll(FOCUS_IN, '').replaceAll(FOCUS_OUT, '');
28
47
  return rest.length > 0 ? { data: rest } : { consume: true };
29
48
  }
30
- // Inline subagent scroll+swap: once the editor hands off at its ↓ boundary,
31
- // ↑/↓ move over the manager/report roster and Enter dives into a node. An
32
- // inactive roster never intercepts editor input.
49
+ const control = extractAttachControlInput(data);
50
+ if (control !== undefined) {
51
+ dispatchAttachAction(control.actionId, actions);
52
+ return control.remaining.length > 0 ? { data: control.remaining } : { consume: true };
53
+ }
54
+ // Inline subagent scroll+swap: once the editor hands off at its ↓ boundary, ↑/↓ move over the manager/report roster and Enter dives into a node. An inactive roster never intercepts editor input.
33
55
  if (hooks.rosterHandleKey(data))
34
56
  return { consume: true };
35
- const dispatched = dispatchAttachBinding(hooks.bindings(), data, {
36
- 'crtr.attach.detach': hooks.detach,
37
- 'crtr.attach.clear-or-detach': () => {
38
- const now = Date.now();
39
- if (now - lastClearOrDetachAt < CLEAR_OR_DETACH_MS) {
40
- lastClearOrDetachAt = 0;
41
- hooks.detach();
42
- return;
43
- }
44
- lastClearOrDetachAt = now;
45
- editor.setText('');
46
- hooks.setNotice(`Editor cleared — press ${hooks.detachHint()} again to detach`);
47
- tui.requestRender();
48
- },
49
- 'crtr.mode.cycle': hooks.cycleMode,
50
- 'crtr.attach.graph.toggle': hooks.toggleGraph,
51
- // A plain broker frame, so this works locally and remotely alike; the
52
- // broker gates observers on controller role.
53
- 'crtr.attach.model-ladder.next': () => hooks.cycleModelLadder('forward'),
54
- 'crtr.attach.model-ladder.previous': () => hooks.cycleModelLadder('backward'),
55
- 'crtr.attach.command.inspect': hooks.inspectLoadedCommand,
56
- 'crtr.attach.file-review': hooks.openFileReview,
57
- });
58
- return dispatched ? { consume: true } : undefined;
57
+ return dispatchAttachBinding(hooks.bindings(), data, actions) ? { consume: true } : undefined;
59
58
  });
60
59
  }
@@ -0,0 +1,8 @@
1
+ interface ProfileFileSearchOptions {
2
+ cwd: string;
3
+ profileId: string | null;
4
+ fdPath: string | null;
5
+ onNotice: (message: string) => void;
6
+ }
7
+ export declare function launchProfileFileSearch(opts: ProfileFileSearchOptions): void;
8
+ export {};
@@ -0,0 +1,157 @@
1
+ // session/profile-files.ts — fuzzy file search across a node's local profile.
2
+ //
3
+ // The popup searches the current workspace plus every project named by the
4
+ // node's profile manifest. Ancestor `.crouter/memory/` stores are added with
5
+ // ignore rules disabled, so project memory remains findable even when a parent
6
+ // ignore file hides `.crouter/`. fzf owns selection and nvim owns viewing; the
7
+ // attach viewer only resolves the relevant roots and wires the two popups.
8
+ import { constants, accessSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { delimiter, dirname, join, resolve, sep } from 'node:path';
11
+ import { execFile, spawnSync } from 'node:child_process';
12
+ import { loadProfileManifest } from '../../../core/profiles/manifest.js';
13
+ import { shellQuote } from '../../../core/runtime/placement-tmux.js';
14
+ import { surfaceTmuxStyleArgs } from '../../../core/runtime/surface-bg.js';
15
+ function isDirectory(path) {
16
+ try {
17
+ return statSync(path).isDirectory();
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ function executableCandidates(name, preferred) {
24
+ const candidates = [
25
+ ...(preferred === undefined || preferred === null ? [] : [preferred]),
26
+ ...(process.env['PATH'] ?? '').split(delimiter).filter(Boolean).map((dir) => join(dir, name)),
27
+ ];
28
+ return [...new Set(candidates)];
29
+ }
30
+ /** Return the first PATH candidate that can actually execute. Checking only
31
+ * X_OK is insufficient when a managed tool cache contains a binary for a
32
+ * different host platform. This runs only when the user opens the search. */
33
+ function workingExecutable(name, preferred) {
34
+ for (const candidate of executableCandidates(name, preferred)) {
35
+ try {
36
+ accessSync(candidate, constants.X_OK);
37
+ }
38
+ catch {
39
+ continue;
40
+ }
41
+ const probe = spawnSync(candidate, ['--version'], { stdio: 'ignore' });
42
+ if (probe.status === 0)
43
+ return candidate;
44
+ }
45
+ return null;
46
+ }
47
+ function profileProjectDirs(profileId) {
48
+ if (profileId === null || profileId === '')
49
+ return [];
50
+ try {
51
+ return loadProfileManifest(profileId).manifest.projects;
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ }
57
+ function dedupeExistingDirs(paths) {
58
+ const seen = new Set();
59
+ const out = [];
60
+ for (const path of paths) {
61
+ const absolute = resolve(path);
62
+ if (seen.has(absolute) || !isDirectory(absolute))
63
+ continue;
64
+ seen.add(absolute);
65
+ out.push(absolute);
66
+ }
67
+ return out;
68
+ }
69
+ /** Keep only the outermost search roots. An outer profile project already
70
+ * covers a cwd or another project nested beneath it, and avoiding the second
71
+ * fd walk keeps every candidate unique without a separate dedupe process. */
72
+ function outermostDirs(paths) {
73
+ return paths.filter((path) => !paths.some((other) => other !== path && path.startsWith(other + sep)));
74
+ }
75
+ /** Every existing `<ancestor>/.crouter/memory/` from each pointer through the
76
+ * filesystem root. A dedicated no-ignore fd pass guarantees these files are
77
+ * present even when the wider project scan respects an ignore rule. */
78
+ function ancestorMemoryDirs(pointers) {
79
+ const found = [];
80
+ for (const pointer of pointers) {
81
+ let cursor = resolve(pointer);
82
+ for (;;) {
83
+ const memory = join(cursor, '.crouter', 'memory');
84
+ if (isDirectory(memory))
85
+ found.push(memory);
86
+ const parent = dirname(cursor);
87
+ if (parent === cursor)
88
+ break;
89
+ cursor = parent;
90
+ }
91
+ }
92
+ return dedupeExistingDirs(found);
93
+ }
94
+ function selectionPath(path) {
95
+ try {
96
+ const bytes = readFileSync(path);
97
+ if (bytes.length === 0)
98
+ return null;
99
+ const end = bytes[bytes.length - 1] === 0 ? bytes.length - 1 : bytes.length;
100
+ return bytes.subarray(0, end).toString('utf8') || null;
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ export function launchProfileFileSearch(opts) {
107
+ const fd = workingExecutable('fd', opts.fdPath);
108
+ if (fd === null) {
109
+ opts.onNotice('fd is required for profile file search');
110
+ return;
111
+ }
112
+ const fzf = workingExecutable('fzf');
113
+ if (fzf === null) {
114
+ opts.onNotice('fzf is required for profile file search');
115
+ return;
116
+ }
117
+ const nvim = workingExecutable('nvim');
118
+ if (nvim === null) {
119
+ opts.onNotice('Neovim is required for profile file search');
120
+ return;
121
+ }
122
+ const pointers = dedupeExistingDirs([opts.cwd, ...profileProjectDirs(opts.profileId)]);
123
+ if (pointers.length === 0) {
124
+ opts.onNotice('No profile file-search directories exist');
125
+ return;
126
+ }
127
+ const roots = outermostDirs(pointers);
128
+ const memoryDirs = ancestorMemoryDirs(pointers);
129
+ const tempDir = mkdtempSync(join(tmpdir(), 'crtr-profile-files-'));
130
+ const selected = join(tempDir, 'selection');
131
+ const scans = [
132
+ ...roots.map((root) => `${shellQuote(fd)} --type f --hidden --exclude .git --exclude ${shellQuote('**/.crouter/memory/**')} --print0 . ${shellQuote(root)}`),
133
+ ...memoryDirs.map((memory) => `${shellQuote(fd)} --type f --hidden --exclude .git --no-ignore --print0 . ${shellQuote(memory)}`),
134
+ ];
135
+ const command = `{ ${scans.map((scan) => `${scan} &`).join(' ')} wait; } | ${shellQuote(fzf)} --read0 --print0 --preview ${shellQuote('head -100 -- {}')} > ${shellQuote(selected)}`;
136
+ try {
137
+ execFile('tmux', ['display-popup', '-E', '-T', ' profile files ', '-w', '80%', '-h', '60%', ...surfaceTmuxStyleArgs(), command], () => {
138
+ const file = selectionPath(selected);
139
+ if (file === null || !existsSync(file)) {
140
+ rmSync(tempDir, { recursive: true, force: true });
141
+ return;
142
+ }
143
+ const open = `exec ${shellQuote(nvim)} -- ${shellQuote(file)}`;
144
+ try {
145
+ execFile('tmux', ['display-popup', '-E', '-T', ' profile file ', '-w', '90%', '-h', '90%', ...surfaceTmuxStyleArgs(), open], () => rmSync(tempDir, { recursive: true, force: true }));
146
+ }
147
+ catch (err) {
148
+ rmSync(tempDir, { recursive: true, force: true });
149
+ opts.onNotice(`Could not open profile file: ${err instanceof Error ? err.message : String(err)}`);
150
+ }
151
+ });
152
+ }
153
+ catch (err) {
154
+ rmSync(tempDir, { recursive: true, force: true });
155
+ opts.onNotice(`Could not open profile file search: ${err instanceof Error ? err.message : String(err)}`);
156
+ }
157
+ }