@quandev104/pi-style 0.2.0 → 0.2.2

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 (33) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/dist/extensions/pi-style.js +1256 -491
  4. package/dist/extensions/pi-style.js.map +1 -1
  5. package/extension-src/pi-style/app/index.ts +3 -2
  6. package/extension-src/pi-style/app/runtime.ts +99 -86
  7. package/extension-src/pi-style/app/snapshot.ts +41 -2
  8. package/extension-src/pi-style/domain/status-renderer.ts +40 -8
  9. package/extension-src/pi-style/domain/status.ts +15 -5
  10. package/extension-src/pi-style/domain/theme.ts +32 -1
  11. package/extension-src/pi-style/features/editor/index.ts +97 -66
  12. package/extension-src/pi-style/features/messages/index.ts +469 -90
  13. package/extension-src/pi-style/features/status-line/index.ts +41 -11
  14. package/extension-src/pi-style/features/tools/bash-execution.ts +12 -1
  15. package/extension-src/pi-style/features/tools/boxed/bash.ts +195 -66
  16. package/extension-src/pi-style/features/tools/boxed/batch.ts +60 -10
  17. package/extension-src/pi-style/features/tools/boxed/edit.ts +45 -25
  18. package/extension-src/pi-style/features/tools/boxed/find.ts +9 -4
  19. package/extension-src/pi-style/features/tools/boxed/git.ts +46 -2
  20. package/extension-src/pi-style/features/tools/boxed/grep.ts +9 -2
  21. package/extension-src/pi-style/features/tools/boxed/ls.ts +9 -4
  22. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +45 -22
  23. package/extension-src/pi-style/features/tools/boxed/read.ts +4 -2
  24. package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -14
  25. package/extension-src/pi-style/features/tools/boxed/shared.ts +51 -0
  26. package/extension-src/pi-style/features/tools/boxed/turn-summary.ts +12 -0
  27. package/extension-src/pi-style/pi/compatibility-probe.ts +1 -1
  28. package/extension-src/pi-style/pi/index.ts +28 -13
  29. package/extension-src/pi-style/pi/session-usage.ts +204 -21
  30. package/extension-src/pi-style/shared/ansi.ts +17 -5
  31. package/extension-src/pi-style/shared/box.ts +83 -6
  32. package/extension-src/pi-style/shared/split-diff.ts +8 -5
  33. package/package.json +1 -1
@@ -39,13 +39,30 @@ interface EditorOptions {
39
39
  onSnapshot: (snapshot: StatusSnapshot) => void;
40
40
  }
41
41
 
42
+ interface RenderPlan {
43
+ readonly style: "compact" | "boxed" | "dock" | "native";
44
+ readonly kind: "compact" | "boxed" | "outline" | "rounded" | "native";
45
+ readonly prompt: string;
46
+ readonly promptWidth: number;
47
+ readonly padding: number;
48
+ readonly sideReserve: number;
49
+ readonly renderWidth: number;
50
+ readonly innerWidth: number;
51
+ readonly prefix: string;
52
+ readonly continuation: string;
53
+ }
54
+
42
55
  const widthOf = visibleWidth;
43
56
 
44
57
  function widthSafe(value: string, width: number): string {
45
58
  if (width <= 0) return "";
46
- const fitted = widthOf(value) > width ? truncateAnsi(value, width, "") : value;
47
- const current = widthOf(fitted);
48
- return current < width ? fitted + " ".repeat(width - current) : fitted;
59
+ const measured = widthOf(value);
60
+ if (measured > width) {
61
+ const fitted = truncateAnsi(value, width, "");
62
+ const current = widthOf(fitted);
63
+ return current < width ? fitted + " ".repeat(width - current) : fitted;
64
+ }
65
+ return measured < width ? value + " ".repeat(width - measured) : value;
49
66
  }
50
67
 
