@bismawy/pi-arnative 0.5.1 → 0.5.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.
- package/CHANGELOG.md +14 -0
- package/extensions/arnative.ts +2 -2
- package/extensions/footer.ts +10 -9
- package/extensions/section-headers.ts +32 -17
- package/extensions/tools.ts +4 -4
- package/extensions/ui-render-tweaks.ts +21 -3
- package/extensions/usage.ts +4 -3
- package/lib/ansi.ts +51 -7
- package/package.json +1 -1
- package/themes/arnative-cyberpunk.json +2 -1
- package/themes/arnative-dracula.json +2 -1
- package/themes/arnative-emerald.json +2 -1
- package/themes/arnative-gruvbox.json +2 -1
- package/themes/arnative-matrix.json +2 -1
- package/themes/arnative-nord.json +2 -1
- package/themes/arnative-sun.json +2 -1
- package/themes/arnative-synthwave.json +2 -1
- package/themes/arnative-violet.json +2 -1
- package/themes/arnative-zinc.json +2 -1
- package/themes/arnative.json +2 -1
- package/themes/gen-themes.mjs +30 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.2] - 2026-10-13
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Restored the footer's two-tone labels. `0.5.1` moved the label colour onto `softOf(accent)`, which softens the *accent* hue, so cwd/branch/clock/status text inherited the accent (pink) instead of the border-accent (blue) tier and the footer collapsed into a single hue (`extensions/footer.ts`).
|
|
7
|
+
- Added `vars.secondarySoft` to all 11 themes, softened from the same value the `borderAccent` token resolves to (L 0.82, chroma × 0.18, hue kept), and `softSecondaryOf` in `lib/ansi.ts` to derive the identical value at runtime — `vars` names are not addressable via `fg()`, and Pi 1.2 rejects custom `colors` keys, so the declared and derived values must be produced by one recipe.
|
|
8
|
+
- Routed the header's model/thinking/token text, tool bodies, list rows and the usage dashboard onto the label tier. Icons, marks and the logo's second tone stay on the accent tier, so each line keeps its two tones (`extensions/section-headers.ts`, `extensions/tools.ts`, `extensions/usage.ts`, `extensions/arnative.ts`, `extensions/ui-render-tweaks.ts`).
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `soften()` is now shared by the generator and the runtime, and `extensions/ui-render-tweaks.ts` asserts every declared `*Soft` var equals its derived value — a drifting or rogue soft colour fails `npm test` instead of shipping (`themes/gen-themes.mjs`, `lib/ansi.ts`).
|
|
12
|
+
- Behavioral test pinning the two-tone rule: labels take the border-accent hue, icons keep the accent hue (`test/footer-minimal.test.ts`).
|
|
13
|
+
|
|
14
|
+
### Notes
|
|
15
|
+
- Label colour is deliberately softer than the pre-0.5.1 `#05dee7`. `SOFT_CHROMA_SCALE` in `lib/ansi.ts` is the single knob: `0.18` gives `#b2c9ca` on cyberpunk, `0.30` gives `#b2dadc`, `0.50` gives `#9adfe3`. Changing it regenerates all 11 themes and the guard forces them to stay in sync.
|
|
16
|
+
|
|
3
17
|
## [0.5.1] - 2026-10-13
|
|
4
18
|
|
|
5
19
|
### Fixed
|
package/extensions/arnative.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { matchesKey, truncateToWidth, visibleWidth, type AutocompleteItem, type Component } from "@earendil-works/pi-tui";
|
|
10
|
-
import {
|
|
10
|
+
import { softSecondaryOf, stripAnsi, thinkingColorOf } from "../lib/ansi.ts";
|
|
11
11
|
import { renderScreen, screenTitle, type ScreenText } from "../lib/box.ts";
|
|
12
12
|
import { assert, isMain } from "../lib/check.ts";
|
|
13
13
|
import { markedLabels } from "../lib/format.ts";
|
|
@@ -172,7 +172,7 @@ export class ListPicker implements Component {
|
|
|
172
172
|
render(width: number): string[] {
|
|
173
173
|
const th = this.theme;
|
|
174
174
|
const bold = th.bold ?? ((t: string) => `\x1b[1m${t}\x1b[22m`);
|
|
175
|
-
const soft =
|
|
175
|
+
const soft = softSecondaryOf(th);
|
|
176
176
|
const edge = (text: string) => this.edge(text);
|
|
177
177
|
const column = Math.max(0, ...this.rows.map((r) => visibleWidth(r.label)));
|
|
178
178
|
const body: string[] = [];
|
package/extensions/footer.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { execFile } from "node:child_process";
|
|
|
11
11
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { dirname, join, resolve } from "node:path";
|
|
14
|
-
import { softOf, stripAnsi } from "../lib/ansi.ts";
|
|
14
|
+
import { softOf, softSecondaryOf, stripAnsi } from "../lib/ansi.ts";
|
|
15
15
|
import { assert, isMain } from "../lib/check.ts";
|
|
16
16
|
import { capitalize, markedLabels, modelDisplayParts } from "../lib/format.ts";
|
|
17
17
|
import { IS_WINDOWS_LIKE, SHORTCUT_RELOAD, SHORTCUT_TOGGLE_FOOTER } from "../lib/shortcuts.ts";
|
|
@@ -889,9 +889,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
889
889
|
const acc = (text: string) => theme.fg("accent", text);
|
|
890
890
|
const dim = (text: string) => theme.fg("dim", text);
|
|
891
891
|
const fgAny = theme.fg.bind(theme) as (color: string, text: string) => string;
|
|
892
|
-
// `accentSoft`
|
|
893
|
-
// through the shared
|
|
892
|
+
// `accentSoft`/`secondarySoft` are vars-only tokens on Pi 1.2+, so both soft tiers
|
|
893
|
+
// are resolved through the shared painters instead of a raw fg() probe. Icons keep
|
|
894
|
+
// `accent`; the cwd/branch/clock/status text takes the border-accent tier, so the
|
|
895
|
+
// footer stays two-toned instead of the text inheriting the accent hue.
|
|
894
896
|
const soft = softOf(theme as never);
|
|
897
|
+
const softLabel = softSecondaryOf(theme as never);
|
|
895
898
|
// Diff colors: the same theme slots pi uses for a diff body, with the
|
|
896
899
|
// plain soft color as fallback for a theme that omits them.
|
|
897
900
|
const colorOr = (name: string, fallback: (t: string) => string) => (t: string) => {
|
|
@@ -901,12 +904,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
901
904
|
return fallback(t);
|
|
902
905
|
}
|
|
903
906
|
};
|
|
904
|
-
// `primary
|
|
905
|
-
//
|
|
907
|
+
// `primary` is a var, not a token (Pi 1.2+ rejects custom `colors` keys); its
|
|
908
|
+
// arnative carrier is the official token it maps to.
|
|
906
909
|
const primary = colorOr("mdHeading", acc);
|
|
907
|
-
const
|
|
908
|
-
const secondary = colorOr("borderAccent", soft);
|
|
909
|
-
const secondarySoft = soft;
|
|
910
|
+
const secondarySoft = softLabel;
|
|
910
911
|
const diffAdd = colorOr("toolDiffAdded", soft);
|
|
911
912
|
const diffDel = colorOr("toolDiffRemoved", soft);
|
|
912
913
|
const sep = dim(" | ");
|
|
@@ -1044,7 +1045,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1044
1045
|
try {
|
|
1045
1046
|
const th = this.getActiveTheme();
|
|
1046
1047
|
if (!th) return text;
|
|
1047
|
-
return
|
|
1048
|
+
return softSecondaryOf(th as never)(text);
|
|
1048
1049
|
} catch {
|
|
1049
1050
|
return text;
|
|
1050
1051
|
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from "@earendil-works/pi-tui";
|
|
34
34
|
import type { BoxTheme } from "../lib/box.ts";
|
|
35
35
|
import { BRAND, renderBoxLines } from "../lib/box.ts";
|
|
36
|
-
import { getActiveTheme, setActiveTheme, softOf, stripAnsi, themeOf } from "../lib/ansi.ts";
|
|
36
|
+
import { getActiveTheme, setActiveTheme, softOf, softSecondaryOf, stripAnsi, themeOf } from "../lib/ansi.ts";
|
|
37
37
|
import { capitalize, expandKeyName, modelDisplayParts } from "../lib/format.ts";
|
|
38
38
|
import { SHORTCUT_NEW_SESSION, SHORTCUT_NEXT_TAB, SHORTCUT_RELOAD } from "../lib/shortcuts.ts";
|
|
39
39
|
import { HEADER_PRESET_KEY as HEADER_PRESET_CONFIG_KEY, loadChoice, saveChoice } from "../lib/preset-store.ts";
|
|
@@ -79,16 +79,31 @@ export function fgFirst(th: Themeish, names: string[], text: string): string {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Painter for the header's soft tier
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* Painter for the header's soft *accent* tier — the logo mark's second tone, paired with
|
|
83
|
+
* the accent `P`. Keeps the mark in one hue family instead of blue.
|
|
84
|
+
*
|
|
85
|
+
* Pi 1.2.0 rejects a custom `accentSoft` key in `colors`, so `fgFirst(["accentSoft", …])`
|
|
86
|
+
* silently degrades to plain text there and the two-tone look flattens. `softOf` derives
|
|
87
|
+
* the same tint from the resolved accent; a theme that genuinely defines `accentSoft`
|
|
88
|
+
* still wins first.
|
|
86
89
|
*/
|
|
87
90
|
export function softFirst(th: Themeish, fallbacks: string[], text: string): string {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
return softOnTier(softOf(th as never), th, fallbacks, text);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Painter for the soft *label* tier of header text (model name, thinking level, token
|
|
96
|
+
* counts). Same colour language as the footer: marks take `accent`, their text takes the
|
|
97
|
+
* soft border-accent tier, so a header line and the footer under it agree.
|
|
98
|
+
*/
|
|
99
|
+
export function softLabelFirst(th: Themeish, fallbacks: string[], text: string): string {
|
|
100
|
+
return softOnTier(softSecondaryOf(th as never), th, fallbacks, text);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Shared tail of the two soft painters: use the resolved soft tone, else the fallback tokens. */
|
|
104
|
+
function softOnTier(paint: (s: string) => string, th: Themeish, fallbacks: string[], text: string): string {
|
|
105
|
+
const out = paint(text);
|
|
106
|
+
return out !== text ? out : fgFirst(th, fallbacks, text);
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
/**
|
|
@@ -542,7 +557,7 @@ export class ArnativeHeader implements Component {
|
|
|
542
557
|
const isActive = t.key === this.activeTab;
|
|
543
558
|
const iconStyled = isActive ? fgFirst(th, ["accent"], t.icon) : fgFirst(th, ["dim"], t.icon);
|
|
544
559
|
const labelStyled = isActive
|
|
545
|
-
?
|
|
560
|
+
? softLabelFirst(th, ["accentSoft", "text"], `\x1b[1m${label}\x1b[22m`)
|
|
546
561
|
: fgFirst(th, ["dim"], label);
|
|
547
562
|
return { key: t.key, plain, formatted: `${iconStyled} ${labelStyled}` };
|
|
548
563
|
};
|
|
@@ -606,19 +621,19 @@ export class ArnativeHeader implements Component {
|
|
|
606
621
|
// Model name in soft, its "(Provider)" tail dim.
|
|
607
622
|
const provSuffix = fullName.match(/(\s*\([^()]*\))$/)?.[1] ?? "";
|
|
608
623
|
const nameMain = provSuffix ? fullName.slice(0, -provSuffix.length) : fullName;
|
|
609
|
-
let line = `${
|
|
624
|
+
let line = `${softLabelFirst(th, ["accentSoft", "text"], nameMain)}${provSuffix ? fgFirst(th, ["dim"], provSuffix) : ""}${sep}${softLabelFirst(th, ["accentSoft", "text"], `Thinking: ${thinkLevel}`)}`;
|
|
610
625
|
|
|
611
626
|
// All-time usage of that model (same as /usage): ↑... ↓... ...
|
|
612
627
|
const usage = getModelAllTimeUsage(modelObj?.id);
|
|
613
628
|
const tokenParts: string[] = [];
|
|
614
629
|
if (usage.input > 0) {
|
|
615
|
-
tokenParts.push(`${fgFirst(th, ["accent"], "↑")}${
|
|
630
|
+
tokenParts.push(`${fgFirst(th, ["accent"], "↑")}${softLabelFirst(th, ["accentSoft", "text"], formatTokens(usage.input))}`);
|
|
616
631
|
}
|
|
617
632
|
if (usage.output > 0) {
|
|
618
|
-
tokenParts.push(`${fgFirst(th, ["accent"], "↓")}${
|
|
633
|
+
tokenParts.push(`${fgFirst(th, ["accent"], "↓")}${softLabelFirst(th, ["accentSoft", "text"], formatTokens(usage.output))}`);
|
|
619
634
|
}
|
|
620
635
|
if (usage.cacheRead > 0) {
|
|
621
|
-
tokenParts.push(`${fgFirst(th, ["accent"], "\uf49b ")}${
|
|
636
|
+
tokenParts.push(`${fgFirst(th, ["accent"], "\uf49b ")}${softLabelFirst(th, ["accentSoft", "text"], formatTokens(usage.cacheRead))}`);
|
|
622
637
|
}
|
|
623
638
|
if (tokenParts.length > 0) {
|
|
624
639
|
line += `${sep}${tokenParts.join(" ")}`;
|
|
@@ -631,11 +646,11 @@ export class ArnativeHeader implements Component {
|
|
|
631
646
|
|
|
632
647
|
if (this.activeTab === "Directory") {
|
|
633
648
|
for (const line of wrapPath(getCwd(this.ctx), infoW)) {
|
|
634
|
-
infoLines.push(
|
|
649
|
+
infoLines.push(softLabelFirst(th, ["accentSoft", "text"], line));
|
|
635
650
|
}
|
|
636
651
|
} else if (this.activeTab === "Shortcut") {
|
|
637
652
|
for (const line of wrapTokens(SHORTCUTS, infoW)) {
|
|
638
|
-
infoLines.push(
|
|
653
|
+
infoLines.push(softLabelFirst(th, ["accentSoft", "text"], line));
|
|
639
654
|
}
|
|
640
655
|
} else if (activeItems.length === 0) {
|
|
641
656
|
infoLines.push(
|
|
@@ -647,7 +662,7 @@ export class ArnativeHeader implements Component {
|
|
|
647
662
|
} else {
|
|
648
663
|
for (const line of wrapCommaItems(activeItems, infoW)) {
|
|
649
664
|
// Point 5: selected menu data uses soft only
|
|
650
|
-
infoLines.push(
|
|
665
|
+
infoLines.push(softLabelFirst(th, ["accentSoft", "text"], line));
|
|
651
666
|
}
|
|
652
667
|
}
|
|
653
668
|
|
package/extensions/tools.ts
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
40
40
|
import { homedir } from "node:os";
|
|
41
41
|
import { renderBoxLines as box } from "../lib/box.ts";
|
|
42
|
-
import { stripAnsi,
|
|
42
|
+
import { stripAnsi, softSecondaryOf } from "../lib/ansi.ts";
|
|
43
43
|
import { assert, isMain } from "../lib/check.ts";
|
|
44
44
|
import { expandKeyName } from "../lib/format.ts";
|
|
45
45
|
|
|
@@ -443,7 +443,7 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
443
443
|
? safeCall(() => own.call(this, { content: result.content, details: result.details }, opts, th, ctx))
|
|
444
444
|
: null;
|
|
445
445
|
return new Lines((width) => {
|
|
446
|
-
const soft =
|
|
446
|
+
const soft = softSecondaryOf(th);
|
|
447
447
|
const inner = Math.max(8, width - 4);
|
|
448
448
|
const icon = isErr ? th.fg("error", "x") : th.fg("success", "✓");
|
|
449
449
|
const boxed2 = componentLines(theirs, width - 6);
|
|
@@ -606,7 +606,7 @@ function minimal(
|
|
|
606
606
|
return new Lines((width) => {
|
|
607
607
|
// Read context.state live (never capture): renderResult may create it after renderCall.
|
|
608
608
|
if (context.state?.hasResult) return [];
|
|
609
|
-
const soft =
|
|
609
|
+
const soft = softSecondaryOf(theme);
|
|
610
610
|
const inner = Math.max(8, width - 4);
|
|
611
611
|
const rows = titleRow(
|
|
612
612
|
theme,
|
|
@@ -635,7 +635,7 @@ function minimal(
|
|
|
635
635
|
const dur = fmtMs(context.toolCallId ? TIMINGS.get(context.toolCallId) : undefined);
|
|
636
636
|
const icon = isErr ? theme.fg("error", "x") : theme.fg("success", "✓");
|
|
637
637
|
return new Lines((width) => {
|
|
638
|
-
const soft =
|
|
638
|
+
const soft = softSecondaryOf(theme);
|
|
639
639
|
const inner = Math.max(8, width - 4);
|
|
640
640
|
const rows = titleRow(
|
|
641
641
|
theme,
|
|
@@ -39,9 +39,9 @@ import {
|
|
|
39
39
|
} from "@earendil-works/pi-tui";
|
|
40
40
|
import { readdirSync, readFileSync } from "node:fs";
|
|
41
41
|
import { renderBoxLines } from "../lib/box.ts";
|
|
42
|
-
import { ansiBgOpen, getActiveTheme, setActiveTheme, stripAnsi, themeOf,
|
|
42
|
+
import { ansiBgOpen, getActiveTheme, setActiveTheme, stripAnsi, themeOf, softSecondaryOf } from "../lib/ansi.ts";
|
|
43
43
|
import { assert, isMain } from "../lib/check.ts";
|
|
44
|
-
import { contrast, parseOklch, toHex } from "../lib/color.ts";
|
|
44
|
+
import { contrast, oklchToRgb, parseOklch, toHex } from "../lib/color.ts";
|
|
45
45
|
import { expandKeyName } from "../lib/format.ts";
|
|
46
46
|
|
|
47
47
|
type ActiveTheme = { fg(color: string, text: string): string; bg?(color: string, text: string): string } | null;
|
|
@@ -174,7 +174,7 @@ const SELECT_ROW_KEY = Symbol.for("pi-arnative.selectRowColors");
|
|
|
174
174
|
|
|
175
175
|
/** Selected row body: `·` separators accent, the text around them soft. */
|
|
176
176
|
export function selectRowText(row: string, th: { fg(c: string, t: string): string }): string {
|
|
177
|
-
const soft =
|
|
177
|
+
const soft = softSecondaryOf(th);
|
|
178
178
|
return row
|
|
179
179
|
.split("·")
|
|
180
180
|
.map((part) => soft(part))
|
|
@@ -396,6 +396,14 @@ if (isMain(import.meta.url)) {
|
|
|
396
396
|
// hex, so resolve every token through lib/color before constructing it.
|
|
397
397
|
const resolve = (value: string) => toHex(themeJson.vars[value] ?? value);
|
|
398
398
|
const hex = (tok: string) => resolve(themeJson.colors[tok] ?? tok);
|
|
399
|
+
// Mirrors `soften()` (themes/gen-themes.mjs) and `softenColor` (lib/ansi.ts): the soft
|
|
400
|
+
// tier is the base color at L 0.82 with a fifth of its chroma. Both sides read the
|
|
401
|
+
// declared OKLCH string (not the 8-bit hex), so the comparison is not rounded twice.
|
|
402
|
+
const softenToHex = (base: string) => {
|
|
403
|
+
const o = parseOklch(base);
|
|
404
|
+
assert(o !== null, `cannot soften a non-oklch color: ${base}`);
|
|
405
|
+
return toHex(`oklch(82% ${oklchToRgb(0.82, o.C * 0.18, o.H).chroma} ${o.H})`);
|
|
406
|
+
};
|
|
399
407
|
// The built-in theme adds fallbacks (scrollbarTrack<-muted, thinkingMax<-thinkingXhigh),
|
|
400
408
|
// so the fg map must be complete; take the whole palette like pi's loader does.
|
|
401
409
|
const fgPalette = Object.fromEntries(
|
|
@@ -508,8 +516,18 @@ if (isMain(import.meta.url)) {
|
|
|
508
516
|
// Accent pill: accent bg + toolPendingBg colored text.
|
|
509
517
|
assert(contrast(hex("accent"), hex("toolPendingBg")) >= 4.5, `theme ${f}: accent vs toolPendingBg contrast >= 4.5`);
|
|
510
518
|
assert(isColor(d.vars.accentSoft ?? ""), `theme ${f}: vars.accentSoft is a color`);
|
|
519
|
+
assert(isColor(d.vars.secondarySoft ?? ""), `theme ${f}: vars.secondarySoft is a color`);
|
|
511
520
|
assert(isColor(d.vars.primary ?? ""), `theme ${f}: vars.primary is a color`);
|
|
512
521
|
assert(isColor(d.vars.secondary ?? ""), `theme ${f}: vars.secondary is a color`);
|
|
522
|
+
// The declared soft vars must equal what the extensions derive at runtime, or the
|
|
523
|
+
// footer label colour drifts from the theme file. `softenToHex` mirrors
|
|
524
|
+
// `soften()` in gen-themes.mjs and `softenColor` in lib/ansi.ts.
|
|
525
|
+
for (const [softName, baseName] of [["accentSoft", "accent"], ["secondarySoft", "borderAccent"]] as const) {
|
|
526
|
+
const base = d.vars[d.colors[baseName] ?? baseName]!;
|
|
527
|
+
const declared = toHex(d.vars[softName]!);
|
|
528
|
+
const derived = softenToHex(base);
|
|
529
|
+
assert(declared === derived, `theme ${f}: vars.${softName} ${declared} != runtime ${derived} (derived from ${baseName})`);
|
|
530
|
+
}
|
|
513
531
|
const canvas = hex("userMessageBg");
|
|
514
532
|
for (const tok of Object.keys(d.colors)) {
|
|
515
533
|
if (nonText.has(tok) || tok === "thinkingMax") continue;
|
package/extensions/usage.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { matchesKey, truncateToWidth, visibleWidth, type Component, type TUI, ty
|
|
|
11
11
|
import { renderScreen, screenTitle } from "../lib/box.ts";
|
|
12
12
|
import { assert, isMain } from "../lib/check.ts";
|
|
13
13
|
import { capitalize } from "../lib/format.ts";
|
|
14
|
-
import { thinkingColorOf,
|
|
14
|
+
import { thinkingColorOf, softSecondaryOf } from "../lib/ansi.ts";
|
|
15
15
|
import { legendUsage, matchesBack } from "../lib/shortcuts.ts";
|
|
16
16
|
import { collectUsageSummary, estimateCost, formatTokens, getModelAllTimeUsage, hasPrice, loadPriceTable, type UsageSummary } from "../lib/usage-store.ts";
|
|
17
17
|
|
|
@@ -196,8 +196,9 @@ export class UsageModalComponent implements Component {
|
|
|
196
196
|
// dashboard reads as belonging to the model that produced the usage.
|
|
197
197
|
const edge = (t: string) => this.edgeOf(t);
|
|
198
198
|
const boxW = width;
|
|
199
|
-
// `
|
|
200
|
-
|
|
199
|
+
// `secondarySoft` is a vars-only token on Pi 1.2+; softSecondaryOf resolves the soft
|
|
200
|
+
// label tier (the border-accent hue) and falls back to the accent tier.
|
|
201
|
+
const soft = softSecondaryOf(th);
|
|
201
202
|
const acc = (s: string) => th.fg("accent", s);
|
|
202
203
|
const dim = (s: string) => th.fg("dim", s);
|
|
203
204
|
const plain = (s: string) => th.fg("text", s);
|
package/lib/ansi.ts
CHANGED
|
@@ -38,6 +38,18 @@ export function setActiveTheme<T>(th: T | null): void {
|
|
|
38
38
|
else delete (globalThis as Record<symbol, T>)[THEME_PROXY_KEY];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Soft tier of a resolved theme color: clone at L 0.82 with a fifth of its chroma and
|
|
43
|
+
* the hue kept. The generator's `soften()` (themes/gen-themes.mjs) applies the identical
|
|
44
|
+
* recipe, so a declared `vars.*Soft` and this derived value agree. `chroma * 0.18` is
|
|
45
|
+
* the single tuning constant for how far a soft tone leans toward grey.
|
|
46
|
+
*/
|
|
47
|
+
const SOFT_L = 0.82;
|
|
48
|
+
const SOFT_CHROMA_SCALE = 0.18;
|
|
49
|
+
function softenColor(c: { c?: number; h?: number }): { kind: "oklch"; l: number; c: number; h: number } {
|
|
50
|
+
return { kind: "oklch", l: SOFT_L, c: (c.c ?? 0) * SOFT_CHROMA_SCALE, h: c.h ?? 0 };
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
/**
|
|
42
54
|
* Painter for arnative's soft accent (`accentSoft`).
|
|
43
55
|
*
|
|
@@ -45,9 +57,9 @@ export function setActiveTheme<T>(th: T | null): void {
|
|
|
45
57
|
* addressable directly. 1.2.0 validates `colors` with `additionalProperties: false`
|
|
46
58
|
* and rejects the whole theme if it carries one, so the token now lives only in
|
|
47
59
|
* `vars` and `fg("accentSoft")` throws. The soft accent stays available through the
|
|
48
|
-
* resolved `accent` color (1.2.0
|
|
49
|
-
* softened to the generator's `*Soft` recipe, which keeps the two-tone look
|
|
50
|
-
* of collapsing to grey. Falls back to `muted`, then `accent`, on
|
|
60
|
+
* resolved `accent` color (both 1.1.0 and 1.2.0 expose `theme.colors`): its hue and
|
|
61
|
+
* chroma are softened to the generator's `*Soft` recipe, which keeps the two-tone look
|
|
62
|
+
* instead of collapsing to grey. Falls back to `muted`, then `accent`, on older themes.
|
|
51
63
|
*/
|
|
52
64
|
export function softOf(th: {
|
|
53
65
|
fg?(c: string, t: string): string;
|
|
@@ -65,13 +77,36 @@ export function softOf(th: {
|
|
|
65
77
|
const accent = th?.colors?.accent;
|
|
66
78
|
const style = th?.style?.bind(th);
|
|
67
79
|
if (accent?.kind === "oklch" && style) {
|
|
68
|
-
|
|
69
|
-
const soft = { kind: "oklch" as const, l: 0.82, c: (accent.c ?? 0) * 0.18, h: accent.h ?? 0 };
|
|
80
|
+
const soft = softenColor(accent);
|
|
70
81
|
return (s) => style(s, { fg: soft });
|
|
71
82
|
}
|
|
72
83
|
return (s) => fg("muted", s);
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Painter for the soft *label* tier (`secondarySoft`): the footer's cwd, branch, clock
|
|
88
|
+
* and status text.
|
|
89
|
+
*
|
|
90
|
+
* The two-tone footer puts icons on `accent` and their text on the border-accent tier.
|
|
91
|
+
* `secondarySoft` is declared only in `vars`, so it cannot be read back with `fg()`;
|
|
92
|
+
* it is derived instead from `borderAccent`, the official token that tier maps to. The
|
|
93
|
+
* generator softens that same value, so declared and derived agree. Falls back to
|
|
94
|
+
* `softOf` (the accent tier) when the theme exposes no resolvable `borderAccent`.
|
|
95
|
+
*/
|
|
96
|
+
export function softSecondaryOf(th: {
|
|
97
|
+
fg?(c: string, t: string): string;
|
|
98
|
+
style?(t: string, o: { fg: unknown }): string;
|
|
99
|
+
colors?: Record<string, { kind?: string; c?: number; h?: number }>;
|
|
100
|
+
} | null): (s: string) => string {
|
|
101
|
+
const border = th?.colors?.borderAccent;
|
|
102
|
+
const style = th?.style?.bind(th);
|
|
103
|
+
if (border?.kind === "oklch" && style) {
|
|
104
|
+
const soft = softenColor(border);
|
|
105
|
+
return (s) => style(s, { fg: soft });
|
|
106
|
+
}
|
|
107
|
+
return softOf(th);
|
|
108
|
+
}
|
|
109
|
+
|
|
75
110
|
/** pi defines one theme slot per thinking level (`thinkingOff`…`thinkingMax`), so a box
|
|
76
111
|
* can follow the model instead of a hardcoded palette — switching theme keeps the
|
|
77
112
|
* meaning of the color. Shared by `usage.ts` and `arnative.ts`. */
|
|
@@ -129,17 +164,26 @@ if (isMain(import.meta.url)) {
|
|
|
129
164
|
};
|
|
130
165
|
assert(softOf(noSoft)("x") === "<muted>x</muted>", "softOf falls back to muted without the token or colors");
|
|
131
166
|
assert(softOf(null)("x") === "x", "softOf without a theme is identity");
|
|
132
|
-
// Pi 1.2.0:
|
|
167
|
+
// Pi 1.2.0: accentSoft is rejected, so the soft tint is derived from `colors.accent`.
|
|
133
168
|
const modern = {
|
|
134
169
|
fg: (c: string, t: string) => {
|
|
135
170
|
throw new Error(`Unknown theme color: ${c}`);
|
|
136
171
|
},
|
|
137
172
|
style: (t: string, o: { fg: unknown }) => `<soft:${JSON.stringify(o.fg)}>${t}</soft>`,
|
|
138
|
-
colors: { accent: { kind: "oklch", c: 0.2, h: 355 } },
|
|
173
|
+
colors: { accent: { kind: "oklch", c: 0.2, h: 355 }, borderAccent: { kind: "oklch", c: 0.1, h: 200 } },
|
|
139
174
|
};
|
|
140
175
|
const modernOut = softOf(modern)("x");
|
|
141
176
|
assert(modernOut.includes('"l":0.82') && modernOut.includes('"c":0.036'), "softOf derives the soft tint from accent (l=0.82, c=accent*0.18)");
|
|
142
177
|
assert(modernOut.includes("x"), "softOf keeps the text");
|
|
178
|
+
// The label tier softens `borderAccent`, NOT `accent`: that is what keeps footer text
|
|
179
|
+
// on the blue tier instead of inheriting the pink accent hue.
|
|
180
|
+
const labelOut = softSecondaryOf(modern)("x");
|
|
181
|
+
assert(
|
|
182
|
+
labelOut.includes('"l":0.82') && labelOut.includes('"c":0.018') && labelOut.includes('"h":200'),
|
|
183
|
+
"softSecondaryOf softens borderAccent (l=0.82, c=borderAccent*0.18, same hue)",
|
|
184
|
+
);
|
|
185
|
+
assert(labelOut !== modernOut, "the soft label tier differs from the soft accent tier");
|
|
186
|
+
assert(softSecondaryOf({ fg: () => "", style: (t) => `<x>${t}</x>`, colors: {} })("x") === softOf({ fg: () => "", style: (t) => `<x>${t}</x>`, colors: {} })("x"), "softSecondaryOf falls back to softOf without a borderAccent");
|
|
143
187
|
|
|
144
188
|
assert(thinkingColorSlot("off") === "thinkingOff", "thinking off maps to thinkingOff");
|
|
145
189
|
assert(thinkingColorSlot(undefined) === "thinkingOff", "missing level falls back to thinkingOff");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bismawy/pi-arnative",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Refined aesthetics. Cohesive tools. Built for Pi. Warm themes, rounded tool boxes, tabbed header, compact footer and a /usage dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.02 275)",
|
|
10
10
|
"primary": "oklch(88% 0.18 95)",
|
|
11
11
|
"secondary": "oklch(82% 0.139 200)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.02502 200)",
|
|
12
13
|
"accent": "oklch(78% 0.146 355)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.02628 355)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.114 355)",
|
|
15
16
|
"success": "oklch(78% 0.15 145)",
|
|
16
17
|
"error": "oklch(76% 0.143 25)",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.012 285)",
|
|
10
10
|
"primary": "oklch(78% 0.125 295)",
|
|
11
11
|
"secondary": "oklch(82% 0.13 190)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.0234 190)",
|
|
12
13
|
"accent": "oklch(78% 0.14 330)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.0252 330)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.084 330)",
|
|
15
16
|
"success": "oklch(78% 0.14 145)",
|
|
16
17
|
"error": "oklch(76% 0.14 25)",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.012 165)",
|
|
10
10
|
"primary": "oklch(84% 0.13 165)",
|
|
11
11
|
"secondary": "oklch(78% 0.1 195)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.01296 165)",
|
|
12
13
|
"accent": "oklch(78% 0.12 165)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.0216 165)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.072 165)",
|
|
15
16
|
"success": "oklch(78% 0.12 145)",
|
|
16
17
|
"error": "oklch(76% 0.12 25)",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.015 70)",
|
|
10
10
|
"primary": "oklch(84% 0.14 83)",
|
|
11
11
|
"secondary": "oklch(78% 0.1 175)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.018 175)",
|
|
12
13
|
"accent": "oklch(78% 0.137 45)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.02466 45)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.084 45)",
|
|
15
16
|
"success": "oklch(78% 0.14 145)",
|
|
16
17
|
"error": "oklch(76% 0.14 25)",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.02 145)",
|
|
10
10
|
"primary": "oklch(88% 0.24 145)",
|
|
11
11
|
"secondary": "oklch(76% 0.16 165)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.02376 144)",
|
|
12
13
|
"accent": "oklch(78% 0.22 144)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.0396 144)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.132 144)",
|
|
15
16
|
"success": "oklch(78% 0.15 145)",
|
|
16
17
|
"error": "oklch(76% 0.143 25)",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.01 240)",
|
|
10
10
|
"primary": "oklch(78% 0.08 217)",
|
|
11
11
|
"secondary": "oklch(82% 0.09 195)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.0162 195)",
|
|
12
13
|
"accent": "oklch(78% 0.07 310)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.0126 310)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.042 310)",
|
|
15
16
|
"success": "oklch(78% 0.07 145)",
|
|
16
17
|
"error": "oklch(76% 0.07 25)",
|
package/themes/arnative-sun.json
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.012 70)",
|
|
10
10
|
"primary": "oklch(84% 0.126 70)",
|
|
11
11
|
"secondary": "oklch(78% 0.12 35)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.01512 70)",
|
|
12
13
|
"accent": "oklch(78% 0.14 70)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.0252 70)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.084 70)",
|
|
15
16
|
"success": "oklch(78% 0.14 145)",
|
|
16
17
|
"error": "oklch(76% 0.14 25)",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.015 300)",
|
|
10
10
|
"primary": "oklch(78% 0.178 339)",
|
|
11
11
|
"secondary": "oklch(82% 0.145 185)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.0261 185)",
|
|
12
13
|
"accent": "oklch(78% 0.142 50)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.02556 50)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.102 50)",
|
|
15
16
|
"success": "oklch(78% 0.15 145)",
|
|
16
17
|
"error": "oklch(76% 0.143 25)",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.012 295)",
|
|
10
10
|
"primary": "oklch(82% 0.1 294)",
|
|
11
11
|
"secondary": "oklch(78% 0.11 215)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.01512 294)",
|
|
12
13
|
"accent": "oklch(78% 0.124 294)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.02232 294)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.084 294)",
|
|
15
16
|
"success": "oklch(78% 0.14 145)",
|
|
16
17
|
"error": "oklch(76% 0.14 25)",
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.005 285)",
|
|
10
10
|
"primary": "oklch(86% 0.015 285)",
|
|
11
11
|
"secondary": "oklch(72% 0.025 240)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.00216 286)",
|
|
12
13
|
"accent": "oklch(78% 0.02 286)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.0036 286)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.012 286)",
|
|
15
16
|
"success": "oklch(78% 0.02 145)",
|
|
16
17
|
"error": "oklch(76% 0.02 25)",
|
package/themes/arnative.json
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
"fgFaint": "oklch(49% 0.012 232)",
|
|
10
10
|
"primary": "oklch(82% 0.138 218)",
|
|
11
11
|
"secondary": "oklch(78% 0.11 282)",
|
|
12
|
+
"secondarySoft": "oklch(82% 0.01404 218)",
|
|
12
13
|
"accent": "oklch(78% 0.13 218)",
|
|
13
|
-
"accentSoft": "oklch(82% 0.
|
|
14
|
+
"accentSoft": "oklch(82% 0.0234 218)",
|
|
14
15
|
"accentBorder": "oklch(82% 0.078 218)",
|
|
15
16
|
"success": "oklch(78% 0.13 145)",
|
|
16
17
|
"error": "oklch(76% 0.13 25)",
|
package/themes/gen-themes.mjs
CHANGED
|
@@ -32,6 +32,23 @@ function oklch(L, C, H) {
|
|
|
32
32
|
return `oklch(${+(L * 100).toFixed(1)}% ${round3(chroma)} ${Math.round(hue(H))})`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Soft tier of a color: clone at L 0.82 with a fifth of the GAMUT-CLAMPED chroma, hue
|
|
37
|
+
* kept. `lib/ansi.ts` softens the resolved token with this same recipe at runtime, so a
|
|
38
|
+
* `*Soft` var declared here and the extension's derived value agree. Grey-collapse is
|
|
39
|
+
* avoided because the hue is carried, not dropped.
|
|
40
|
+
*
|
|
41
|
+
* The chroma keeps five decimals (not the usual three): the soft tier multiplies it by
|
|
42
|
+
* 0.18, so a coarser round-trip shifts the last hex digit and the `declare == derive`
|
|
43
|
+
* guard in ui-render-tweaks.ts fails.
|
|
44
|
+
*/
|
|
45
|
+
function soften(value) {
|
|
46
|
+
const o = parseOklch(value);
|
|
47
|
+
assert(o !== null, `cannot soften a non-oklch color: ${value}`);
|
|
48
|
+
const { chroma } = oklchToRgb(0.82, o.C * 0.18, o.H);
|
|
49
|
+
return `oklch(82% ${+chroma.toFixed(5)} ${Math.round(hue(o.H))})`;
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
// --- shared ramps ----------------------------------------------------------
|
|
36
53
|
|
|
37
54
|
const NEUTRAL = { fg: 0.88, fgMuted: 0.72, fgDim: 0.61, fgFaint: 0.49 };
|
|
@@ -252,6 +269,15 @@ function derive(spec) {
|
|
|
252
269
|
const primaryVal = primarySeed ? oklch(primarySeed.L, primarySeed.C, primarySeed.H) : oklch(0.82, c, h);
|
|
253
270
|
const secondaryVal = secondarySeed ? oklch(secondarySeed.L, secondarySeed.C, secondarySeed.H) : oklch(0.78, Math.min(c * 0.8, 0.12), roleHue("label"));
|
|
254
271
|
|
|
272
|
+
// Soft counterpart of the border-accent tier — the footer's label, clock and status
|
|
273
|
+
// colour. Softened from the SAME value the `borderAccent` token resolves to, because
|
|
274
|
+
// that token is what extensions read back at runtime (`softSecondaryOf` in
|
|
275
|
+
// lib/ansi.ts). One source keeps the declared value and the runtime one identical.
|
|
276
|
+
const accentBorderVal = oklch(0.82, c * 0.6, h);
|
|
277
|
+
const accentVal = oklch(0.78, c, h);
|
|
278
|
+
const borderAccentRole = spec.borderAccent ?? "accentBorder";
|
|
279
|
+
const borderAccentVal = { accentBorder: accentBorderVal, secondary: secondaryVal }[borderAccentRole] ?? borderAccentRole;
|
|
280
|
+
|
|
255
281
|
const vars = {
|
|
256
282
|
fg: oklch(NEUTRAL.fg, nc, nh),
|
|
257
283
|
fgMuted: oklch(NEUTRAL.fgMuted, nc, nh),
|
|
@@ -259,14 +285,10 @@ function derive(spec) {
|
|
|
259
285
|
fgFaint: oklch(NEUTRAL.fgFaint, nc, nh),
|
|
260
286
|
primary: primaryVal,
|
|
261
287
|
secondary: secondaryVal,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// same clamped chroma makes the declared value and the runtime one agree; using `c`
|
|
267
|
-
// drifts wherever the raw chroma is out of gamut at the accent's lightness.
|
|
268
|
-
accentSoft: oklch(0.82, oklchToRgb(0.78, c, h).chroma * 0.18, h),
|
|
269
|
-
accentBorder: oklch(0.82, c * 0.6, h),
|
|
288
|
+
secondarySoft: soften(borderAccentVal),
|
|
289
|
+
accent: accentVal,
|
|
290
|
+
accentSoft: soften(accentVal),
|
|
291
|
+
accentBorder: accentBorderVal,
|
|
270
292
|
success: oklch(0.78, Math.min(c, 0.15), sem("success")),
|
|
271
293
|
error: oklch(0.76, Math.min(c, 0.15), sem("error")),
|
|
272
294
|
warning: oklch(0.86, Math.min(c, 0.14), sem("warning")),
|