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.
Files changed (122) hide show
  1. package/README.md +24 -6
  2. package/assets/agents/gentle-ai-worker.md +5 -1
  3. package/assets/agents/sdd-apply.md +9 -7
  4. package/assets/agents/sdd-archive.md +42 -23
  5. package/assets/agents/sdd-proposal.md +2 -2
  6. package/assets/agents/sdd-remediate.md +4 -4
  7. package/assets/agents/sdd-research.md +20 -48
  8. package/assets/agents/sdd-tasks.md +5 -5
  9. package/assets/agents/sdd-verify.md +6 -28
  10. package/assets/chains/sdd-full.chain.md +4 -22
  11. package/assets/chains/sdd-verify.chain.md +3 -12
  12. package/assets/orchestrator-delegation.md +33 -3
  13. package/assets/orchestrator-memory.md +20 -7
  14. package/assets/orchestrator.md +5 -3
  15. package/assets/sdd-orchestrator-workflow.md +25 -58
  16. package/assets/support/sdd-status-contract.md +9 -12
  17. package/docs/gentle-shell.md +14 -4
  18. package/docs/readme-reference.md +171 -32
  19. package/extensions/codegraph-tools.ts +2 -0
  20. package/extensions/gentle-agents.ts +281 -361
  21. package/extensions/gentle-ai.ts +588 -117
  22. package/extensions/gentle-shell.ts +74 -30
  23. package/extensions/pi-pretty.ts +63 -14
  24. package/extensions/quiet-tools.ts +1 -2
  25. package/extensions/startup-banner.ts +10 -9
  26. package/lib/agent-home.ts +8 -0
  27. package/lib/agent-profile-pin.ts +336 -0
  28. package/lib/agent-profiles.ts +28 -8
  29. package/lib/agents-config.ts +24 -2
  30. package/lib/agents-history.ts +3 -97
  31. package/lib/agents-keys.ts +27 -0
  32. package/lib/agents-protocol.ts +2 -15
  33. package/lib/agents-runner.ts +67 -111
  34. package/lib/agents-session-transport.ts +691 -0
  35. package/lib/command-palette-catalog.ts +87 -0
  36. package/lib/command-palette.ts +346 -0
  37. package/lib/native-choice-list.ts +5 -0
  38. package/lib/native-review-cli.ts +10 -97
  39. package/lib/review-publication-gate.ts +11 -1
  40. package/lib/review-repository.ts +1 -1
  41. package/lib/review-snapshot.ts +1 -0
  42. package/lib/review-transaction.ts +4 -2
  43. package/lib/sdd-preflight.ts +2 -1
  44. package/lib/sdd-research-capabilities.ts +18 -152
  45. package/lib/sdd-status.ts +7 -779
  46. package/lib/session-change-capture.ts +2 -1
  47. package/lib/session-changes.ts +8 -1
  48. package/lib/shell-bar.ts +21 -12
  49. package/lib/shell-card.ts +8 -12
  50. package/lib/shell-changes.ts +3 -2
  51. package/lib/shell-prompt.ts +25 -8
  52. package/lib/shell-sidebar-banner.ts +2 -2
  53. package/lib/shell-sidebar-layout.ts +5 -2
  54. package/lib/windows-session-transport.ts +877 -0
  55. package/package.json +3 -3
  56. package/runtime/native-review-cli.mjs +9 -96
  57. package/runtime/windows-session-transport.ps1 +791 -0
  58. package/scripts/test-packed-runner.mjs +1668 -20
  59. package/scripts/verify-package-files.mjs +0 -1
  60. package/tests/agent-home.test.ts +52 -0
  61. package/tests/agent-profiles.test.ts +30 -1
  62. package/tests/agents-config.test.ts +44 -0
  63. package/tests/agents-history.test.ts +12 -24
  64. package/tests/agents-runner.test.ts +307 -58
  65. package/tests/agents-session-transport-process.test.ts +249 -0
  66. package/tests/agents-session-transport.test.ts +823 -0
  67. package/tests/artifact-language.test.ts +10 -7
  68. package/tests/command-palette.test.ts +378 -0
  69. package/tests/delegated-key-learnings-contract.test.ts +2 -2
  70. package/tests/fixtures/agents-session-transport-process.mjs +108 -0
  71. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
  72. package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
  73. package/tests/fixtures/windows-session-compile.ps1 +110 -0
  74. package/tests/gentle-agents.test.ts +849 -356
  75. package/tests/gentle-ai.test.ts +472 -4
  76. package/tests/gentle-shell.test.ts +201 -8
  77. package/tests/native-choice-list.test.ts +13 -0
  78. package/tests/native-review-cli.test.ts +0 -33
  79. package/tests/odd-routing-contract.test.ts +208 -0
  80. package/tests/orchestrator-budget.test.ts +17 -2
  81. package/tests/package-manifest.test.ts +115 -27
  82. package/tests/persona-single-channel.test.ts +3 -3
  83. package/tests/pi-pretty.test.ts +45 -0
  84. package/tests/profile-pin.test.ts +370 -0
  85. package/tests/quiet-tool-rendering.test.ts +32 -5
  86. package/tests/review-contract-prompt.test.ts +9 -0
  87. package/tests/review-controller.test.ts +0 -44
  88. package/tests/review-session-standing-permission-ipc.test.ts +427 -13
  89. package/tests/runtime-harness.mjs +4 -4
  90. package/tests/sdd-agent-tools.test.ts +15 -36
  91. package/tests/sdd-archive-replay.test.ts +82 -0
  92. package/tests/sdd-classical-continuation.test.ts +74 -0
  93. package/tests/sdd-execution-routing-contract.test.ts +18 -2
  94. package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
  95. package/tests/sdd-native-managed-uptake.test.ts +11 -21
  96. package/tests/sdd-no-attempts-contract.test.ts +15 -0
  97. package/tests/sdd-odd-integration.test.ts +33 -0
  98. package/tests/sdd-optional-research.test.ts +124 -0
  99. package/tests/sdd-planning-routing-contract.test.ts +1 -1
  100. package/tests/sdd-preflight-rpc-input.test.ts +125 -0
  101. package/tests/sdd-preflight.test.ts +1 -1
  102. package/tests/sdd-research-capabilities.test.ts +20 -162
  103. package/tests/sdd-selection-transport.test.ts +180 -88
  104. package/tests/sdd-status.test.ts +5 -778
  105. package/tests/sdd-task-truth.test.ts +43 -0
  106. package/tests/session-change-capture.test.ts +20 -2
  107. package/tests/session-changes.test.ts +11 -0
  108. package/tests/shell-bar.test.ts +21 -0
  109. package/tests/shell-card.test.ts +8 -6
  110. package/tests/shell-changes.test.ts +8 -0
  111. package/tests/shell-prompt.test.ts +41 -7
  112. package/tests/shell-sidebar-banner.test.ts +4 -4
  113. package/tests/shell-sidebar-layout.test.ts +97 -13
  114. package/tests/startup-banner.test.ts +55 -2
  115. package/tests/windows-hidden-processes.test.ts +303 -0
  116. package/tests/windows-session-bootstrap.test.ts +1772 -0
  117. package/tests/windows-session-compile.test.ts +170 -0
  118. package/tests/windows-session-transport.test.ts +754 -0
  119. package/assets/agents/sdd-sync.md +0 -146
  120. package/lib/openspec-guardrails.ts +0 -99
  121. package/tests/native-sdd-attempt-authority.test.ts +0 -240
  122. 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
