@bismawy/pi-arnative 0.5.1 → 0.5.3
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 +31 -0
- package/README.md +1 -1
- package/extensions/arnative.ts +2 -2
- package/extensions/footer.ts +118 -26
- package/extensions/section-headers.ts +32 -17
- package/extensions/tools.ts +4 -4
- package/extensions/ui-render-tweaks.ts +21 -4
- package/extensions/usage.ts +4 -3
- package/lib/ansi.ts +51 -7
- package/lib/usage-store.ts +1 -1
- 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,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.3] - 2026-10-14
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- A new session showed the previous session's token counts. The footer's 2s `getUsage` cache is keyed on branch length, and the module is not re-imported by `/new`, so a fresh session whose branch happened to reach the same length read the cached sums instead of its own. The cache is now cleared in the non-reload `session_start` branch (`extensions/footer.ts`).
|
|
7
|
+
- **The cell was invisible: the footer still matched the value on `jev-eye`.** The value became `Eye: ● ON (Clef Flash)`, which contains no `jev-eye` anywhere, so `value.includes("jev-eye")` went false and the whole cell fell through to the generic path — no icon, no dim backend suffix, just grey text. The router now recognises the cell by the label its value carries (`isJevEyeStatus`, `^\s*Eye\s*:`), and the self-check pins that rule from both sides: every mode's real value is recognised, `Jev: ● ON` and `MCP: 2 servers enabled` are not, and the value is asserted to contain no internal key name. Proved negative: reverting the rule to the substring test makes both `node extensions/footer.ts` and pi-jev-eye's `render-check` fail.
|
|
8
|
+
- **One space after the glyph, not two.** The two spaces existed because the previous glyph's ink overran its cell (`\uedcf`); with `\uf21b` in place they read as a gap in ` Eye: ● ON (Clef Flash)`. Pinned in pi-jev-eye's render check as one space, with an explicit assert that the glyph is not followed by two.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **The cell glyph is U+F21B (`fa-user_secret`), chosen by the operator.** U+EDCF (`fa-user_shield`) was the previous one; both exist in JetBrainsMono Nerd Font, and the codepoint is pinned by number in pi-jev-eye's render check because a Nerd glyph that renders blank on another platform is the bug the ponytail cell had (`\uf51` -> `\uef04`).
|
|
12
|
+
- **The pi-jev-eye status cell renders `Eye: ● ON (Backend)`, parsed from the value instead of hardcoded.** The format is `Eye: ● ON (Clef Flash)` when a Clef model answers and `Eye: ● ON (Jev)` when TypeSafe or OpenRouter does. `jevEyeStatusLine()` reads the label before the first colon and the backend from the trailing parentheses — so nothing about the cell is a literal in this repo, and an older pi-jev-eye value with no suffix gains no guessed one.
|
|
13
|
+
- **The backend suffix takes `dim`, while `Eye:` and the state keep their tiers.** A backend painted like the label would bury `Eye: ● ON`, which is the case this cell exists for; the spaced `Clef Flash` (not a nested `clef (flash)`) reads as one label on a single line.
|
|
14
|
+
- **The status line moved out of the render closure**, next to `ponytailStatusLine`, so the self-check in `node extensions/footer.ts` drives the exact path the footer uses instead of a copy of it.
|
|
15
|
+
- Dropped two unused imports (`CustomMessageComponent`, `isPointerOrFocusInput`) and corrected three stale statements: the usage estimate comment named `prices.json` where the code reads `models.json`, the footer header listed two of the three footer presets, and the README's local-test snippet ran one extension where `npm run dev` runs all eight (`extensions/ui-render-tweaks.ts`, `lib/usage-store.ts`, `extensions/footer.ts`, `test/footer-minimal.test.ts`, `README.md`).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Self-check for the pi-jev-eye cell: both requested forms (`Eye: ● ON (Clef Flash)`, `Eye: ● ON (Jev)`), the tierless `(Clef)`, an upstream `(Clef Omni)`, `OFF` taking the dim bullet, `REVIEW` keeping its warning bullet, and a suffix-less value drawing no parenthesis.
|
|
19
|
+
|
|
20
|
+
## [0.5.2] - 2026-10-13
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- 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`).
|
|
24
|
+
- 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.
|
|
25
|
+
- 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`).
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `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`).
|
|
29
|
+
- Behavioral test pinning the two-tone rule: labels take the border-accent hue, icons keep the accent hue (`test/footer-minimal.test.ts`).
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
- 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.
|
|
33
|
+
|
|
3
34
|
## [0.5.1] - 2026-10-13
|
|
4
35
|
|
|
5
36
|
### Fixed
|
package/README.md
CHANGED
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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Arnative footer (fixed 3-row grid). Row 1: cwd | active time | branch — model + thinking;
|
|
3
3
|
* rows 2-3: extension status | tok/s · cache · token, each side wrapped in its half of the
|
|
4
4
|
* width. The clock counts active time, not wall-clock age — see {@link ActiveClock}.
|
|
5
|
-
* Preset (`/arnative footers`), live: "Arnative (Full)" (default)
|
|
5
|
+
* Preset (`/arnative footers`), live: "Arnative (Full)" (default), "Arnative (Minimal)"
|
|
6
|
+
* or "Pi (system)".
|
|
6
7
|
*/
|
|
7
8
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
8
9
|
import { CustomEditor, FooterComponent, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -11,7 +12,7 @@ import { execFile } from "node:child_process";
|
|
|
11
12
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
13
14
|
import { dirname, join, resolve } from "node:path";
|
|
14
|
-
import { softOf, stripAnsi } from "../lib/ansi.ts";
|
|
15
|
+
import { softOf, softSecondaryOf, stripAnsi } from "../lib/ansi.ts";
|
|
15
16
|
import { assert, isMain } from "../lib/check.ts";
|
|
16
17
|
import { capitalize, markedLabels, modelDisplayParts } from "../lib/format.ts";
|
|
17
18
|
import { IS_WINDOWS_LIKE, SHORTCUT_RELOAD, SHORTCUT_TOGGLE_FOOTER } from "../lib/shortcuts.ts";
|
|
@@ -210,6 +211,55 @@ export function currentToolVerb(active: ReadonlyMap<string, string>): string {
|
|
|
210
211
|
return verb;
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The pi-jev-eye status cell: `Eye: ● MODE (Backend)`. pi-jev-eye writes the value plain because a status
|
|
216
|
+
* is a fact, not a layout; this decides the painting.
|
|
217
|
+
*
|
|
218
|
+
* The trailing parenthesised backend is a modifier, so it takes `dim` while the `Eye:` label and the state
|
|
219
|
+
* keep the soft and state tiers — that is what makes `(Clef Flash)` recede and `Eye: ● ON` stay readable
|
|
220
|
+
* at a glance. The backend name itself is read out of the value, never hardcoded here: the extension
|
|
221
|
+
* reports the active backend from `/jev-eye models`, so the footer cannot know it in advance.
|
|
222
|
+
*/
|
|
223
|
+
/**
|
|
224
|
+
* Is this status value pi-jev-eye's own cell? It is matched on the LABEL the value carries (`Eye:`), not on
|
|
225
|
+
* the internal key name: the value is `Eye: ● ON (Clef Flash)` and contains no `jev-eye` anywhere, so the
|
|
226
|
+
* old `value.includes("jev-eye")` test went false and the whole cell fell through to the generic path —
|
|
227
|
+
* which is exactly how it silently lost its icon and its dim backend suffix.
|
|
228
|
+
*
|
|
229
|
+
* Recognising the label keeps the rule honest: if pi-jev-eye renames its label, this test fails loudly in
|
|
230
|
+
* the self-check below instead of degrading to a plain grey cell.
|
|
231
|
+
*/
|
|
232
|
+
export const isJevEyeStatus = (status: string): boolean => /^\s*Eye\s*:/i.test(stripAnsi(status));
|
|
233
|
+
|
|
234
|
+
export function jevEyeStatusLine(
|
|
235
|
+
status: string,
|
|
236
|
+
acc: (text: string) => string,
|
|
237
|
+
dim: (text: string) => string,
|
|
238
|
+
soft: (text: string) => string,
|
|
239
|
+
theme?: { fg?: (color: string, text: string) => string },
|
|
240
|
+
): string {
|
|
241
|
+
const clean = stripAnsi(status);
|
|
242
|
+
const isOff = clean.includes("OFF") || clean.includes("\u25CB");
|
|
243
|
+
const isReview = /REVIEW/i.test(clean);
|
|
244
|
+
let bullet = isOff ? dim("\u25CB") : acc("\u25CF");
|
|
245
|
+
if (isReview && typeof theme?.fg === "function") {
|
|
246
|
+
try {
|
|
247
|
+
bullet = theme.fg("warning", "\u25CF");
|
|
248
|
+
} catch {
|
|
249
|
+
bullet = acc("\u25CF");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const label = isOff ? "OFF" : isReview ? "REVIEW" : "ON";
|
|
253
|
+
// `Eye: ● ON (Clef Flash)` -> the base before the first colon, the backend in the trailing parentheses.
|
|
254
|
+
const base = clean.split(":")[0]!.trim() || "Eye";
|
|
255
|
+
const at = clean.lastIndexOf("(");
|
|
256
|
+
const backend = at >= 0 && clean.trimEnd().endsWith(")") ? clean.slice(at, clean.lastIndexOf(")") + 1).trim() : "";
|
|
257
|
+
// One space, not the two the previous glyph needed: \uf21b's ink fits its own cell, so the label sits
|
|
258
|
+
// right after it. The space before the backend stays outside the dim run, so the modifier does not
|
|
259
|
+
// start with a painted blank.
|
|
260
|
+
return `${acc("\uf21b")} ${soft(`${base}:`)} ${bullet} ${soft(label)}${backend ? ` ${dim(backend)}` : ""}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
213
263
|
/**
|
|
214
264
|
* The ponytail status cell. The horse lives at U+EF04 (fa-horse). U+EF51
|
|
215
265
|
* (fa-circle_radiation) carries no outline in the Windows Nerd Font build, so it
|
|
@@ -848,6 +898,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
848
898
|
currentModel = ctx.model;
|
|
849
899
|
currentThinkingLevel = ctx.thinkingLevel;
|
|
850
900
|
git = null;
|
|
901
|
+
// A new session reuses this module (no re-import), so the 2s cache would serve the
|
|
902
|
+
// previous session's sums whenever the fresh branch happens to be the same length.
|
|
903
|
+
usageScanAt = 0;
|
|
904
|
+
usageScanLen = -1;
|
|
851
905
|
} else {
|
|
852
906
|
currentModel = (globalThis as Record<symbol, any>)[MODEL_KEY] ?? ctx.model;
|
|
853
907
|
currentThinkingLevel = (globalThis as Record<symbol, any>)[THINKING_KEY] ?? ctx.thinkingLevel;
|
|
@@ -889,9 +943,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
889
943
|
const acc = (text: string) => theme.fg("accent", text);
|
|
890
944
|
const dim = (text: string) => theme.fg("dim", text);
|
|
891
945
|
const fgAny = theme.fg.bind(theme) as (color: string, text: string) => string;
|
|
892
|
-
// `accentSoft`
|
|
893
|
-
// through the shared
|
|
946
|
+
// `accentSoft`/`secondarySoft` are vars-only tokens on Pi 1.2+, so both soft tiers
|
|
947
|
+
// are resolved through the shared painters instead of a raw fg() probe. Icons keep
|
|
948
|
+
// `accent`; the cwd/branch/clock/status text takes the border-accent tier, so the
|
|
949
|
+
// footer stays two-toned instead of the text inheriting the accent hue.
|
|
894
950
|
const soft = softOf(theme as never);
|
|
951
|
+
const softLabel = softSecondaryOf(theme as never);
|
|
895
952
|
// Diff colors: the same theme slots pi uses for a diff body, with the
|
|
896
953
|
// plain soft color as fallback for a theme that omits them.
|
|
897
954
|
const colorOr = (name: string, fallback: (t: string) => string) => (t: string) => {
|
|
@@ -901,12 +958,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
901
958
|
return fallback(t);
|
|
902
959
|
}
|
|
903
960
|
};
|
|
904
|
-
// `primary
|
|
905
|
-
//
|
|
961
|
+
// `primary` is a var, not a token (Pi 1.2+ rejects custom `colors` keys); its
|
|
962
|
+
// arnative carrier is the official token it maps to.
|
|
906
963
|
const primary = colorOr("mdHeading", acc);
|
|
907
|
-
const
|
|
908
|
-
const secondary = colorOr("borderAccent", soft);
|
|
909
|
-
const secondarySoft = soft;
|
|
964
|
+
const secondarySoft = softLabel;
|
|
910
965
|
const diffAdd = colorOr("toolDiffAdded", soft);
|
|
911
966
|
const diffDel = colorOr("toolDiffRemoved", soft);
|
|
912
967
|
const sep = dim(" | ");
|
|
@@ -947,22 +1002,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
947
1002
|
return `${acc("\uf233")} ${secondarySoft(m ? m[0] : rest || "MCP")}`;
|
|
948
1003
|
}
|
|
949
1004
|
if (s.includes("ponytail")) return ponytailStatusLine(s, acc, dim, secondarySoft);
|
|
950
|
-
if (s
|
|
951
|
-
const isOff = s.includes("OFF") || s.includes("○");
|
|
952
|
-
const isReview = /REVIEW/i.test(s);
|
|
953
|
-
let bullet = isOff ? dim("○") : acc("●");
|
|
954
|
-
if (isReview) {
|
|
955
|
-
try {
|
|
956
|
-
bullet = theme.fg("warning", "●");
|
|
957
|
-
} catch {
|
|
958
|
-
bullet = acc("●");
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
const label = isOff ? "OFF" : isReview ? "REVIEW" : "ON";
|
|
962
|
-
// Two spaces: \uedcf's ink is wider than MCP's \uf233 or ponytail's \uef04,
|
|
963
|
-
// so it needs the extra cell to clear the label.
|
|
964
|
-
return `${acc("\uedcf")} ${secondarySoft("Jev:")} ${bullet} ${secondarySoft(label)}`;
|
|
965
|
-
}
|
|
1005
|
+
if (isJevEyeStatus(s)) return jevEyeStatusLine(s, acc, dim, secondarySoft, theme);
|
|
966
1006
|
let clean = stripAnsi(s);
|
|
967
1007
|
clean = clean.replace(/\uFFFD/g, "").replace(/\?{1,2}\s*/g, "");
|
|
968
1008
|
return secondarySoft(clean.trim());
|
|
@@ -1044,7 +1084,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1044
1084
|
try {
|
|
1045
1085
|
const th = this.getActiveTheme();
|
|
1046
1086
|
if (!th) return text;
|
|
1047
|
-
return
|
|
1087
|
+
return softSecondaryOf(th as never)(text);
|
|
1048
1088
|
} catch {
|
|
1049
1089
|
return text;
|
|
1050
1090
|
}
|
|
@@ -1437,6 +1477,58 @@ if (isMain(import.meta.url)) {
|
|
|
1437
1477
|
assert(!ponyLine.includes("\uef51"), "the blank \\uef51 glyph must never come back");
|
|
1438
1478
|
assert(ponytailStatusLine("ponytail: LITE", (s) => s, (s) => s, (s) => s).includes("LITE"), "the ponytail mode is read from the status text");
|
|
1439
1479
|
|
|
1480
|
+
// Self-check: the status router must recognise the pi-jev-eye cell by the label its VALUE carries.
|
|
1481
|
+
// The old test was `value.includes("jev-eye")`, and the value is `Eye: ● ON (Clef Flash)` — no
|
|
1482
|
+
// `jev-eye` anywhere — so the branch went false and the cell silently lost its icon and dim suffix.
|
|
1483
|
+
// This pins the recognition rule itself, at the same place the router uses it.
|
|
1484
|
+
assert(isJevEyeStatus("Eye: ● ON (Clef Flash)"), "the value pi-jev-eye publishes is recognised as its own cell");
|
|
1485
|
+
assert(isJevEyeStatus("Eye: ○ OFF (Jev)"), "and so is every other mode's value");
|
|
1486
|
+
assert(!isJevEyeStatus("Jev: ● ON"), "the previous format is not claimed as this cell");
|
|
1487
|
+
assert(!isJevEyeStatus("MCP: 2 servers enabled"), "and neither is another extension's status");
|
|
1488
|
+
assert(
|
|
1489
|
+
!stripAnsi("Eye: ● ON (Clef Flash)").includes("jev-eye"),
|
|
1490
|
+
"the value carries no internal key name, which is what broke the old matcher",
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
// Self-check: the pi-jev-eye status cell. The value is `Eye: ● MODE (Backend)`; the backend is read out
|
|
1494
|
+
// of that string rather than hardcoded, and it takes `dim` while the base label and the state stay
|
|
1495
|
+
// readable — a backend painted like the label would bury `Eye: ● ON`, which is the case this cell
|
|
1496
|
+
// exists for. Both footer formats the operator asked for are pinned here.
|
|
1497
|
+
const jevPlain = (s: string) => s;
|
|
1498
|
+
const jevDim = (s: string) => `[dim]${s}[/dim]`;
|
|
1499
|
+
assert(
|
|
1500
|
+
jevEyeStatusLine("Eye: ● ON (Clef Flash)", jevPlain, jevDim, jevPlain, undefined) ===
|
|
1501
|
+
"\uf21b" + " Eye: ● ON [dim](Clef Flash)[/dim]",
|
|
1502
|
+
"Clef Flash is dim while `Eye:` and the state are not",
|
|
1503
|
+
);
|
|
1504
|
+
assert(
|
|
1505
|
+
jevEyeStatusLine("Eye: ● ON (Jev)", jevPlain, jevDim, jevPlain, undefined) === "\uf21b" + " Eye: ● ON [dim](Jev)[/dim]",
|
|
1506
|
+
"TypeSafe and OpenRouter both read as (Jev)",
|
|
1507
|
+
);
|
|
1508
|
+
assert(
|
|
1509
|
+
jevEyeStatusLine("Eye: ● ON (Clef)", jevPlain, jevDim, jevPlain, undefined) === "\uf21b" + " Eye: ● ON [dim](Clef)[/dim]",
|
|
1510
|
+
"the untiered Clef model draws no tier word",
|
|
1511
|
+
);
|
|
1512
|
+
assert(
|
|
1513
|
+
jevEyeStatusLine("Eye: ● ON (Clef Omni)", jevPlain, jevDim, jevPlain, undefined) === "\uf21b" + " Eye: ● ON [dim](Clef Omni)[/dim]",
|
|
1514
|
+
"a model added upstream draws its own tier",
|
|
1515
|
+
);
|
|
1516
|
+
assert(
|
|
1517
|
+
jevEyeStatusLine("Eye: ○ OFF (Jev)", jevPlain, jevDim, jevPlain, undefined) === "\uf21b" + " Eye: [dim]○[/dim] OFF [dim](Jev)[/dim]",
|
|
1518
|
+
"OFF takes the dim bullet",
|
|
1519
|
+
);
|
|
1520
|
+
assert(
|
|
1521
|
+
jevEyeStatusLine("Eye: ● REVIEW (Clef Flash)", jevPlain, jevDim, jevPlain, { fg: (color, text) => `<${color}>${text}</${color}>` }) ===
|
|
1522
|
+
"\uf21b" + " Eye: <warning>●</warning> REVIEW [dim](Clef Flash)[/dim]",
|
|
1523
|
+
"REVIEW keeps the warning bullet",
|
|
1524
|
+
);
|
|
1525
|
+
// A value with no parenthesised backend (an older pi-jev-eye) must not gain a stray `(`: the suffix is
|
|
1526
|
+
// drawn only when the value actually carries one.
|
|
1527
|
+
assert(
|
|
1528
|
+
jevEyeStatusLine("Eye: ● ON", jevPlain, jevDim, jevPlain, undefined) === "\uf21b" + " Eye: ● ON",
|
|
1529
|
+
"no backend suffix means no parenthesis, never a guessed one",
|
|
1530
|
+
);
|
|
1531
|
+
|
|
1440
1532
|
// Self-check: session age survives /resume (header timestamp wins over now).
|
|
1441
1533
|
const NOW = Date.parse("2026-10-08T12:00:00.000Z");
|
|
1442
1534
|
assert(resolveSessionStartMs("resume", "2026-10-08T09:00:00.000Z", NOW) === Date.parse("2026-10-08T09:00:00.000Z"), "resume restores the session age from the header");
|
|
@@ -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,
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import {
|
|
18
18
|
CompactionSummaryMessageComponent,
|
|
19
|
-
CustomMessageComponent,
|
|
20
19
|
ExtensionSelectorComponent,
|
|
21
20
|
getMarkdownTheme,
|
|
22
21
|
initTheme,
|
|
@@ -39,9 +38,9 @@ import {
|
|
|
39
38
|
} from "@earendil-works/pi-tui";
|
|
40
39
|
import { readdirSync, readFileSync } from "node:fs";
|
|
41
40
|
import { renderBoxLines } from "../lib/box.ts";
|
|
42
|
-
import { ansiBgOpen, getActiveTheme, setActiveTheme, stripAnsi, themeOf,
|
|
41
|
+
import { ansiBgOpen, getActiveTheme, setActiveTheme, stripAnsi, themeOf, softSecondaryOf } from "../lib/ansi.ts";
|
|
43
42
|
import { assert, isMain } from "../lib/check.ts";
|
|
44
|
-
import { contrast, parseOklch, toHex } from "../lib/color.ts";
|
|
43
|
+
import { contrast, oklchToRgb, parseOklch, toHex } from "../lib/color.ts";
|
|
45
44
|
import { expandKeyName } from "../lib/format.ts";
|
|
46
45
|
|
|
47
46
|
type ActiveTheme = { fg(color: string, text: string): string; bg?(color: string, text: string): string } | null;
|
|
@@ -174,7 +173,7 @@ const SELECT_ROW_KEY = Symbol.for("pi-arnative.selectRowColors");
|
|
|
174
173
|
|
|
175
174
|
/** Selected row body: `·` separators accent, the text around them soft. */
|
|
176
175
|
export function selectRowText(row: string, th: { fg(c: string, t: string): string }): string {
|
|
177
|
-
const soft =
|
|
176
|
+
const soft = softSecondaryOf(th);
|
|
178
177
|
return row
|
|
179
178
|
.split("·")
|
|
180
179
|
.map((part) => soft(part))
|
|
@@ -396,6 +395,14 @@ if (isMain(import.meta.url)) {
|
|
|
396
395
|
// hex, so resolve every token through lib/color before constructing it.
|
|
397
396
|
const resolve = (value: string) => toHex(themeJson.vars[value] ?? value);
|
|
398
397
|
const hex = (tok: string) => resolve(themeJson.colors[tok] ?? tok);
|
|
398
|
+
// Mirrors `soften()` (themes/gen-themes.mjs) and `softenColor` (lib/ansi.ts): the soft
|
|
399
|
+
// tier is the base color at L 0.82 with a fifth of its chroma. Both sides read the
|
|
400
|
+
// declared OKLCH string (not the 8-bit hex), so the comparison is not rounded twice.
|
|
401
|
+
const softenToHex = (base: string) => {
|
|
402
|
+
const o = parseOklch(base);
|
|
403
|
+
assert(o !== null, `cannot soften a non-oklch color: ${base}`);
|
|
404
|
+
return toHex(`oklch(82% ${oklchToRgb(0.82, o.C * 0.18, o.H).chroma} ${o.H})`);
|
|
405
|
+
};
|
|
399
406
|
// The built-in theme adds fallbacks (scrollbarTrack<-muted, thinkingMax<-thinkingXhigh),
|
|
400
407
|
// so the fg map must be complete; take the whole palette like pi's loader does.
|
|
401
408
|
const fgPalette = Object.fromEntries(
|
|
@@ -508,8 +515,18 @@ if (isMain(import.meta.url)) {
|
|
|
508
515
|
// Accent pill: accent bg + toolPendingBg colored text.
|
|
509
516
|
assert(contrast(hex("accent"), hex("toolPendingBg")) >= 4.5, `theme ${f}: accent vs toolPendingBg contrast >= 4.5`);
|
|
510
517
|
assert(isColor(d.vars.accentSoft ?? ""), `theme ${f}: vars.accentSoft is a color`);
|
|
518
|
+
assert(isColor(d.vars.secondarySoft ?? ""), `theme ${f}: vars.secondarySoft is a color`);
|
|
511
519
|
assert(isColor(d.vars.primary ?? ""), `theme ${f}: vars.primary is a color`);
|
|
512
520
|
assert(isColor(d.vars.secondary ?? ""), `theme ${f}: vars.secondary is a color`);
|
|
521
|
+
// The declared soft vars must equal what the extensions derive at runtime, or the
|
|
522
|
+
// footer label colour drifts from the theme file. `softenToHex` mirrors
|
|
523
|
+
// `soften()` in gen-themes.mjs and `softenColor` in lib/ansi.ts.
|
|
524
|
+
for (const [softName, baseName] of [["accentSoft", "accent"], ["secondarySoft", "borderAccent"]] as const) {
|
|
525
|
+
const base = d.vars[d.colors[baseName] ?? baseName]!;
|
|
526
|
+
const declared = toHex(d.vars[softName]!);
|
|
527
|
+
const derived = softenToHex(base);
|
|
528
|
+
assert(declared === derived, `theme ${f}: vars.${softName} ${declared} != runtime ${derived} (derived from ${baseName})`);
|
|
529
|
+
}
|
|
513
530
|
const canvas = hex("userMessageBg");
|
|
514
531
|
for (const tok of Object.keys(d.colors)) {
|
|
515
532
|
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/lib/usage-store.ts
CHANGED
|
@@ -117,7 +117,7 @@ export interface UsageSummary {
|
|
|
117
117
|
cost: number;
|
|
118
118
|
/** Provider-reported cost only (never includes the estimate). */
|
|
119
119
|
reportedCost: number;
|
|
120
|
-
/** Cost reconstructed from `
|
|
120
|
+
/** Cost reconstructed from `models.json` rates. Shown with a `~`, never folded into `cost`. */
|
|
121
121
|
estimatedCost: number;
|
|
122
122
|
};
|
|
123
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bismawy/pi-arnative",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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")),
|