@quandev104/pi-style 0.1.3 → 0.1.5

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 (39) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +8 -4
  3. package/dist/extensions/pi-style.js +3777 -958
  4. package/dist/extensions/pi-style.js.map +1 -1
  5. package/extension-src/pi-style/app/command-service.ts +2 -0
  6. package/extension-src/pi-style/domain/config-normalization.ts +21 -5
  7. package/extension-src/pi-style/domain/config-presets.ts +1 -1
  8. package/extension-src/pi-style/domain/config-types.ts +7 -3
  9. package/extension-src/pi-style/domain/status.ts +1 -1
  10. package/extension-src/pi-style/domain/theme.ts +6 -1
  11. package/extension-src/pi-style/features/editor/index.ts +169 -27
  12. package/extension-src/pi-style/features/messages/index.ts +66 -0
  13. package/extension-src/pi-style/features/tools/bash-execution.ts +112 -0
  14. package/extension-src/pi-style/features/tools/boxed/bash.ts +430 -172
  15. package/extension-src/pi-style/features/tools/boxed/batch.ts +50 -27
  16. package/extension-src/pi-style/features/tools/boxed/command-shape.ts +136 -0
  17. package/extension-src/pi-style/features/tools/boxed/edit.ts +28 -2
  18. package/extension-src/pi-style/features/tools/boxed/fallback.ts +47 -4
  19. package/extension-src/pi-style/features/tools/boxed/find.ts +2 -2
  20. package/extension-src/pi-style/features/tools/boxed/gh.ts +1012 -0
  21. package/extension-src/pi-style/features/tools/boxed/git.ts +1960 -0
  22. package/extension-src/pi-style/features/tools/boxed/grep.ts +2 -2
  23. package/extension-src/pi-style/features/tools/boxed/output-tree.ts +9 -10
  24. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +25 -4
  25. package/extension-src/pi-style/features/tools/boxed/read.ts +3 -3
  26. package/extension-src/pi-style/features/tools/boxed/session-config.ts +64 -4
  27. package/extension-src/pi-style/features/tools/boxed/shared.ts +41 -1
  28. package/extension-src/pi-style/features/tools/boxed/write.ts +19 -2
  29. package/extension-src/pi-style/pi/compatibility-coordinator.ts +17 -0
  30. package/extension-src/pi-style/pi/compatibility-probe.ts +104 -10
  31. package/extension-src/pi-style/pi/compatibility-registry.ts +19 -3
  32. package/extension-src/pi-style/pi/index.ts +18 -1
  33. package/extension-src/pi-style/pi/session-coordinator.ts +33 -1
  34. package/extension-src/pi-style/shared/box.ts +86 -25
  35. package/extension-src/pi-style/shared/split-diff.ts +9 -9
  36. package/package.json +1 -1
  37. package/themes/titanium-light.json +82 -0
  38. package/themes/titanium.json +79 -0
  39. package/themes/.gitkeep +0 -0
@@ -5,7 +5,8 @@ export type CompatibilitySubtype =
5
5
  | "native-skill-message"
6
6
  | "native-custom-message"
7
7
  | "tool-call-renderer"
8
- | "tool-result-renderer";
8
+ | "tool-result-renderer"
9
+ | "native-bash-execution";
9
10
 
10
11
  type CompatibilityShape = "supported" | "unsupported" | "conflict" | "installed" | "skipped";
11
12
 
