gentle-pi 2.7.0 → 3.0.0
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 +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -20,7 +20,8 @@ export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.Proces
|
|
|
20
20
|
const publish = (evidence: SessionChangeEvidence) => {
|
|
21
21
|
if (!current || !store || current.sessionManager.getSessionId() !== store.sessionId) return;
|
|
22
22
|
if (store.record(evidence)) pi.appendEntry(SESSION_CHANGE_ENTRY, { sessionId: store.sessionId, evidence });
|
|
23
|
-
|
|
23
|
+
const notice = store.takeNotice();
|
|
24
|
+
pi.events.emit(SESSION_CHANGE_EVENT, { sessionId: store.sessionId, ...(notice ? { notice } : {}) });
|
|
24
25
|
};
|
|
25
26
|
pi.on("session_start", (_event, ctx) => {
|
|
26
27
|
pending.clear(); current = ctx;
|
package/lib/session-changes.ts
CHANGED
|
@@ -70,6 +70,7 @@ export class SessionChanges {
|
|
|
70
70
|
private readonly seen = new Set<string>();
|
|
71
71
|
private readonly files = new Map<string, Map<string, FileState>>();
|
|
72
72
|
private bytes = 0;
|
|
73
|
+
private announced = false;
|
|
73
74
|
notice: string | undefined;
|
|
74
75
|
readonly sessionId: string;
|
|
75
76
|
private cached: WorktreeChanges[] | undefined;
|
|
@@ -130,7 +131,13 @@ export class SessionChanges {
|
|
|
130
131
|
}
|
|
131
132
|
get model() {
|
|
132
133
|
const trees = this.worktrees;
|
|
133
|
-
return changesModel(trees.flatMap(tree => tree.model.files.map(file => ({ ...file, path: trees.length === 1 ? file.path : tree.root + "/" + file.path }))));
|
|
134
|
+
return { ...changesModel(trees.flatMap(tree => tree.model.files.map(file => ({ ...file, path: trees.length === 1 ? file.path : tree.root + "/" + file.path })))), ...(this.notice ? { notice: this.notice } : {}) };
|
|
135
|
+
}
|
|
136
|
+
/** Return the capture-limit notice once, for a single user notification. The `notice` field stays set. */
|
|
137
|
+
takeNotice(): string | undefined {
|
|
138
|
+
if (!this.notice || this.announced) return undefined;
|
|
139
|
+
this.announced = true;
|
|
140
|
+
return this.notice;
|
|
134
141
|
}
|
|
135
142
|
async refresh() { return this.model; }
|
|
136
143
|
loadDiff(root: string, file: ChangedFile): string {
|
package/lib/shell-bar.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { gaugeTone, renderGauge, type GaugeTone };
|
|
|
12
12
|
|
|
13
13
|
export interface ShellBarModel {
|
|
14
14
|
profile?: string;
|
|
15
|
+
changes?: { files: number; added: number; deleted: number; notice?: string };
|
|
15
16
|
cwd: string;
|
|
16
17
|
branch: string | null;
|
|
17
18
|
dirty: number | undefined;
|
|
@@ -123,7 +124,7 @@ function joinSegments(segments: string[], theme: ShellBarTheme): string {
|
|
|
123
124
|
export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme, width: number): string[] {
|
|
124
125
|
const value = (text: string) => theme.fg(ROLE.VALUE, theme.bold(text));
|
|
125
126
|
const label = (text: string) => theme.fg(ROLE.LABEL, text);
|
|
126
|
-
const
|
|
127
|
+
const changes = model.changes;
|
|
127
128
|
const branch = model.branch ? `${label("Branch")} ${value(model.branch)}` : "";
|
|
128
129
|
const percent = model.contextPercent === null ? "?%" : `${Math.round(model.contextPercent)}%`;
|
|
129
130
|
const capacity = label(`${formatTokens(model.contextWindow)} tokens`);
|
|
@@ -133,27 +134,35 @@ export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme
|
|
|
133
134
|
title: "Project",
|
|
134
135
|
lines: [
|
|
135
136
|
value(model.cwd),
|
|
136
|
-
...(
|
|
137
|
+
...(branch ? [branch] : []),
|
|
137
138
|
...(model.sessionName ? [`${label("Session")} ${value(model.sessionName)}`] : []),
|
|
138
|
-
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
title: "Model",
|
|
142
|
-
lines: [
|
|
143
|
-
value(model.modelId),
|
|
139
|
+
`${label("Model")} ${value(model.modelId)}`,
|
|
144
140
|
...(model.effort ? [`${label("Effort")} ${theme.fg(ROLE.EFFORT, model.effort)}`] : []),
|
|
145
141
|
...(model.profile ? [`${label("Profile")} ${value(sanitizeStatus(model.profile))}`] : []),
|
|
146
142
|
],
|
|
147
143
|
},
|
|
148
144
|
{
|
|
149
|
-
title: "
|
|
150
|
-
lines: [
|
|
145
|
+
title: "Changes",
|
|
146
|
+
lines: [
|
|
147
|
+
changes?.files
|
|
148
|
+
? `${changes.files} ${changes.files === 1 ? "file" : "files"} · ${theme.fg("success", `+${changes.added}`)} ${theme.fg("error", `−${changes.deleted}`)}`
|
|
149
|
+
: label("No captured changes"),
|
|
150
|
+
...(changes?.notice ? [theme.fg("warning", sanitizeStatus(changes.notice))] : []),
|
|
151
|
+
label("/gentle:changes"),
|
|
152
|
+
],
|
|
151
153
|
},
|
|
152
154
|
{
|
|
153
155
|
title: "Usage",
|
|
154
|
-
lines: [
|
|
156
|
+
lines: [
|
|
157
|
+
`${label("Context")} ${paintGauge(model.contextPercent, theme)} ${value(percent)}`,
|
|
158
|
+
capacity,
|
|
159
|
+
`${label("Cost")} ${value(formatCost(model.costTotal, model.subscription))}`,
|
|
160
|
+
...(usage ? [usage] : []),
|
|
161
|
+
],
|
|
155
162
|
},
|
|
156
|
-
|
|
163
|
+
{ title: "Integrations", lines: model.statuses.length
|
|
164
|
+
? model.statuses.map((status) => theme.fg(ROLE.STATUS, sanitizeStatus(status)))
|
|
165
|
+
: [label("No status reported")] },
|
|
157
166
|
];
|
|
158
167
|
// Pre-wrap values before indenting so Unicode/ANSI continuation lines keep
|
|
159
168
|
// the same inset without consuming the card's right border.
|
package/lib/shell-card.ts
CHANGED
|
@@ -60,12 +60,8 @@ function bodyLines(card: Card, innerWidth: number): string[] {
|
|
|
60
60
|
return card.body.flatMap((paragraph) => (paragraph === "" ? [""] : wrapTextWithAnsi(paragraph, innerWidth)));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const FRAME_ROLE = "border";
|
|
66
|
-
|
|
67
|
-
function soft(theme: CardTheme, _tone: CardTone, text: string): string {
|
|
68
|
-
return theme.fg(FRAME_ROLE, text);
|
|
63
|
+
function frame(theme: CardTheme, tone: CardTone, text: string): string {
|
|
64
|
+
return theme.fg(TONE_ROLE[tone], text);
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
export function cardTop(card: Card, theme: CardTheme, width: number, hint?: string): string {
|
|
@@ -74,7 +70,7 @@ export function cardTop(card: Card, theme: CardTheme, width: number, hint?: stri
|
|
|
74
70
|
if (targetWidth < 5) {
|
|
75
71
|
const left = theme.fg(TONE_ROLE[card.tone], "╭");
|
|
76
72
|
if (targetWidth === 1) return left;
|
|
77
|
-
return left +
|
|
73
|
+
return left + frame(theme, card.tone, `${rule(targetWidth - 2)}╮`);
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
const title = titleText(card, theme);
|
|
@@ -86,7 +82,7 @@ export function cardTop(card: Card, theme: CardTheme, width: number, hint?: stri
|
|
|
86
82
|
const styledTitleWidth = title.width <= titleWidth ? title.width : visibleWidth(styledTitle);
|
|
87
83
|
const fill = rule(targetWidth - styledTitleWidth - 5 - hintWidth);
|
|
88
84
|
const tail = shownHint ? ` ${theme.fg(HINT_ROLE, shownHint)} ` : "";
|
|
89
|
-
return theme.fg(TONE_ROLE[card.tone], "╭") +
|
|
85
|
+
return theme.fg(TONE_ROLE[card.tone], "╭") + frame(theme, card.tone, "─ ") + styledTitle + frame(theme, card.tone, ` ${fill}`) + tail + frame(theme, card.tone, "╮");
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
export function cardLine(text: string, tone: CardTone, theme: CardTheme, width: number): string {
|
|
@@ -94,13 +90,13 @@ export function cardLine(text: string, tone: CardTone, theme: CardTheme, width:
|
|
|
94
90
|
if (targetWidth === 0) return "";
|
|
95
91
|
const left = theme.fg(TONE_ROLE[tone], "│");
|
|
96
92
|
if (targetWidth === 1) return left;
|
|
97
|
-
if (targetWidth === 2) return left +
|
|
98
|
-
if (targetWidth === 3) return `${left} ${
|
|
93
|
+
if (targetWidth === 2) return left + frame(theme, tone, "│");
|
|
94
|
+
if (targetWidth === 3) return `${left} ${frame(theme, tone, "│")}`;
|
|
99
95
|
|
|
100
96
|
const innerWidth = targetWidth - FRAME_COLUMNS;
|
|
101
97
|
const clipped = innerWidth === 0 ? "" : truncateToWidth(text, innerWidth, "…");
|
|
102
98
|
const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)));
|
|
103
|
-
return `${left} ${clipped}${padding} ${
|
|
99
|
+
return `${left} ${clipped}${padding} ${frame(theme, tone, "│")}`;
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
export function cardBottom(tone: CardTone, theme: CardTheme, width: number): string {
|
|
@@ -108,7 +104,7 @@ export function cardBottom(tone: CardTone, theme: CardTheme, width: number): str
|
|
|
108
104
|
if (targetWidth === 0) return "";
|
|
109
105
|
const left = theme.fg(TONE_ROLE[tone], "╰");
|
|
110
106
|
if (targetWidth === 1) return left;
|
|
111
|
-
return left +
|
|
107
|
+
return left + frame(theme, tone, `${rule(targetWidth - 2)}╯`);
|
|
112
108
|
}
|
|
113
109
|
|
|
114
110
|
export function cardInnerWidth(width: number): number {
|
package/lib/shell-changes.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface ChangesModel {
|
|
|
27
27
|
files: ChangedFile[];
|
|
28
28
|
added: number;
|
|
29
29
|
deleted: number;
|
|
30
|
+
notice?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export interface ChangesSnapshot {
|
|
@@ -117,7 +118,7 @@ export function changesModel(changed: ChangedFile[]): ChangesModel {
|
|
|
117
118
|
|
|
118
119
|
export function changesSummary(model: ChangesModel): string {
|
|
119
120
|
const noun = model.files.length === 1 ? "file" : "files";
|
|
120
|
-
return `${model.files.length} ${noun} · +${model.added} −${model.deleted}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}`;
|
|
121
|
+
return `${model.files.length} ${noun} · +${model.added} −${model.deleted}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}${model.notice ? " · capture limit reached" : ""}`;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
// One line: summary, the files joined by dots, and the command pushed to
|
|
@@ -126,7 +127,7 @@ export function renderChangesWidget(model: ChangesModel, theme: ChangesTheme, wi
|
|
|
126
127
|
if (model.files.length === 0) return [];
|
|
127
128
|
const noun = model.files.length === 1 ? "file" : "files";
|
|
128
129
|
const dot = theme.fg("muted", "·");
|
|
129
|
-
const head = `${theme.fg("accent", WIDGET_GLYPH)} ${theme.fg("text", `${model.files.length} ${noun}`)} ${dot} ${theme.fg("success", `+${model.added}`)} ${theme.fg("error", `−${model.deleted}`)}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}`;
|
|
130
|
+
const head = `${theme.fg("accent", WIDGET_GLYPH)} ${theme.fg("text", `${model.files.length} ${noun}`)} ${dot} ${theme.fg("success", `+${model.added}`)} ${theme.fg("error", `−${model.deleted}`)}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}${model.notice ? ` ${dot} ${theme.fg("warning", "capture limit reached")}` : ""}`;
|
|
130
131
|
const hint = theme.fg("dim", CHANGES_COMMAND);
|
|
131
132
|
const list = model.files.map((file) => theme.fg("muted", file.path)).join(` ${dot} `);
|
|
132
133
|
const left = `${head} ${dot} ${list}`;
|
package/lib/shell-prompt.ts
CHANGED
|
@@ -23,9 +23,24 @@ const PETAL_TONE = {
|
|
|
23
23
|
|
|
24
24
|
export type PetalTone = (typeof PETAL_TONE)[keyof typeof PETAL_TONE];
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
26
|
+
// Keep Pi's original loader cadence while carrying the banner's reflected-light
|
|
27
|
+
// language into the compact working labels. The highlight enters before the
|
|
28
|
+
// word, crosses it as a soft symmetric wave, then exits before wrapping.
|
|
29
|
+
export const SHELL_PULSE_MS = 80;
|
|
30
|
+
export const SHELL_SCANNER_STEPS = 15;
|
|
31
|
+
const SCANNER_ENTRY_OFFSET = 3;
|
|
32
|
+
|
|
33
|
+
/** Banner-style reflected-light wave across text; foreground only. */
|
|
34
|
+
export function scanWorkingText(text: string, tick: number, fg: (role: string, text: string) => string): string {
|
|
35
|
+
const phase = ((Math.floor(tick) % SHELL_SCANNER_STEPS) + SHELL_SCANNER_STEPS) % SHELL_SCANNER_STEPS;
|
|
36
|
+
const head = phase - SCANNER_ENTRY_OFFSET;
|
|
37
|
+
return Array.from(text, (char, index) => {
|
|
38
|
+
const distance = Math.abs(head - index);
|
|
39
|
+
const role = distance === 0 ? "borderAccent" : distance === 1 ? "accent" : distance === 2 ? "thinkingHigh" : "muted";
|
|
40
|
+
return fg(role, char);
|
|
41
|
+
}).join("");
|
|
42
|
+
}
|
|
43
|
+
const PETAL_TONE_FRAMES = [PETAL_TONE.DEEP, PETAL_TONE.SOFT, PETAL_TONE.ROSE, PETAL_TONE.BRIGHT, PETAL_TONE.ROSE, PETAL_TONE.SOFT, PETAL_TONE.DEEP, PETAL_TONE.DEEP] as const;
|
|
29
44
|
|
|
30
45
|
export interface PromptFrameOptions {
|
|
31
46
|
state: PromptState;
|
|
@@ -46,7 +61,7 @@ const FAKE_CURSOR = "\x1b[7m \x1b[0m";
|
|
|
46
61
|
const SCROLL_INDICATOR = /[↑↓] \d+ more/;
|
|
47
62
|
const STATE_LABEL: Record<PromptState, string | undefined> = {
|
|
48
63
|
[PROMPT_STATE.IDLE]: undefined,
|
|
49
|
-
[PROMPT_STATE.WORKING]: "working",
|
|
64
|
+
[PROMPT_STATE.WORKING]: "working…",
|
|
50
65
|
[PROMPT_STATE.QUEUED]: "queued",
|
|
51
66
|
};
|
|
52
67
|
|
|
@@ -71,13 +86,15 @@ function rule(length: number): string {
|
|
|
71
86
|
|
|
72
87
|
function topRule(width: number, options: PromptFrameOptions, indicator: string | undefined): string {
|
|
73
88
|
const label = indicator ?? STATE_LABEL[options.state];
|
|
89
|
+
const scanning = options.state === PROMPT_STATE.WORKING;
|
|
74
90
|
const glyph = petalGlyph(options.state, options.tick);
|
|
75
91
|
const petal = options.fg(petalTone(options.state, options.tick), options.bold ? options.bold(glyph) : glyph);
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
92
|
+
const paintedLabel = label && scanning && !indicator ? scanWorkingText(label, options.tick, options.fg) : label ? options.fg(LABEL_ROLE, label) : "";
|
|
93
|
+
const title = [petal, paintedLabel].filter(Boolean).join(" ");
|
|
94
|
+
const titleWidth = visibleWidth(glyph) + (label ? visibleWidth(label) + (glyph ? 1 : 0) : 0);
|
|
95
|
+
const fill = width - titleWidth - 5;
|
|
79
96
|
if (fill < 0) return options.borderColor(`╭${rule(width - 2)}╮`);
|
|
80
|
-
return options.borderColor("╭─ ") +
|
|
97
|
+
return options.borderColor("╭─ ") + title + options.borderColor(` ${rule(fill)}╮`);
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
function bottomRule(width: number, options: PromptFrameOptions, indicator: string | undefined): string {
|
|
@@ -2,10 +2,10 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { ShellBarTheme } from "./shell-bar.ts";
|
|
3
3
|
|
|
4
4
|
export function renderSidebarBanner(theme: ShellBarTheme, width: number): string[] {
|
|
5
|
-
const label = "✿ Gentle
|
|
5
|
+
const label = "✿ Gentle Shell ✿";
|
|
6
6
|
const space = width - visibleWidth(label);
|
|
7
7
|
if (space < 0) return [];
|
|
8
8
|
// Gentleman's accent is pink; card titles use a separate champagne role.
|
|
9
|
-
const title = theme.fg("accent", "✿") + " " + theme.fg("text", "Gentle
|
|
9
|
+
const title = theme.fg("accent", "✿") + " " + theme.fg("text", "Gentle Shell") + " " + theme.fg("accent", "✿");
|
|
10
10
|
return [" ".repeat(Math.floor(space / 2)) + title + " ".repeat(Math.ceil(space / 2))];
|
|
11
11
|
}
|
|
@@ -134,7 +134,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
134
134
|
}
|
|
135
135
|
try {
|
|
136
136
|
const contentWidth = scroll.getContentWidth(RAIL_WIDTH);
|
|
137
|
-
const sections = ["footer", "
|
|
137
|
+
const sections = ["footer", "agents", "todo"].map((key) => {
|
|
138
138
|
const component = state.parts.get(key);
|
|
139
139
|
const lines = [...(component?.render(contentWidth - RAIL_PADDING * 2) ?? [])];
|
|
140
140
|
while (lines.length && lines[lines.length - 1]?.trim() === "") lines.pop();
|
|
@@ -171,7 +171,10 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
171
171
|
if (roots.has(root)) return;
|
|
172
172
|
const original = root[NODE]!;
|
|
173
173
|
const descriptor = Object.getOwnPropertyDescriptor(root, NODE);
|
|
174
|
-
|
|
174
|
+
// Fullscreen gives this stretched stack an explicit viewport height.
|
|
175
|
+
// Its intrinsic-height probe is unused; real painting traverses NODE.
|
|
176
|
+
// Delegating that probe to root.render would render the transcript twice.
|
|
177
|
+
const left = { render: () => [], invalidate() {}, [NODE]: () => original.call(root) };
|
|
175
178
|
const replacement = () => {
|
|
176
179
|
if (!prepare(tui.terminal.columns, root)) return original.call(root);
|
|
177
180
|
const current = prepared!;
|