@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.
- package/dist/build-root.d.ts +6 -6
- package/dist/build-root.js +12 -13
- package/dist/builtin-memory/00-runtime-base.md +3 -4
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
- package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
- package/dist/builtin-memory/advisor/council.md +40 -0
- package/dist/builtin-memory/design.md +8 -11
- package/dist/builtin-memory/development.md +6 -9
- package/dist/builtin-memory/internal/INDEX.md +4 -5
- package/dist/builtin-memory/internal/agent-shaping.md +5 -7
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
- package/dist/builtin-memory/internal/marketplaces.md +13 -14
- package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
- package/dist/builtin-memory/internal/plugins.md +61 -45
- package/dist/builtin-memory/planning.md +4 -7
- package/dist/builtin-memory/spec.md +9 -12
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
- package/dist/cli.js +1 -1
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
- package/dist/clients/attach/__tests__/context-message.test.js +31 -20
- package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
- package/dist/clients/attach/render/chat-view.d.ts +20 -20
- package/dist/clients/attach/render/chat-view.js +42 -54
- package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
- package/dist/clients/attach/render/condensed-history.js +60 -0
- package/dist/clients/attach/render/context-message.js +9 -14
- package/dist/clients/attach/session/bindings.d.ts +5 -3
- package/dist/clients/attach/session/bindings.js +7 -13
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +36 -37
- package/dist/clients/attach/session/profile-files.d.ts +8 -0
- package/dist/clients/attach/session/profile-files.js +157 -0
- package/dist/clients/attach/viewer.js +525 -525
- package/dist/commands/node.js +2 -2
- package/dist/commands/pkg/plugin-inspect.js +6 -7
- package/dist/commands/pkg/plugin-manage.d.ts +1 -1
- package/dist/commands/pkg/plugin-manage.js +131 -19
- package/dist/commands/pkg/plugin.js +2 -2
- package/dist/commands/pkg.js +6 -11
- package/dist/commands/profile/env.js +3 -3
- package/dist/commands/sys/config.js +17 -76
- package/dist/commands/sys/doctor.js +5 -91
- package/dist/commands/sys/setup-wizard.d.ts +9 -11
- package/dist/commands/sys/setup-wizard.js +47 -81
- package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
- package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
- package/dist/core/__tests__/command-plugins.test.js +36 -16
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -1
- package/dist/core/command-manifests/manifest.d.ts +24 -0
- package/dist/core/{configured-clis → command-manifests}/manifest.js +21 -25
- package/dist/core/command-manifests/registry.d.ts +1 -2
- package/dist/core/command-manifests/registry.js +2 -2
- package/dist/core/command-manifests/schema.d.ts +11 -13
- package/dist/core/command-manifests/schema.js +53 -192
- package/dist/core/command-plugins/compose.d.ts +0 -6
- package/dist/core/command-plugins/compose.js +28 -75
- package/dist/core/command-plugins/discovery.d.ts +25 -58
- package/dist/core/command-plugins/discovery.js +152 -259
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +48 -0
- package/dist/core/command-plugins/store.d.ts +16 -0
- package/dist/core/command-plugins/store.js +64 -0
- package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
- package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
- package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
- package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
- package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
- package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
- package/dist/core/command.d.ts +8 -9
- package/dist/core/command.js +6 -9
- package/dist/core/config.js +6 -10
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/io.d.ts +1 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
- package/dist/core/keybindings/attach-control.d.ts +37 -0
- package/dist/core/keybindings/attach-control.js +38 -0
- package/dist/core/keybindings/catalog.d.ts +5 -4
- package/dist/core/keybindings/catalog.js +16 -8
- package/dist/core/keybindings/index.d.ts +1 -0
- package/dist/core/keybindings/index.js +1 -0
- package/dist/core/keybindings/types.d.ts +1 -1
- package/dist/core/preview-registry.js +41 -74
- package/dist/core/runtime/bearings.d.ts +2 -5
- package/dist/core/runtime/bearings.js +2 -5
- package/dist/core/runtime/front-door.d.ts +1 -1
- package/dist/core/runtime/front-door.js +2 -2
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +4 -3
- package/dist/core/runtime/situational-context.d.ts +1 -1
- package/dist/core/runtime/situational-context.js +1 -1
- package/dist/core/runtime/spawn.js +9 -9
- package/dist/core/runtime/tmux.js +29 -2
- package/dist/core/scope.d.ts +0 -5
- package/dist/core/scope.js +0 -10
- package/dist/core/user-settings.d.ts +193 -0
- package/dist/core/user-settings.js +252 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -2
- package/dist/shared/generated-context.d.ts +34 -0
- package/dist/shared/generated-context.js +98 -0
- package/dist/types.d.ts +23 -16
- package/dist/types.js +3 -14
- package/dist/web-client/assets/{index-NIuSCOHM.js → index-BMTOGuOZ.js} +19 -19
- package/dist/web-client/assets/index-DvA6Zw-R.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/public-api.md +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
- package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
- package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
- package/dist/builtin-memory/internal/workflow-codification.md +0 -82
- package/dist/builtin-memory/product.md +0 -80
- package/dist/clients/attach/render/frozen-history.js +0 -100
- package/dist/commands/pkg/cli-inspect.d.ts +0 -17
- package/dist/commands/pkg/cli-inspect.js +0 -190
- package/dist/commands/pkg/cli-manage.d.ts +0 -3
- package/dist/commands/pkg/cli-manage.js +0 -206
- package/dist/commands/pkg/cli.d.ts +0 -1
- package/dist/commands/pkg/cli.js +0 -14
- package/dist/core/configured-clis/cache.d.ts +0 -16
- package/dist/core/configured-clis/cache.js +0 -57
- package/dist/core/configured-clis/compose.d.ts +0 -14
- package/dist/core/configured-clis/compose.js +0 -60
- package/dist/core/configured-clis/discovery.d.ts +0 -47
- package/dist/core/configured-clis/discovery.js +0 -173
- package/dist/core/configured-clis/manifest.d.ts +0 -24
- package/dist/core/configured-clis/registration.d.ts +0 -40
- package/dist/core/configured-clis/registration.js +0 -201
- 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 {
|
|
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 {
|
|
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
|
|
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
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
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
|
|
378
|
+
const condensed = await this.buildCondensedBlock(messages.slice(0, liveStart), generation);
|
|
383
379
|
if (generation !== this.snapshotGeneration)
|
|
384
380
|
return;
|
|
385
|
-
if (
|
|
386
|
-
this.append(
|
|
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
|
-
*
|
|
487
|
-
*
|
|
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`
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
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
|
|
497
|
-
const
|
|
492
|
+
async buildCondensedBlock(messages, generation) {
|
|
493
|
+
const condensed = new Container();
|
|
498
494
|
const previousTarget = this.appendTarget;
|
|
499
|
-
this.appendTarget =
|
|
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
|
-
|
|
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
|
|
568
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
7
|
-
// roster while it holds the cursor, and a configured crtr binding
|
|
8
|
-
// returns
|
|
9
|
-
import {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|