@@ -171,6 +172,13 @@ function validateInstall(
171
172
  if (!options.shape) return skippedResult(options, "unsupported", options.diagnostic ?? "unsupported shape", current);
172
173
  const conflict = existing && activeConflict(existing, current, options.generation);
173
174
  if (conflict) return conflict;
175
+ if (options.kind === "add-method") {
176
+ // Additive install: the slot must be unowned (the method is inherited); the
177
+ // delegate receives the inherited native function as its fallback identity.
178
+ if (current !== undefined || ownDescriptor !== undefined)
179
+ return skippedResult(options, "conflict", "target already owns the additive method", current);
180
+ return undefined;
181
+ }
174
182
  if (typeof current !== "function")
175
183
  return skippedResult(
176
184
  options,
@@ -225,6 +233,8 @@ export function installDelegatingPatch(options: {
225
233
  expectedIdentity?: unknown;
226
234
  hasExpectedIdentity?: boolean;
227
235
  diagnostic?: string | undefined;
236
+ /** "add-method" installs a new own method on the prototype (nothing may already own the slot); the delegate receives the inherited native function as the fallback. */
237
+ kind?: "method" | "add-method";
228
238
  delegate: (original: unknown, thisArg: object, args: unknown[]) => unknown;
229
239
  }): InstallResult {
230
240
  const { target, method } = options;
@@ -235,7 +245,9 @@ export function installDelegatingPatch(options: {
235
245
  const validation = validateInstall(options, current, records.get(method), inspection.descriptor);
236
246
  if (validation) return validation;
237
247
  const originalDescriptor = Object.getOwnPropertyDescriptor(target, method);
238
- const originalIdentity = current;
248
+ // Additive installs have no current owner; the captured inherited native
249
+ // function (expectedIdentity) is both the fallback and the delegate's original.
250
+ const originalIdentity = options.kind === "add-method" ? options.expectedIdentity : current;
239
251
  let active = true;
240
252
  const installed = function (this: object, ...args: unknown[]): unknown {
241
253
  if (!active) return Reflect.apply(originalIdentity as (...values: unknown[]) => unknown, this, args);
@@ -260,7 +272,11 @@ export function installDelegatingPatch(options: {
260
272
  };
261
273
  try {
262
274
  Object.defineProperty(installed, "__piStyleCompatibilityRecord", { value: record, configurable: false });
263
- const descriptor = { ...originalDescriptor, value: installed };
275
+ // Additive installs have no original descriptor; write a fresh writable,
276
+ // configurable, non-enumerable own method so the slot stays reversible.
277
+ const descriptor = originalDescriptor
278
+ ? { ...originalDescriptor, value: installed }
279
+ : { value: installed, writable: true, enumerable: false, configurable: true };
264
280
  const wrote = Reflect.defineProperty(target, method, descriptor);
265
281
  registryTestHooks.afterWrite?.();
266
282
  const currentDescriptor = Object.getOwnPropertyDescriptor(target, method);
@@ -66,7 +66,24 @@ export default function piStyleExtension(pi: ExtensionAPI): void {
66
66
  await coordinator.start(event, ctx);
67
67
  });
68
68
  pi.on("agent_start", () => coordinator.app.runtime.current?.dismissStartup());
69
- pi.on("input", () => coordinator.app.runtime.current?.dismissStartup());
69
+ pi.on("input", (event, _ctx) => {
70
+ coordinator.app.runtime.current?.dismissStartup();
71
+ // Bare `!`/`!!` submit guard: Pi treats `!`-prefixed input as a direct bash
72
+ // command but falls through to normal message submission when the bang has
73
+ // no command after it — sending a literal `!` to the agent. Drop those
74
+ // accidental submits instead; Pi's submit path already cleared the editor
75
+ // (onChange("") resets isBashMode), so the input returns to the normal
76
+ // prompt without sending anything. Only the interactive input box is
77
+ // guarded; rpc/extension sources keep sending text verbatim.
78
+ if (event.source === "interactive") {
79
+ const trimmed = event.text.trimStart();
80
+ if (trimmed.startsWith("!")) {
81
+ const bangLength = trimmed.startsWith("!!") ? 2 : 1;
82
+ if (trimmed.slice(bangLength).trim() === "") return { action: "handled" };
83
+ }
84
+ }
85
+ return undefined;
86
+ });
70
87
  pi.on("tool_execution_start", () => coordinator.app.runtime.current?.dismissStartup());
71
88
  pi.on("model_select", (event) =>
72
89
  coordinator.app.update(
@@ -4,10 +4,15 @@ import type { ConfigFilePort } from "../app/config-storage.js";
4
4
  import { createPiStyleApp, type PiStyleApp } from "../app/index.js";
5
5
  import { resolveTheme } from "../domain/theme.js";
6
6
  import { setSpecialBlockTheme } from "../features/messages/special-blocks.js";
7
+ import { setBashExecutionTheme } from "../features/tools/bash-execution.js";
7
8
  import { resetBashTreeRegistry } from "../features/tools/boxed/bash.js";
8
9
  import { resetBatchRegistry } from "../features/tools/boxed/batch.js";
9
10
  import { resetGrepRegistry } from "../features/tools/boxed/grep.js";
10
- import { setToolsRenderConfig, type ToolsRenderConfig } from "../features/tools/boxed/session-config.js";
11
+ import {
12
+ setToolsRenderConfig,
13
+ stopAllElapsedTickers,
14
+ type ToolsRenderConfig,
15
+ } from "../features/tools/boxed/session-config.js";
11
16
  import { createCompatibilityCoordinator } from "./compatibility-coordinator.js";
12
17
  import {
13
18
  type CompatibilityCleanupResult,
@@ -90,6 +95,26 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
90
95
  const applyMessagesConfig = (config: import("../domain/config-types.js").NormalizedPiStyleConfig) => {
91
96
  sessionUi?.setHiddenThinkingLabel?.(config.messages.hideThinkingLabel ? "" : undefined);
92
97
  };
98
+ /**
99
+ * Auto-apply the configured pi-style theme (default "titanium") once per TUI
100
+ * session before any surface captures the active theme, so a fresh install
101
+ * renders with the intended palette. Failure-safe: an unresolvable target is
102
+ * never passed to Pi (its setTheme falls back to the dark theme on load
103
+ * error, which would clobber the user's theme), and "off" disables the
104
+ * surface for users who keep their own theme.
105
+ */
106
+ const applyAutoTheme = (
107
+ config: import("../domain/config-types.js").NormalizedPiStyleConfig,
108
+ ctx: ExtensionContext,
109
+ ) => {
110
+ const target = config.theme.autoApply;
111
+ if (ctx.mode !== "tui" || !target || target === "off") return;
112
+ const ui = ctx.ui;
113
+ if (ui?.theme?.name === target) return;
114
+ // Resolve before switching (see failure-safe note above).
115
+ if (!ui?.getTheme?.(target)) return;
116
+ ui.setTheme?.(target);
117
+ };
93
118
  const app: PiStyleApp = createPiStyleApp(
94
119
  undefined,
95
120
  {
@@ -150,11 +175,16 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
150
175
  resetBatchRegistry();
151
176
  resetGrepRegistry();
152
177
  resetBashTreeRegistry();
178
+ // Stop any 1s elapsed re-render ticker left by a tool that was still
179
+ // running when the session ended.
180
+ stopAllElapsedTickers();
153
181
  active = false;
154
182
  await app.reload();
155
183
  productGate = app.productPolicy.corePatchGate;
156
184
  active = true;
157
185
  compatibility.install(app.config, ctx.mode === "tui", productGate);
186
+ // Auto-apply the configured theme before surfaces capture the active one.
187
+ applyAutoTheme(app.config, ctx);
158
188
  // Session-scoped render configuration for the boxed tool/message surfaces.
159
189
  // Populated once per session (never inside render).
160
190
  sessionTheme = ctx.ui?.theme as never;
@@ -162,6 +192,7 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
162
192
  applyToolsRenderConfig(app.config);
163
193
  applyMessagesConfig(app.config);
164
194
  if (ctx.ui?.theme) setSpecialBlockTheme(ctx.ui.theme as never);
195
+ if (ctx.ui?.theme) setBashExecutionTheme(ctx.ui.theme as never);
165
196
  const toolDetails = collectToolDetails(pi.getActiveTools?.(), pi.getAllTools?.());
166
197
  app.sessionStart(
167
198
  {
@@ -216,6 +247,7 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
216
247
  resetBatchRegistry();
217
248
  resetGrepRegistry();
218
249
  resetBashTreeRegistry();
250
+ stopAllElapsedTickers();
219
251
  app.sessionShutdown();
220
252
  // Tier C prototype patches stay installed across session switches. Pi renders
221
253
  // the restored chat (renderBeforeBind) AFTER session_shutdown but BEFORE the
@@ -43,7 +43,15 @@ export interface BoxedRenderOptions {
43
43
  isError?: boolean;
44
44
  isPartial?: boolean;
45
45
  isPending?: boolean;
46
+ /** Execution has started but the tool is still running (title `◌` instead of `✓`). */
47
+ running?: boolean;
48
+ /** A result renderer already produced a continuation for this call, so the call
49
+ * leaves the box open instead of closing it with a pending label. */
50
+ resultSeen?: boolean;
46
51
  pendingText?: string;
52
+ /** Verbatim bottom-border label for the pending/running card (overrides the
53
+ * `… ${pendingText}` default, e.g. a live `◌ Running · 12.4s` status). */
54
+ pendingLabel?: string;
47
55
  state?: Record<string, unknown>;
48
56
  /** Wall-clock elapsed override (used when metrics are not in result.details). */
49
57
  elapsedMs?: number;
@@ -311,8 +319,6 @@ export function formatToolParamLines(args: unknown, theme?: BoxTheme): string[]
311
319
  return lines;
312
320
  }
313
321
 
314
- const RESET_INTENSITY = "\x1b[22m";
315
-
316
322
  function colorFromExtra(theme: BoxTheme, extraKey: string, fallbackColor: string, text: string): string {
317
323
  const color = getThemeExtra(theme, extraKey);
318
324
  if (color) {
@@ -333,27 +339,56 @@ function formatBoxedStatusIcon(theme: BoxTheme, isError?: boolean): string {
333
339
 
334
340
  /**
335
341
  * Colored `➔ Name` prefix for tool titles (identity color). The status glyph
336
- * (✓/✗) is appended separately by formatBoxedToolTitle.
342
+ * (✓/◌/✗) is appended separately by formatBoxedToolTitle.
337
343
  */
338
344
  export function formatToolTitlePrefix(theme: BoxTheme, name: string): string {
339
345
  return colorFromExtra(theme, "bashPromptColor", "bashMode", `➔ ${name}`);
340
346
  }
341
347
 
342
- export function formatBoxedToolTitle(theme: BoxTheme, name: string, isError?: boolean): string {
348
+ export type BoxedTitleStatus = "running" | "pending";
349
+
350
+ /**
351
+ * Boxed tool title: `➔ Name ✓` when settled, `➔ Name ◌` while running, plain
352
+ * `➔ Name` while pending, and a fully error-colored `➔ Name ✗` on failure.
353
+ * The ✓/◌ glyphs are never shown before the tool settles, so a card that is
354
+ * still executing never reads as finished.
355
+ */
356
+ export function formatBoxedToolTitle(
357
+ theme: BoxTheme,
358
+ name: string,
359
+ isError?: boolean,
360
+ status?: BoxedTitleStatus,
361
+ ): string {
343
362
  // On failure the whole title turns error-colored (not just the ✗) so a failed
344
363
  // tool reads instantly; on success the tool keeps its identity color and only
345
364
  // the ✓ carries the success color.
346
365
  const coloredTitle = isError
347
366
  ? theme.fg("error", `➔ ${name} ✗`)
348
- : `${formatToolTitlePrefix(theme, name)} ${formatBoxedStatusIcon(theme, false)}`;
367
+ : status === "running"
368
+ ? `${formatToolTitlePrefix(theme, name)} ${theme.fg("text", "◌")}`
369
+ : status === "pending"
370
+ ? formatToolTitlePrefix(theme, name)
371
+ : `${formatToolTitlePrefix(theme, name)} ${formatBoxedStatusIcon(theme, false)}`;
349
372
  return typeof theme?.bold === "function" ? theme.bold(coloredTitle) : coloredTitle;
350
373
  }
351
374
 
375
+ /** Live running status label for pending/running cards and streaming footers. */
376
+ export function formatBoxedRunningStatus(theme: BoxTheme, elapsedMs: number | undefined): string {
377
+ const elapsed = elapsedMs === undefined ? "" : `${theme.fg("text", ` · ${(elapsedMs / 1000).toFixed(1)}s`)}`;
378
+ return `${theme.fg("dim", "◌ Running")}${elapsed}`;
379
+ }
380
+
381
+ /** Structural line — box frame, tree branch, divider, gutter — wrapped in
382
+ * dim terminal-default intensity. Visible in every theme; matches omp. */
383
+ export function dimLine(text: string): string {
384
+ return `\x1b[2m${text}\x1b[22m`;
385
+ }
386
+
352
387
  function boxText(theme: BoxTheme, text: string): string {
353
- return `${RESET_INTENSITY}${theme.fg("borderMuted", text)}`;
388
+ return dimLine(text);
354
389
  }
355
390
  function boxFrameText(theme: BoxTheme, text: string): string {
356
- return `${RESET_INTENSITY}${theme.fg("border", text)}`;
391
+ return dimLine(text);
357
392
  }
358
393
 
359
394
  export function boxedToolBgName(isError?: boolean, isPartial?: boolean): string {
@@ -571,7 +606,12 @@ export function renderBoxedToolCall(
571
606
  },
572
607
  render(width: number): string[] {
573
608
  if (cache?.width === width) return cache.lines;
574
- const title = formatBoxedToolTitle(theme, toolName, options.isError);
609
+ const title = formatBoxedToolTitle(
610
+ theme,
611
+ toolName,
612
+ options.isError,
613
+ options.isPending ? (options.running ? "running" : "pending") : undefined,
614
+ );
575
615
  const headerLabel = options.headerDetail ? `${title} · ${options.headerDetail}` : title;
576
616
  const renderedWidth = boxWidth(width);
577
617
  const lines = [
@@ -579,22 +619,26 @@ export function renderBoxedToolCall(
579
619
  boxBlankLine(theme, renderedWidth),
580
620
  ...detailLines.flatMap((line) => boxedWrappedLines(theme, line, renderedWidth)),
581
621
  ];
582
- if (options.isPending) {
583
- const pendingText = options.pendingText ?? "Waiting for output…";
622
+ if (options.isPending && !options.resultSeen) {
623
+ // Pending/running card: close the box with the status label. Once a
624
+ // result renderer has produced a continuation, the box stays open and
625
+ // that continuation closes it.
626
+ const pendingLabel =
627
+ options.pendingLabel ?? theme.fg("dim", `… ${options.pendingText ?? "Waiting for output…"}`);
584
628
  lines.push(
585
629
  boxBlankLine(theme, renderedWidth),
586
630
  boxLabeledBorder(
587
631
  theme,
588
632
  BOX_ROUND_BOTTOM_LEFT,
589
633
  BOX_ROUND_BOTTOM_RIGHT,
590
- theme.fg("dim", `… ${pendingText}`),
634
+ pendingLabel,
591
635
  undefined,
592
636
  renderedWidth,
593
637
  ),
594
638
  );
595
639
  } else {
596
640
  // Leave the box open with trailing breathing room; the result renderer
597
- // continues it with the Response divider.
641
+ // continues it with the result divider.
598
642
  lines.push(boxBlankLine(theme, renderedWidth));
599
643
  }
600
644
  cache = { width, lines };
@@ -624,7 +668,12 @@ export function renderCompactBoxedToolCall(
624
668
  invalidate() {},
625
669
  render(width: number): string[] {
626
670
  const renderedWidth = boxWidth(width);
627
- const title = formatBoxedToolTitle(theme, toolName, options.isError);
671
+ const title = formatBoxedToolTitle(
672
+ theme,
673
+ toolName,
674
+ options.isError,
675
+ options.isPending ? (options.running ? "running" : "pending") : undefined,
676
+ );
628
677
  const headerLabel = detailLine ? `${title} · ${detailLine}` : title;
629
678
  const compactFooter =
630
679
  typeof options.state?.[COMPACT_FOOTER_KEY] === "string" ? options.state[COMPACT_FOOTER_KEY] : "";
@@ -649,19 +698,23 @@ export function renderCompactBoxedToolCall(
649
698
  ),
650
699
  );
651
700
  } else if (options.isPending) {
652
- const pendingText = options.pendingText ?? "Waiting for output…";
701
+ const pendingLabel =
702
+ options.pendingLabel ??
703
+ (options.running
704
+ ? formatBoxedRunningStatus(theme, undefined)
705
+ : theme.fg("dim", `… ${options.pendingText ?? "Waiting for output…"}`));
653
706
  lines.push(
654
707
  boxLabeledBorder(
655
708
  theme,
656
709
  BOX_ROUND_BOTTOM_LEFT,
657
710
  BOX_ROUND_BOTTOM_RIGHT,
658
- theme.fg("dim", `… ${pendingText}`),
711
+ pendingLabel,
659
712
  options.bottomRightLabel,
660
713
  renderedWidth,
661
714
  ),
662
715
  );
663
716
  } else {
664
- // No footer yet (transient, or the result opens the Response divider):
717
+ // No footer yet (transient, or the result opens the result divider):
665
718
  // leave the box open so the result renderer continues the same box.
666
719
  }
667
720
  return lines;
@@ -689,6 +742,10 @@ export function renderBoxedToolResult(
689
742
  expandHint?: string;
690
743
  isError?: boolean;
691
744
  isPartial?: boolean;
745
+ /** Skip the result divider entirely (streaming continuation into an open call box). */
746
+ showDivider?: boolean;
747
+ /** Error state marker prepended to the body (default `✗ Error`). */
748
+ errorLabel?: string;
692
749
  } = {},
693
750
  ): Component {
694
751
  let cache: RenderLinesCache | null = null;
@@ -702,7 +759,7 @@ export function renderBoxedToolResult(
702
759
  const renderedWidth = boxWidth(width);
703
760
  const maxContentWidth = boxInnerWidth(renderedWidth);
704
761
  const bodyLines = typeof body === "function" ? body(maxContentWidth) : body.render(maxContentWidth);
705
- const errorPrefix = options.isError ? [theme.fg("error", "✗ Error")] : [];
762
+ const errorPrefix = options.isError ? [theme.fg("error", options.errorLabel ?? "✗ Error")] : [];
706
763
  const outputLines =
707
764
  bodyLines.length > 0
708
765
  ? [...errorPrefix, ...bodyLines]
@@ -713,14 +770,18 @@ export function renderBoxedToolResult(
713
770
  ? options.dividerLabel(renderedWidth)
714
771
  : (options.dividerLabel ?? "Response");
715
772
  const rendered = [
716
- boxLabeledBorder(
717
- theme,
718
- BOX_DIVIDER_LEFT,
719
- BOX_DIVIDER_RIGHT,
720
- theme.fg("dim", dividerText),
721
- options.dividerRightLabel ? theme.fg("dim", options.dividerRightLabel) : undefined,
722
- renderedWidth,
723
- ),
773
+ ...(options.showDivider === false
774
+ ? []
775
+ : [
776
+ boxLabeledBorder(
777
+ theme,
778
+ BOX_DIVIDER_LEFT,
779
+ BOX_DIVIDER_RIGHT,
780
+ theme.fg("dim", dividerText),
781
+ options.dividerRightLabel ? theme.fg("dim", options.dividerRightLabel) : undefined,
782
+ renderedWidth,
783
+ ),
784
+ ]),
724
785
  boxBlankLine(theme, renderedWidth),
725
786
  ...renderBoxedOutputLines(theme, outputLines, renderedWidth, options.renderLineBudget ?? outputLines.length),
726
787
  boxBlankLine(theme, renderedWidth),
@@ -7,6 +7,7 @@ import { highlightCode } from "@earendil-works/pi-coding-agent";
7
7
  import type { Component } from "@earendil-works/pi-tui";
8
8
 
9
9
  import { stripAnsi } from "./ansi.js";
10
+ import { dimLine } from "./box.js";
10
11
  import { safeTruncateToWidth, safeVisibleWidth } from "./render-budget.js";
11
12
 
12
13
  // ── Types ──────────────────────────────────────────────────────────
@@ -761,7 +762,7 @@ class SplitDiffRenderer {
761
762
  lineKind === "add" ? "toolDiffAdded" : lineKind === "remove" ? "toolDiffRemoved" : "borderMuted";
762
763
  const marker = this.ctx.fg(markerColor, markerChar);
763
764
  const lineNumber = this.ctx.fg("dim", " ".repeat(this.ctx.lineNumberWidth));
764
- const divider = this.ctx.fg("borderMuted", " │ ");
765
+ const divider = dimLine(" │ ");
765
766
  const prefix = `${marker} ${lineNumber}${divider}`;
766
767
  const prefixPlain = `${markerChar} ${" ".repeat(this.ctx.lineNumberWidth)} │ `;
767
768
  const tailWidth = Math.max(0, columnWidth - safeVisibleWidth(prefixPlain));
@@ -791,14 +792,14 @@ class SplitDiffRenderer {
791
792
  this.ctx.fg(markerColor, markerChar) +
792
793
  " " +
793
794
  this.ctx.fg(this.getNumberColor(lineKind), lineNumber) +
794
- this.ctx.fg("borderMuted", " │ ");
795
+ dimLine(" │ ");
795
796
  const firstPrefixPlain = `${markerChar} ${lineNumber} │ `;
796
797
 
797
798
  const contPrefixAnsi =
798
799
  this.ctx.fg(markerColor, markerChar) +
799
800
  " " +
800
801
  this.ctx.fg("dim", " ".repeat(this.ctx.lineNumberWidth)) +
801
- this.ctx.fg("borderMuted", " │ ");
802
+ dimLine(" │ ");
802
803
  const contPrefixPlain = `${markerChar} ${" ".repeat(this.ctx.lineNumberWidth)} │ `;
803
804
 
804
805
  const codeWidth = Math.max(1, columnWidth - safeVisibleWidth(firstPrefixPlain));
@@ -865,7 +866,7 @@ class SplitDiffRenderer {
865
866
 
866
867
  render(width: number): string[] {
867
868
  const safeWidth = Math.max(20, width);
868
- const columnSeparator = this.ctx.fg("borderMuted", " │ ");
869
+ const columnSeparator = dimLine(" │ ");
869
870
  const separatorWidth = safeVisibleWidth(stripAnsi(columnSeparator));
870
871
  const leftWidth = Math.max(20, Math.floor((safeWidth - separatorWidth) / 2));
871
872
  const rightWidth = Math.max(20, safeWidth - separatorWidth - leftWidth);
@@ -877,15 +878,14 @@ class SplitDiffRenderer {
877
878
  if (dividerIndex >= 0 && dividerIndex < chars.length) {
878
879
  chars[dividerIndex] = junction;
879
880
  }
880
- return this.ctx.fg("borderMuted", chars.join(""));
881
+ return dimLine(chars.join(""));
881
882
  };
882
883
 
883
884
  const formatHeaderCell = (label: string, columnWidth: number): string => {
884
885
  // Keep marker+space columns, then place label inside the line-number column.
885
886
  const markerPad = " ";
886
887
  const lineNumberLabel = fitToWidth(label, this.ctx.lineNumberWidth);
887
- const prefixAnsi =
888
- this.ctx.fg("borderMuted", markerPad) + this.ctx.fg("dim", lineNumberLabel) + this.ctx.fg("borderMuted", " │ ");
888
+ const prefixAnsi = dimLine(markerPad) + this.ctx.fg("dim", lineNumberLabel) + dimLine(" │ ");
889
889
  const prefixPlain = `${markerPad}${stripAnsi(lineNumberLabel)} │ `;
890
890
  const codeWidth = Math.max(0, columnWidth - safeVisibleWidth(prefixPlain));
891
891
  return padRenderedLineWidth(prefixAnsi + " ".repeat(codeWidth), columnWidth);
@@ -894,7 +894,7 @@ class SplitDiffRenderer {
894
894
  const lines: string[] = [];
895
895
  lines.push(
896
896
  padRenderedLineWidth(
897
- formatBorderCell(leftWidth, "┬") + this.ctx.fg("borderMuted", "─┬─") + formatBorderCell(rightWidth, "┬"),
897
+ formatBorderCell(leftWidth, "┬") + dimLine("─┬─") + formatBorderCell(rightWidth, "┬"),
898
898
  safeWidth,
899
899
  ),
900
900
  );
@@ -931,7 +931,7 @@ class SplitDiffRenderer {
931
931
 
932
932
  lines.push(
933
933
  padRenderedLineWidth(
934
- formatBorderCell(leftWidth, "┴") + this.ctx.fg("borderMuted", "─┴─") + formatBorderCell(rightWidth, "┴"),
934
+ formatBorderCell(leftWidth, "┴") + dimLine("─┴─") + formatBorderCell(rightWidth, "┴"),
935
935
  safeWidth,
936
936
  ),
937
937
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quandev104/pi-style",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "A native-layout, cohesive visual style package for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,82 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "titanium-light",
4
+ "vars": {
5
+ "teal": "#5a8080",
6
+ "blue": "#547da7",
7
+ "green": "#588458",
8
+ "red": "#aa5555",
9
+ "yellow": "#9a7326",
10
+ "mediumGray": "#6c6c6c",
11
+ "dimGray": "#767676",
12
+ "lightGray": "#b0b0b0",
13
+ "selectedBg": "#d0d0e0",
14
+ "userMsgBg": "#e8e8e8",
15
+ "toolPendingBg": "#e8e8f0",
16
+ "toolSuccessBg": "#e8f0e8",
17
+ "toolErrorBg": "#f0e8e8",
18
+ "customMsgBg": "#ede7f6"
19
+ },
20
+ "colors": {
21
+ "accent": "teal",
22
+ "border": "blue",
23
+ "borderAccent": "teal",
24
+ "borderMuted": "lightGray",
25
+ "success": "green",
26
+ "error": "red",
27
+ "warning": "yellow",
28
+ "muted": "mediumGray",
29
+ "dim": "dimGray",
30
+ "text": "",
31
+ "thinkingText": "mediumGray",
32
+
33
+ "selectedBg": "selectedBg",
34
+ "userMessageBg": "userMsgBg",
35
+ "userMessageText": "",
36
+ "customMessageBg": "customMsgBg",
37
+ "customMessageText": "",
38
+ "customMessageLabel": "#7e57c2",
39
+ "toolPendingBg": "toolPendingBg",
40
+ "toolSuccessBg": "toolSuccessBg",
41
+ "toolErrorBg": "toolErrorBg",
42
+ "toolTitle": "",
43
+ "toolOutput": "mediumGray",
44
+
45
+ "mdHeading": "yellow",
46
+ "mdLink": "blue",
47
+ "mdLinkUrl": "dimGray",
48
+ "mdCode": "teal",
49
+ "mdCodeBlock": "green",
50
+ "mdCodeBlockBorder": "mediumGray",
51
+ "mdQuote": "mediumGray",
52
+ "mdQuoteBorder": "mediumGray",
53
+ "mdHr": "mediumGray",
54
+ "mdListBullet": "green",
55
+
56
+ "toolDiffAdded": "green",
57
+ "toolDiffRemoved": "red",
58
+ "toolDiffContext": "mediumGray",
59
+ "syntaxComment": "#008000",
60
+ "syntaxKeyword": "#0000FF",
61
+ "syntaxFunction": "#795E26",
62
+ "syntaxVariable": "#001080",
63
+ "syntaxString": "#A31515",
64
+ "syntaxNumber": "#098658",
65
+ "syntaxType": "#267F99",
66
+ "syntaxOperator": "#000000",
67
+ "syntaxPunctuation": "#000000",
68
+
69
+ "thinkingOff": "lightGray",
70
+ "thinkingMinimal": "#767676",
71
+ "thinkingLow": "blue",
72
+ "thinkingMedium": "teal",
73
+ "thinkingHigh": "#875f87",
74
+ "thinkingXhigh": "#8b008b",
75
+ "bashMode": "green"
76
+ },
77
+ "export": {
78
+ "pageBg": "#f8f8f8",
79
+ "cardBg": "#ffffff",
80
+ "infoBg": "#fffae6"
81
+ }
82
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "titanium",
4
+ "vars": {
5
+ "brushedTitanium": "#151820",
6
+ "darkTitanium": "#0f1216",
7
+ "electricBlue": "#00b4ff",
8
+ "deepBlue": "#0082b3",
9
+ "titaniumGold": "#d4c090",
10
+ "brightAluminum": "#e8ecf4",
11
+ "dimAluminum": "#9ca3b0",
12
+ "warningAmber": "#ffb347",
13
+ "readoutGreen": "#00ff88",
14
+ "alertRed": "#ff4757",
15
+ "subtleGray": "#2a3038"
16
+ },
17
+ "colors": {
18
+ "accent": "electricBlue",
19
+ "border": "subtleGray",
20
+ "borderAccent": "electricBlue",
21
+ "borderMuted": "#1f252d",
22
+ "success": "readoutGreen",
23
+ "error": "alertRed",
24
+ "warning": "warningAmber",
25
+ "muted": "dimAluminum",
26
+ "dim": "#6b7280",
27
+ "text": "",
28
+ "thinkingText": "dimAluminum",
29
+
30
+ "selectedBg": "deepBlue",
31
+ "userMessageBg": "darkTitanium",
32
+ "userMessageText": "",
33
+ "customMessageBg": "subtleGray",
34
+ "customMessageText": "",
35
+ "customMessageLabel": "titaniumGold",
36
+ "toolPendingBg": "darkTitanium",
37
+ "toolSuccessBg": "darkTitanium",
38
+ "toolErrorBg": "#1a0f10",
39
+ "toolTitle": "",
40
+ "toolOutput": "dimAluminum",
41
+
42
+ "mdHeading": "electricBlue",
43
+ "mdLink": "electricBlue",
44
+ "mdLinkUrl": "deepBlue",
45
+ "mdCode": "readoutGreen",
46
+ "mdCodeBlock": "dimAluminum",
47
+ "mdCodeBlockBorder": "subtleGray",
48
+ "mdQuote": "dimAluminum",
49
+ "mdQuoteBorder": "subtleGray",
50
+ "mdHr": "subtleGray",
51
+ "mdListBullet": "electricBlue",
52
+
53
+ "toolDiffAdded": "readoutGreen",
54
+ "toolDiffRemoved": "alertRed",
55
+ "toolDiffContext": "dimAluminum",
56
+ "syntaxComment": "#6b7280",
57
+ "syntaxKeyword": "electricBlue",
58
+ "syntaxFunction": "readoutGreen",
59
+ "syntaxVariable": "brightAluminum",
60
+ "syntaxString": "titaniumGold",
61
+ "syntaxNumber": "warningAmber",
62
+ "syntaxType": "electricBlue",
63
+ "syntaxOperator": "electricBlue",
64
+ "syntaxPunctuation": "dimAluminum",
65
+
66
+ "thinkingOff": "#4a5058",
67
+ "thinkingMinimal": "#5a6068",
68
+ "thinkingLow": "#6a7078",
69
+ "thinkingMedium": "dimAluminum",
70
+ "thinkingHigh": "electricBlue",
71
+ "thinkingXhigh": "titaniumGold",
72
+ "bashMode": "readoutGreen"
73
+ },
74
+ "export": {
75
+ "pageBg": "brushedTitanium",
76
+ "cardBg": "darkTitanium",
77
+ "infoBg": "subtleGray"
78
+ }
79
+ }
package/themes/.gitkeep DELETED
File without changes