@north-light/crouter 0.3.248 → 0.3.250
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/README.md +1 -1
- package/dist/api/__tests__/integration/client.test.js +28 -7
- package/dist/api/client.d.ts +1 -1
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +3 -0
- package/dist/api/dto/messages.d.ts +4 -0
- package/dist/api/dto/nodes.d.ts +4 -0
- package/dist/api/dto/reports.d.ts +7 -2
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +4 -3
- package/dist/clients/attach/session/chrome-refresh.d.ts +3 -0
- package/dist/clients/attach/session/chrome-refresh.js +1 -0
- package/dist/clients/attach/viewer.js +532 -532
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.d.ts +1 -0
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.js +53 -0
- package/dist/clients/inbox/controller.js +32 -5
- package/dist/clients/inbox/tui/ansi.d.ts +3 -1
- package/dist/clients/inbox/tui/ansi.js +14 -10
- package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -7
- package/dist/commands/api-client.d.ts +2 -2
- package/dist/commands/api-client.js +5 -5
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/node/create.js +3 -1
- package/dist/commands/node/inspect.js +1 -0
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +6 -4
- package/dist/commands/node/wait.js +1 -1
- package/dist/commands/push.js +36 -37
- package/dist/commands/sys/daemon.js +21 -8
- package/dist/commands/sys/setup-core.js +2 -2
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.d.ts +1 -0
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.js +84 -0
- package/dist/core/__tests__/broker-turn-admission.test.d.ts +1 -0
- package/dist/core/__tests__/broker-turn-admission.test.js +44 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +5 -1
- package/dist/core/__tests__/child-death-wake.test.js +11 -7
- package/dist/core/__tests__/collectible.test.d.ts +1 -0
- package/dist/core/__tests__/collectible.test.js +88 -0
- package/dist/core/__tests__/context-intro.test.js +4 -41
- package/dist/core/__tests__/daemon-boot.test.js +30 -14
- package/dist/core/__tests__/daemon-wedge.test.js +25 -19
- package/dist/core/__tests__/fixtures/fake-engine.js +11 -0
- package/dist/core/__tests__/human-deliver.test.js +5 -5
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +65 -4
- package/dist/core/__tests__/integration/human-deliver-e2e.test.js +3 -3
- package/dist/core/__tests__/integration/revive.test.js +48 -0
- package/dist/core/__tests__/integration/spawn-root.test.js +36 -1
- package/dist/core/__tests__/integration/worktree-land.test.js +82 -3
- package/dist/core/__tests__/integration/worktree-reap.test.js +44 -6
- package/dist/core/__tests__/migration.test.js +30 -0
- package/dist/core/__tests__/passive-subscription.test.js +39 -1
- package/dist/core/__tests__/prune.test.js +12 -3
- package/dist/core/__tests__/relaunch-root.test.js +1 -1
- package/dist/core/__tests__/respawn-throttle.test.js +16 -146
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +23 -11
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +81 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +1 -1
- package/dist/core/__tests__/seam/self-close-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/seam/self-close-teardown.test.js +49 -0
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +18 -10
- package/dist/core/canvas/canvas.d.ts +9 -15
- package/dist/core/canvas/canvas.js +41 -47
- package/dist/core/canvas/crons.d.ts +5 -8
- package/dist/core/canvas/crons.js +5 -8
- package/dist/core/canvas/history.d.ts +5 -0
- package/dist/core/canvas/history.js +12 -1
- package/dist/core/canvas/migrations.js +58 -1
- package/dist/core/canvas/pid.d.ts +1 -1
- package/dist/core/canvas/pid.js +5 -2
- package/dist/core/canvas/types.d.ts +4 -0
- package/dist/core/feed/feed.d.ts +4 -0
- package/dist/core/feed/feed.js +13 -8
- package/dist/core/feed/inbox.d.ts +2 -1
- package/dist/core/feed/inbox.js +22 -3
- package/dist/core/git.d.ts +3 -0
- package/dist/core/git.js +3 -1
- package/dist/core/human/feedback-companion.js +1 -0
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/companion.js +1 -0
- package/dist/core/review/realize.js +1 -0
- package/dist/core/runtime/broker/auth-reload.d.ts +2 -0
- package/dist/core/runtime/broker/auth-reload.js +9 -3
- package/dist/core/runtime/broker/engine-drive.d.ts +2 -0
- package/dist/core/runtime/broker/engine-drive.js +28 -23
- package/dist/core/runtime/broker/event-projection.d.ts +4 -0
- package/dist/core/runtime/broker/event-projection.js +43 -23
- package/dist/core/runtime/broker/fault-retry.d.ts +9 -0
- package/dist/core/runtime/broker/fault-retry.js +94 -8
- package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
- package/dist/core/runtime/broker/frame-dispatch.js +4 -1
- package/dist/core/runtime/broker/inbox.js +4 -0
- package/dist/core/runtime/broker/rebind.js +18 -0
- package/dist/core/runtime/broker/turn-admission.d.ts +11 -0
- package/dist/core/runtime/broker/turn-admission.js +31 -0
- package/dist/core/runtime/broker/turn-ignition.d.ts +2 -0
- package/dist/core/runtime/broker/turn-ignition.js +25 -15
- package/dist/core/runtime/broker.js +21 -2
- package/dist/core/runtime/close.d.ts +11 -9
- package/dist/core/runtime/close.js +23 -19
- package/dist/core/runtime/fault.d.ts +21 -0
- package/dist/core/runtime/fault.js +129 -19
- package/dist/core/runtime/fleet.d.ts +0 -6
- package/dist/core/runtime/host.d.ts +7 -1
- package/dist/core/runtime/host.js +20 -2
- package/dist/core/runtime/nodes.d.ts +5 -1
- package/dist/core/runtime/nodes.js +24 -1
- package/dist/core/runtime/placement.d.ts +6 -6
- package/dist/core/runtime/placement.js +15 -8
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/revive.js +40 -4
- package/dist/core/runtime/spawn.d.ts +3 -0
- package/dist/core/runtime/spawn.js +1 -0
- package/dist/core/runtime/warm-pool.d.ts +4 -0
- package/dist/core/runtime/warm-pool.js +2 -0
- package/dist/core/worktree.d.ts +4 -3
- package/dist/core/worktree.js +73 -27
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.d.ts +1 -0
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.js +81 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +38 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +1 -0
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +13 -2
- package/dist/daemon/api/handlers/bash-jobs.js +2 -0
- package/dist/daemon/api/handlers/broker-ops.js +23 -2
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/messages.js +11 -2
- package/dist/daemon/api/handlers/nodes.js +13 -1
- package/dist/daemon/api/handlers/reports.js +14 -6
- package/dist/daemon/api/map.js +1 -0
- package/dist/daemon/api/server.d.ts +9 -12
- package/dist/daemon/api/server.js +30 -19
- package/dist/daemon/companion-retire.js +3 -9
- package/dist/daemon/cron/sinks.js +7 -2
- package/dist/daemon/crtrd.js +4 -1
- package/dist/daemon/fleet.d.ts +2 -16
- package/dist/daemon/fleet.js +63 -150
- package/dist/daemon/human/finish.js +2 -0
- package/dist/daemon/manage.d.ts +11 -0
- package/dist/daemon/manage.js +32 -3
- package/dist/daemon/messaging/node-message.js +7 -1
- package/dist/daemon/reconcilers/bash-deadline.js +2 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +1 -0
- package/dist/daemon/reconcilers/broker-supervision.js +22 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +16 -6
- package/dist/daemon/reconcilers/live-obligation.js +18 -6
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +15 -4
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +34 -3
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -1
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +11 -2
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +14 -7
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -2
- package/dist/daemon/reconcilers/storage-maintenance.js +3 -3
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -34
- package/dist/pi-extensions/canvas-context-intro.js +4 -76
- package/dist/pi-extensions/canvas-review-boundary.d.ts +3 -24
- package/dist/pi-extensions/canvas-review-boundary.js +4 -29
- package/dist/pi-extensions/canvas-stophook.js +1 -6
- package/dist/shared/birth-announcement.d.ts +12 -0
- package/dist/shared/birth-announcement.js +25 -0
- package/dist/shared/generated-context.d.ts +8 -4
- package/dist/shared/generated-context.js +15 -9
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +0 -254
- package/dist/clients/inbox/tui/clipboard.d.ts +0 -2
- package/dist/clients/inbox/tui/clipboard.js +0 -26
- package/dist/pi-extensions/truncate.d.ts +0 -5
- package/dist/pi-extensions/truncate.js +0 -14
|
@@ -15,15 +15,21 @@ export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone a
|
|
|
15
15
|
"Pipe the result to `crtr push final` through a single-quoted heredoc if the work is done, or use `crtr human send` if you are blocked or need the user.";
|
|
16
16
|
/** The daemon's parking mandate: the last turn of a conversation the unattended
|
|
17
17
|
* clock is concluding. Delivered live, wrapped in a `park` runtime card. It
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
* governs both the node's durable inheritance and reader-facing output:
|
|
19
|
+
* one update for subscribers and history, then a goodbye that does not narrate
|
|
20
|
+
* the parking.
|
|
21
|
+
*
|
|
22
|
+
* The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
|
|
23
|
+
* guide (roadmap current, short and shrinking; context dir for in-progress
|
|
24
|
+
* material; memory only for gated permanent lessons) but not a string: yield is
|
|
25
|
+
* read by an agent choosing to refresh, this by an agent being told to conclude. */
|
|
26
|
+
export const PARK_SUMMARY_PROMPT = 'This conversation has been idle with nothing left to wake it, so it is being concluded. This is your last turn. Use it to leave a trustworthy inheritance, not to restart or broaden the work. Do these six things now, then stop.\n\n'
|
|
27
|
+
+ '1. Establish current truth. Check only state that may have changed outside the transcript and matters to resuming—such as the working tree, a remote run, or an external decision. Do not start new work; perform only a quick check needed to avoid recording an unverified claim. If no mandate or work ever began, say that plainly and keep every artifact minimal.\n\n'
|
|
28
|
+
+ '2. Rewrite `$CRTR_CONTEXT_DIR/roadmap.md` for a fresh context window. This is the only handoff document a later fresh cycle receives in full. Preserve the current goal and exit criteria when they still apply, then state the present outcome; what remains or is blocked; decisions or questions still open; exact recovery handles for in-flight state; and the first safe move on return. Keep strategy and present state, not a transcript recap. Delete stale and completed steps instead of marking them done; the roadmap should stay short and shrink. Write a minimal one now if none exists.\n\n'
|
|
29
|
+
+ '3. Put supporting material in your context directory only when the roadmap would become bulky without it. Rewrite existing living documents rather than leave superseded versions. Name every supporting file the next cycle must read from the roadmap and say what it is for—the revive shows filenames but does not inject their contents. Task state, identifiers, and recovery detail belong here, not in memory.\n\n'
|
|
30
|
+
+ '4. Use memory only for a non-obvious, reusable lesson that should survive this task and is not already recorded. Read `crtr memory write -h`, find before writing, and choose the narrowest scope that will reach the next agent who needs it. Do not put a conversation recap, task status, recovery handles, or facts already captured in code or docs into memory.\n\n'
|
|
31
|
+
+ '5. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write it for subscribers and history, not as a second roadmap. Its first line must stand alone as the current outcome, blocker, or decision that matters; then include only unfinished work, a needed decision, and concrete handles a subscriber may need. This concludes the conversation; it does not finish the mandate.\n\n'
|
|
32
|
+
+ '6. End with a short sign-off to the reader in second person: say you are putting your notes in order and pausing here until they come back, and mention anything genuinely worth their attention, such as unfinished work or an open question. Keep all visible text this turn—including the update\'s first line—about their work, not crouter\'s machinery: do not mention roadmap or context paths, filing a report, parking, idling, residency, or node ids. Then stop. A later message reopens you on a fresh context window grounded in your goal and roadmap, so the inheritance you leave now is what you get back.';
|
|
27
33
|
/** Static recovery prompts shared by the broker producer and display classifier. */
|
|
28
34
|
export const AUTH_FAULT_RECOVERY_BODY = 'Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.';
|
|
29
35
|
export const CONNECTION_FAULT_RECOVERY_BODY = 'The network connection is back online. Your previous turn stopped on a connection error (the network was down). Continue from where you left off and retry the work that failed.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.250",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
"dev": "tsx --conditions=crtr-src src/cli.ts",
|
|
84
84
|
"link": "npm link",
|
|
85
85
|
"prepublishOnly": "npm run build",
|
|
86
|
-
"test": "CRTR_NO_DAEMON_AUTOSTART=1 node --conditions=crtr-src --import tsx/esm --test --test-force-exit --test-concurrency=$(node -p 'Math.max(4, require(`node:os`).availableParallelism() - 1)') --test-timeout=10000 'src/**/__tests__/*.test.ts'",
|
|
87
|
-
"test:integration": "CRTR_NO_DAEMON_AUTOSTART=1 node --conditions=crtr-src --import tsx/esm --test --test-concurrency=$(node -p 'Math.min(8, Math.max(2, require(`node:os`).availableParallelism() - 1))') --test-timeout=60000 'src/**/__tests__/integration/*.test.ts'",
|
|
86
|
+
"test": "CRTR_NO_DAEMON_AUTOSTART=1 node --conditions=crtr-src --import tsx/esm --import ./scripts/test-env-scrub.mjs --test --test-force-exit --test-concurrency=$(node -p 'Math.max(4, require(`node:os`).availableParallelism() - 1)') --test-timeout=10000 'src/**/__tests__/*.test.ts'",
|
|
87
|
+
"test:integration": "CRTR_NO_DAEMON_AUTOSTART=1 node --conditions=crtr-src --import tsx/esm --import ./scripts/test-env-scrub.mjs --test --test-concurrency=$(node -p 'Math.min(8, Math.max(2, require(`node:os`).availableParallelism() - 1))') --test-timeout=60000 'src/**/__tests__/integration/*.test.ts'",
|
|
88
88
|
"test:seam": "npm run build && npm run test:seam:built",
|
|
89
|
-
"test:seam:built": "CRTR_NO_DAEMON_AUTOSTART=1 node --test --test-concurrency=4 --test-timeout=180000 'dist/**/__tests__/seam/*.test.js'",
|
|
89
|
+
"test:seam:built": "CRTR_NO_DAEMON_AUTOSTART=1 node --import ./scripts/test-env-scrub.mjs --test --test-concurrency=4 --test-timeout=180000 'dist/**/__tests__/seam/*.test.js'",
|
|
90
90
|
"test:all": "npm test && npm run test:integration && npm run test:seam"
|
|
91
91
|
},
|
|
92
92
|
"overrides": {
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.250",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.250",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Statusline + session-name badge.
|
|
3
|
-
*
|
|
4
|
-
* Footer (statusline): <model> [· subscription] · <tokens used> · <cycle>, with
|
|
5
|
-
* <cwd last dir> (<git>) right-aligned on the far right.
|
|
6
|
-
* Input bar border: session name rendered as a solid-background badge,
|
|
7
|
-
* shown only when a name is set (via /name or
|
|
8
|
-
* pi.setSessionName()).
|
|
9
|
-
*
|
|
10
|
-
* This extension owns only the statusline and session-name badge. Canvas
|
|
11
|
-
* navigation and interactive chrome live in crouter's attach viewer.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { execFile } from 'node:child_process';
|
|
15
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
16
|
-
import { CustomEditor, type ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
17
|
-
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
18
|
-
|
|
19
|
-
function lastDir(cwd: string): string {
|
|
20
|
-
const trimmed = cwd.replace(/\/+$/, "");
|
|
21
|
-
const base = trimmed.split("/").pop();
|
|
22
|
-
return base && base.length > 0 ? base : "/";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function fmtTokens(n: number): string {
|
|
26
|
-
return n < 1000 ? `${n}` : `${Math.round(n / 1000)}k`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface GitStatus {
|
|
30
|
-
staged: number;
|
|
31
|
-
modified: number;
|
|
32
|
-
untracked: number;
|
|
33
|
-
conflicts: number;
|
|
34
|
-
ahead: number;
|
|
35
|
-
behind: number;
|
|
36
|
-
stashed: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Run `git status --porcelain=v1 --branch` and parse a summary. */
|
|
40
|
-
function loadGitStatus(cwd: string): Promise<GitStatus | null> {
|
|
41
|
-
return new Promise((resolve) => {
|
|
42
|
-
execFile(
|
|
43
|
-
"git",
|
|
44
|
-
["status", "--porcelain=v1", "--branch", "--untracked-files=all"],
|
|
45
|
-
{ cwd, timeout: 2000, windowsHide: true },
|
|
46
|
-
(err, stdout) => {
|
|
47
|
-
if (err) return resolve(null);
|
|
48
|
-
const s: GitStatus = {
|
|
49
|
-
staged: 0,
|
|
50
|
-
modified: 0,
|
|
51
|
-
untracked: 0,
|
|
52
|
-
conflicts: 0,
|
|
53
|
-
ahead: 0,
|
|
54
|
-
behind: 0,
|
|
55
|
-
stashed: 0,
|
|
56
|
-
};
|
|
57
|
-
for (const line of stdout.split("\n")) {
|
|
58
|
-
if (line.startsWith("## ")) {
|
|
59
|
-
const ahead = line.match(/ahead (\d+)/);
|
|
60
|
-
const behind = line.match(/behind (\d+)/);
|
|
61
|
-
if (ahead) s.ahead = Number(ahead[1]);
|
|
62
|
-
if (behind) s.behind = Number(behind[1]);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (line.length < 2) continue;
|
|
66
|
-
const x = line[0];
|
|
67
|
-
const y = line[1];
|
|
68
|
-
if (x === "?" && y === "?") {
|
|
69
|
-
s.untracked++;
|
|
70
|
-
} else if (x === "U" || y === "U" || (x === "A" && y === "A") || (x === "D" && y === "D")) {
|
|
71
|
-
s.conflicts++;
|
|
72
|
-
} else {
|
|
73
|
-
if (x !== " " && x !== "?") s.staged++;
|
|
74
|
-
if (y !== " " && y !== "?") s.modified++;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
resolve(s);
|
|
78
|
-
},
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Render git status using standard prompt symbols. */
|
|
84
|
-
function fmtGitStatus(s: GitStatus | null): string {
|
|
85
|
-
if (!s) return "";
|
|
86
|
-
const parts: string[] = [];
|
|
87
|
-
if (s.ahead) parts.push(`⇡${s.ahead}`); // ⇡ ahead
|
|
88
|
-
if (s.behind) parts.push(`⇣${s.behind}`); // ⇣ behind
|
|
89
|
-
if (s.conflicts) parts.push(`×${s.conflicts}`); // × conflicts
|
|
90
|
-
if (s.staged) parts.push(`+${s.staged}`); // + staged
|
|
91
|
-
if (s.modified) parts.push(`!${s.modified}`); // ! modified
|
|
92
|
-
if (s.untracked) parts.push(`?${s.untracked}`); // ? untracked
|
|
93
|
-
if (s.stashed) parts.push(`$${s.stashed}`); // $ stashed
|
|
94
|
-
if (parts.length === 0) return "✓"; // ✓ clean
|
|
95
|
-
return parts.join(" ");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function formatTokens(ctx: Pick<ExtensionContext, "getContextUsage">): string {
|
|
99
|
-
const usage = ctx.getContextUsage();
|
|
100
|
-
if (!usage || usage.tokens == null) return "0 tokens";
|
|
101
|
-
return `${fmtTokens(usage.tokens)} tokens`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
interface FooterDataLike {
|
|
105
|
-
// pi's provider reports "no branch" as null; keep undefined too so tests can pass a
|
|
106
|
-
// bare stub.
|
|
107
|
-
getGitBranch(): string | null | undefined;
|
|
108
|
-
getExtensionStatuses(): ReadonlyMap<string, string>;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function buildStatuslineFooterLine(
|
|
112
|
-
width: number,
|
|
113
|
-
ctx: Pick<ExtensionContext, "cwd" | "model" | "getContextUsage">,
|
|
114
|
-
footerData: FooterDataLike,
|
|
115
|
-
gitStatus: GitStatus | null,
|
|
116
|
-
): string {
|
|
117
|
-
if (width <= 0) return "";
|
|
118
|
-
|
|
119
|
-
const cwd = lastDir(ctx.cwd);
|
|
120
|
-
const branch = footerData.getGitBranch();
|
|
121
|
-
const status = fmtGitStatus(gitStatus);
|
|
122
|
-
const gitStr = branch
|
|
123
|
-
? status
|
|
124
|
-
? `${branch} ${status}`
|
|
125
|
-
: branch
|
|
126
|
-
: "";
|
|
127
|
-
const cwdStr = gitStr ? `${cwd} (${gitStr})` : cwd;
|
|
128
|
-
|
|
129
|
-
const model = ctx.model?.id ?? "no model";
|
|
130
|
-
const subscription = footerData.getExtensionStatuses().get("provider-rotation")?.trim() ?? "";
|
|
131
|
-
const tokens = formatTokens(ctx);
|
|
132
|
-
const cyclesEnv = process.env["CRTR_CYCLES"]?.trim();
|
|
133
|
-
const cycleCount = cyclesEnv ? Number.parseInt(cyclesEnv, 10) : Number.NaN;
|
|
134
|
-
const cycle = Number.isFinite(cycleCount) ? `↻${cycleCount}` : "";
|
|
135
|
-
|
|
136
|
-
const leftParts = [model];
|
|
137
|
-
if (subscription !== "") leftParts.push(subscription);
|
|
138
|
-
leftParts.push(tokens);
|
|
139
|
-
if (cycle !== "") leftParts.push(cycle);
|
|
140
|
-
const leftRaw = leftParts.join(" · ");
|
|
141
|
-
let rightRaw = cwdStr;
|
|
142
|
-
|
|
143
|
-
let rightWidth = visibleWidth(rightRaw);
|
|
144
|
-
if (rightWidth > width) {
|
|
145
|
-
const cwdWidth = visibleWidth(cwd);
|
|
146
|
-
if (cwdWidth < width) {
|
|
147
|
-
rightRaw = cwd;
|
|
148
|
-
rightWidth = cwdWidth;
|
|
149
|
-
} else {
|
|
150
|
-
return truncateToWidth(cwd, width);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (rightWidth === width) return rightRaw;
|
|
155
|
-
|
|
156
|
-
const leftLimit = Math.max(0, width - rightWidth - 1);
|
|
157
|
-
const leftRawClamped = truncateToWidth(leftRaw, leftLimit);
|
|
158
|
-
const leftWidth = visibleWidth(leftRawClamped);
|
|
159
|
-
const padWidth = Math.max(1, width - leftWidth - rightWidth);
|
|
160
|
-
return `${leftRawClamped}${" ".repeat(padWidth)}${rightRaw}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Insert `badge` into a single border line at the left, after one corner cell. */
|
|
164
|
-
function borderWithBadge(
|
|
165
|
-
width: number,
|
|
166
|
-
badge: string,
|
|
167
|
-
border: (text: string) => string,
|
|
168
|
-
): string {
|
|
169
|
-
if (width <= 0) return "";
|
|
170
|
-
const badgeWidth = visibleWidth(badge);
|
|
171
|
-
// 1 leading border cell + badge + fill out to width.
|
|
172
|
-
if (badgeWidth + 1 >= width) {
|
|
173
|
-
return border("─") + truncateToWidth(badge, Math.max(0, width - 1), "");
|
|
174
|
-
}
|
|
175
|
-
const fill = border("─".repeat(width - 1 - badgeWidth));
|
|
176
|
-
return `${border("─")}${badge}${fill}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export default function (pi: ExtensionAPI) {
|
|
180
|
-
pi.on("session_start", (_event, ctx) => {
|
|
181
|
-
// Statusline (footer): model · [subscription] · tokens · cycle, with cwd/git right-aligned
|
|
182
|
-
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
183
|
-
let gitStatus: GitStatus | null = null;
|
|
184
|
-
|
|
185
|
-
const refreshStatus = () => {
|
|
186
|
-
if (!footerData.getGitBranch()) {
|
|
187
|
-
if (gitStatus) {
|
|
188
|
-
gitStatus = null;
|
|
189
|
-
tui.requestRender();
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
void loadGitStatus(ctx.cwd).then((next) => {
|
|
194
|
-
if (JSON.stringify(next) !== JSON.stringify(gitStatus)) {
|
|
195
|
-
gitStatus = next;
|
|
196
|
-
tui.requestRender();
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
refreshStatus();
|
|
202
|
-
const unsubBranch = footerData.onBranchChange(() => {
|
|
203
|
-
tui.requestRender();
|
|
204
|
-
refreshStatus();
|
|
205
|
-
});
|
|
206
|
-
const timer = setInterval(refreshStatus, 4000);
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
dispose() {
|
|
210
|
-
unsubBranch();
|
|
211
|
-
clearInterval(timer);
|
|
212
|
-
},
|
|
213
|
-
invalidate() {},
|
|
214
|
-
render(width: number): string[] {
|
|
215
|
-
return [theme.fg("dim", buildStatuslineFooterLine(width, ctx, footerData, gitStatus))];
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Input bar border: session-name badge with solid background
|
|
221
|
-
// Compose with whatever editor is already installed instead of replacing it:
|
|
222
|
-
// wrap the previous factory in a Proxy that overrides only render
|
|
223
|
-
// (session-name badge on the top border). Other editor decorations keep
|
|
224
|
-
// working regardless of load order.
|
|
225
|
-
const previousEditorFactory = ctx.ui.getEditorComponent();
|
|
226
|
-
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
227
|
-
const base = (
|
|
228
|
-
previousEditorFactory
|
|
229
|
-
? previousEditorFactory(tui, theme, keybindings)
|
|
230
|
-
: new CustomEditor(tui, theme, keybindings)
|
|
231
|
-
) as unknown as CustomEditor;
|
|
232
|
-
|
|
233
|
-
const render = (width: number): string[] => {
|
|
234
|
-
const lines = base.render(width);
|
|
235
|
-
const name = pi.getSessionName();
|
|
236
|
-
if (!name || lines.length === 0) return lines;
|
|
237
|
-
|
|
238
|
-
const thm = ctx.ui.theme;
|
|
239
|
-
const badge = thm.bg("selectedBg", thm.fg("text", ` ${name} `));
|
|
240
|
-
const borderColor = (text: string) => base.borderColor(text);
|
|
241
|
-
lines[0] = borderWithBadge(width, badge, borderColor);
|
|
242
|
-
return lines;
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
return new Proxy(base, {
|
|
246
|
-
get(target, prop) {
|
|
247
|
-
if (prop === "render") return render;
|
|
248
|
-
const value = Reflect.get(target, prop, target);
|
|
249
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
const SPAWN_TIMEOUT_MS = 3000;
|
|
3
|
-
function writers() {
|
|
4
|
-
if (process.platform === 'darwin')
|
|
5
|
-
return [['pbcopy', []]];
|
|
6
|
-
const wayland = Boolean(process.env.WAYLAND_DISPLAY)
|
|
7
|
-
|| process.env.XDG_SESSION_TYPE === 'wayland';
|
|
8
|
-
const xclip = ['xclip', ['-selection', 'clipboard']];
|
|
9
|
-
const wlcopy = ['wl-copy', []];
|
|
10
|
-
return wayland ? [wlcopy, xclip] : [xclip, wlcopy];
|
|
11
|
-
}
|
|
12
|
-
/** Write text to the system clipboard without throwing. */
|
|
13
|
-
export function writeClipboardText(text) {
|
|
14
|
-
for (const [command, args] of writers()) {
|
|
15
|
-
const result = spawnSync(command, args, {
|
|
16
|
-
input: text,
|
|
17
|
-
timeout: SPAWN_TIMEOUT_MS,
|
|
18
|
-
encoding: 'utf-8',
|
|
19
|
-
});
|
|
20
|
-
if (result.error)
|
|
21
|
-
continue;
|
|
22
|
-
if (result.status === 0)
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/** Hard-truncate to `width` columns with an ellipsis when the text would
|
|
2
|
-
* overflow. Extension-rendered content is ANSI-free plain text, so a
|
|
3
|
-
* code-point count stands in for visible width. Every renderer MUST keep its
|
|
4
|
-
* emitted lines within the terminal width or pi's TUI aborts the whole render. */
|
|
5
|
-
export declare function truncateToWidth(text: string, width: number): string;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/** Hard-truncate to `width` columns with an ellipsis when the text would
|
|
2
|
-
* overflow. Extension-rendered content is ANSI-free plain text, so a
|
|
3
|
-
* code-point count stands in for visible width. Every renderer MUST keep its
|
|
4
|
-
* emitted lines within the terminal width or pi's TUI aborts the whole render. */
|
|
5
|
-
export function truncateToWidth(text, width) {
|
|
6
|
-
if (width <= 0)
|
|
7
|
-
return '';
|
|
8
|
-
const chars = Array.from(text);
|
|
9
|
-
if (chars.length <= width)
|
|
10
|
-
return text;
|
|
11
|
-
if (width === 1)
|
|
12
|
-
return '…';
|
|
13
|
-
return chars.slice(0, width - 1).join('') + '…';
|
|
14
|
-
}
|