51
68
  function isNativeBorderLine(line: string): boolean {
@@ -129,7 +146,10 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
129
146
  private readonly fullTheme: EditorOptions["fullTheme"];
130
147
  private readonly onSnapshot: (snapshot: StatusSnapshot) => void;
131
148
  private semantic: ResolvedTheme;
149
+ /** Set when config changes; invalidate() then rebuilds the (otherwise stable) semantic theme. */
150
+ private semanticDirty = false;
132
151
  private disposed = false;
152
+ private renderPlanCache: { key: string; plan: RenderPlan } | undefined;
133
153
 
134
154
  constructor(tui: Tui, theme: PiEditorTheme, keybindings: Keybindings, options: EditorOptions) {
135
155
  super(tui, theme, keybindings);
@@ -151,7 +171,7 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
151
171
  configure(config: NormalizedPiStyleConfig): void {
152
172
  if (this.disposed) return;
153
173
  this.config = config;
154
- this.semantic = semanticTheme(this.piTheme, config);
174
+ this.semanticDirty = true;
155
175
  this.invalidate();
156
176
  }
157
177
 
@@ -163,89 +183,44 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
163
183
 
164
184
  override invalidate(): void {
165
185
  super.invalidate();
166
- this.semantic = semanticTheme(this.piTheme, this.config);
186
+ // The semantic theme is derived from (piTheme, config); piTheme is fixed per
187
+ // instance and config changes go through configure(), so keystroke-driven
188
+ // invalidations reuse the cached instance instead of rebuilding it.
189
+ if (this.semanticDirty) {
190
+ this.semantic = semanticTheme(this.piTheme, this.config);
191
+ this.semanticDirty = false;
192
+ }
193
+ this.renderPlanCache = undefined;
167
194
  this.tui.requestRender();
168
195
  }
169
196
 
170
197
  override render(width: number): string[] {
171
198
  if (width <= 0) return [];
172
- const nativeLines = super.render(width);
173
- const style = this.styleFor(width);
174
- // Autocomplete (slash menu / @-mentions) restructure: Pi draws the
175
- // suggestions after its own bottom border, which pushes the below-editor
176
- // widgets (status line) down. Re-frame the native output so the dropdown
177
- // lives INSIDE the input box, keeping the footer directly below the input.
178
- // Native layout: [top border, text lines, bottom border, dropdown lines...].
179
- if ((this as unknown as { autocompleteState?: unknown }).autocompleteState) {
180
- const prompt = this.prompt();
181
- const padding = this.paddingFor(width, style);
182
- const promptWidth = widthOf(prompt) + 1;
183
- const prefix = `${" ".repeat(padding)}${prompt} `;
184
- const continuation = " ".repeat(padding + promptWidth);
185
- const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
186
- const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
187
- const body = nativeLines.slice(1, split);
188
- const dropdown = nativeLines.slice(split);
189
- const border = this.borderFor();
190
- const kind = this.frameKind(style);
191
- const renderWidth = width - (kind === "rounded" ? 2 : 0);
192
- const sideColor = kind === "rounded" ? this.borderColorFor() : undefined;
193
- const wrap = (line: string) =>
194
- kind === "rounded" && sideColor ? `${sideColor("│")}${line}${sideColor("│")}` : line;
195
- const bashHidden = this.bashHiddenCount();
196
- const renderedBody = body.map((line, index) => {
197
- const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
198
- return wrap(widthSafe(`${index === 0 ? prefix : continuation}${source}`, renderWidth));
199
- });
200
- const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, renderWidth)));
201
- if (kind === "rounded") {
202
- return [
203
- border(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
204
- ...renderedBody,
205
- ...dropdownLines,
206
- border(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
207
- ];
208
- }
209
- return [border("─".repeat(width)), ...renderedBody, ...dropdownLines, border("─".repeat(width))];
210
- }
211
- if (style === "native") return nativeLines.map((line) => widthSafe(line, width));
199
+ const plan = this.renderPlan(width);
200
+ const autocompleteState = (this as unknown as { autocompleteState?: unknown }).autocompleteState;
201
+ if (plan.style === "native") return super.render(width).map((line) => widthSafe(line, width));
202
+ if (autocompleteState) return this.renderAutocompleteFrame(width, plan);
212
203
 
213
- const prompt = this.prompt();
214
- const promptWidth = widthOf(prompt) + 1;
215
- const padding = this.paddingFor(width, style);
216
- const kind = this.frameKind(style);
217
- const sideReserve = kind === "rounded" ? 2 : 0;
218
- const renderWidth = Math.max(1, width - sideReserve);
219
- const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
220
- const innerLines = super.render(innerWidth);
204
+ const innerLines = super.render(plan.innerWidth);
221
205
  if (innerLines.length === 0) return [];
222
-
223
206
  const body = innerLines.slice(1, -1);
224
- const prefix = `${" ".repeat(padding)}${prompt} `;
225
- const continuation = " ".repeat(padding + promptWidth);
226
207
  const hint = this.config.editor.hint;
227
208
  const showHint = hint !== "" && this.getText() === "";
228
209
  const bashHidden = this.bashHiddenCount();
229
210
  const renderedBody = body.map((line, index) => {
230
- const lead = index === 0 ? prefix : continuation;
211
+ const lead = index === 0 ? plan.prefix : plan.continuation;
231
212
  const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
232
213
  let content = `${lead}${source}`;
233
- // Empty-input hint: the cursor block (first cell of the native empty
234
- // line) stays at the input position, the dim hint trails it. The native
235
- // line is pre-padded to renderWidth with literal spaces; drop them from
236
- // the raw end (safe: no ANSI follows the padding) before appending the
237
- // hint, or the hint is truncated away by widthSafe. Typing any character
238
- // makes the text non-empty and the hint disappears.
239
214
  if (showHint && index === 0 && line) {
240
215
  let end = content.length;
241
216
  while (end > 0 && content[end - 1] === " ") end--;
242
217
  if (end < content.length) content = content.slice(0, end);
243
218
  content += this.semantic.apply("hint", hint);
244
219
  }
245
- return widthSafe(content, renderWidth);
220
+ return widthSafe(content, plan.renderWidth);
246
221
  });
247
- const metadata = this.metadata(width, style);
248
- const framed = this.frame(width, style, renderedBody, metadata);
222
+ const metadata = this.metadata(width, plan.style);
223
+ const framed = this.frame(width, plan.style, renderedBody, metadata);
249
224
  return framed.map((line) => widthSafe(line, width));
250
225
  }
251
226
 
@@ -255,6 +230,62 @@ export class StyledEditor extends CustomEditor implements EditorComponent {
255
230
  this.invalidate();
256
231
  }
257
232
 
233
+ private renderPlan(width: number): RenderPlan {
234
+ const style = this.styleFor(width);
235
+ const prompt = this.prompt();
236
+ const kind = this.frameKind(style);
237
+ const key = `${width}:${style}:${kind}:${prompt}`;
238
+ if (this.renderPlanCache?.key === key) return this.renderPlanCache.plan;
239
+ const promptWidth = widthOf(prompt) + 1;
240
+ const padding = this.paddingFor(width, style);
241
+ const sideReserve = kind === "rounded" ? 2 : 0;
242
+ const renderWidth = Math.max(1, width - sideReserve);
243
+ const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
244
+ const prefix = `${" ".repeat(padding)}${prompt} `;
245
+ const continuation = " ".repeat(padding + promptWidth);
246
+ const plan = {
247
+ style,
248
+ kind,
249
+ prompt,
250
+ promptWidth,
251
+ padding,
252
+ sideReserve,
253
+ renderWidth,
254
+ innerWidth,
255
+ prefix,
256
+ continuation,
257
+ };
258
+ this.renderPlanCache = { key, plan };
259
+ return plan;
260
+ }
261
+
262
+ private renderAutocompleteFrame(width: number, plan: RenderPlan): string[] {
263
+ const nativeLines = super.render(width);
264
+ const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
265
+ const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
266
+ const body = nativeLines.slice(1, split);
267
+ const dropdown = nativeLines.slice(split);
268
+ const border = this.borderFor();
269
+ const sideColor = plan.kind === "rounded" ? this.borderColorFor() : undefined;
270
+ const wrap = (line: string) =>
271
+ plan.kind === "rounded" && sideColor ? `${sideColor("│")}${line}${sideColor("│")}` : line;
272
+ const bashHidden = this.bashHiddenCount();
273
+ const renderedBody = body.map((line, index) => {
274
+ const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
275
+ return wrap(widthSafe(`${index === 0 ? plan.prefix : plan.continuation}${source}`, plan.renderWidth));
276
+ });
277
+ const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, plan.renderWidth)));
278
+ if (plan.kind === "rounded") {
279
+ return [
280
+ border(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
281
+ ...renderedBody,
282
+ ...dropdownLines,
283
+ border(`╰${"─".repeat(Math.max(0, width - 2))}╯`),
284
+ ];
285
+ }
286
+ return [border("─".repeat(width)), ...renderedBody, ...dropdownLines, border("─".repeat(width))];
287
+ }
288
+
258
289
  private prompt(): string {
259
290
  if (this.isBashMode()) {
260
291
  // Bash mode (`!` prefix): the prompt glyph becomes the bash icon and the