- pi.events.emit(SESSION_CHANGE_EVENT, { sessionId: store.sessionId, notice: store.notice });
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;
@@ -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 dirty = model.dirty ? theme.fg(ROLE.DIRTY, `±${model.dirty}`) : "";
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
- ...((branch || dirty) ? [[branch, dirty].filter(Boolean).join(" ")] : []),
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: "Context",
150
- lines: [`${paintGauge(model.contextPercent, theme)} ${value(percent)} ${capacity}`],
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: [`${label("Cost")} ${value(formatCost(model.costTotal, model.subscription))}`, ...(usage ? [usage] : [])],
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
- ...(model.statuses.length ? [{ title: "Integrations", lines: model.statuses.map((status) => theme.fg(ROLE.STATUS, sanitizeStatus(status))) }] : []),
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
- // The left rail, corners included, carries the tone at full strength; the
64
- // rest of the frame stays in the theme's border color, so the state reads from the rail.
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 + soft(theme, card.tone, `${rule(targetWidth - 2)}╮`);
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], "╭") + soft(theme, card.tone, "─ ") + styledTitle + soft(theme, card.tone, ` ${fill}`) + tail + soft(theme, 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 + soft(theme, tone, "│");
98
- if (targetWidth === 3) return `${left} ${soft(theme, tone, "│")}`;
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} ${soft(theme, tone, "│")}`;
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 + soft(theme, tone, `${rule(targetWidth - 2)}╯`);
107
+ return left + frame(theme, tone, `${rule(targetWidth - 2)}╯`);
112
108
  }
113
109
 
114
110
  export function cardInnerWidth(width: number): number {
@@ -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}`;
@@ -23,9 +23,24 @@ const PETAL_TONE = {
23
23
 
24
24
  export type PetalTone = (typeof PETAL_TONE)[keyof typeof PETAL_TONE];
25
25
 
26
- // The Gentle themes map these to the rose ramp: active pink, rose, soft
27
- // rose, deep pink. The spin walks the ramp one shade per frame.
28
- const PETAL_TONE_FRAMES = [PETAL_TONE.BRIGHT, PETAL_TONE.ROSE, PETAL_TONE.SOFT, PETAL_TONE.DEEP] as const;
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 labelText = label ? ` ${options.fg(LABEL_ROLE, label)}` : "";
77
- const labelWidth = label ? label.length + 1 : 0;
78
- const fill = width - 3 - visibleWidth(glyph) - labelWidth - 1 - 1;
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("╭─ ") + petal + labelText + options.borderColor(` ${rule(fill)}╮`);
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-Pi ✿";
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-Pi") + " " + theme.fg("accent", "✿");
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", "changes", "agents", "todo"].map((key) => {
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
- const left = { render: (width: number) => root.render(width), invalidate() {}, [NODE]: () => original.call(root) };
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!;