@gavin-hu/my-pi-agent 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
29
29
|
import { type Component, stripTerminalSequences, truncateToWidth } from "@earendil-works/pi-tui";
|
|
30
30
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
31
|
-
import { BODY_INDENT, bodyLines, indentBlockLines } from "../../../lib/tui/indent.ts";
|
|
31
|
+
import { BODY_INDENT, bodyLines, indentBlockLines, isBlank } from "../../../lib/tui/indent.ts";
|
|
32
32
|
import { RenderCache } from "../../../lib/tui/render-cache.ts";
|
|
33
33
|
import { paintPrefix } from "../../../lib/tui/tokens.ts";
|
|
34
34
|
|
|
@@ -174,14 +174,6 @@ export function isRewritable(component: Component): component is Component & Rew
|
|
|
174
174
|
return typeof candidate.text === "string" && typeof candidate.setText === "function";
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
/**
|
|
178
|
-
* Whether a line has no visible text. Tested after stripping terminal sequences,
|
|
179
|
-
* so a line carrying only a background or the indent counts as blank.
|
|
180
|
-
*/
|
|
181
|
-
function isBlankLine(line: string): boolean {
|
|
182
|
-
return stripTerminalSequences(line).trim().length === 0;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
177
|
/**
|
|
186
178
|
* Whether a call line degenerated to the tool name alone, so the rest of it —
|
|
187
179
|
* the path, the command — moved onto the body lines under it.
|
|
@@ -195,7 +187,7 @@ function isBlankLine(line: string): boolean {
|
|
|
195
187
|
*/
|
|
196
188
|
function isBareHeadline(body: readonly string[], toolName: string | undefined): boolean {
|
|
197
189
|
if (!toolName) return false;
|
|
198
|
-
const headline = body.find((line) => !
|
|
190
|
+
const headline = body.find((line) => !isBlank(line));
|
|
199
191
|
return headline !== undefined && stripTerminalSequences(headline).trim() === sanitizeLine(toolName);
|
|
200
192
|
}
|
|
201
193
|
|
|
@@ -206,9 +198,9 @@ function isBareHeadline(body: readonly string[], toolName: string | undefined):
|
|
|
206
198
|
*/
|
|
207
199
|
function callContinuationCount(body: readonly string[], toolName: string | undefined): number {
|
|
208
200
|
if (!isBareHeadline(body, toolName)) return 0;
|
|
209
|
-
let index = body.findIndex((line) => !
|
|
201
|
+
let index = body.findIndex((line) => !isBlank(line)) + 1;
|
|
210
202
|
let count = 0;
|
|
211
|
-
while (index < body.length && !
|
|
203
|
+
while (index < body.length && !isBlank(body[index] ?? "")) {
|
|
212
204
|
count++;
|
|
213
205
|
index++;
|
|
214
206
|
}
|
|
@@ -228,7 +220,7 @@ function frameLines(
|
|
|
228
220
|
options: ToolBlockOptions = {},
|
|
229
221
|
): string[] {
|
|
230
222
|
const w = Math.max(1, width);
|
|
231
|
-
const indent =
|
|
223
|
+
const indent = outputPad + BODY_INDENT;
|
|
232
224
|
const lines = indentBlockLines(body, w, {
|
|
233
225
|
indent,
|
|
234
226
|
rail: options.rail,
|
|
@@ -277,7 +269,7 @@ export function toolBlockLines(
|
|
|
277
269
|
options: ToolBlockOptions = {},
|
|
278
270
|
): string[] {
|
|
279
271
|
const w = Math.max(1, width);
|
|
280
|
-
const indent =
|
|
272
|
+
const indent = outputPad + BODY_INDENT;
|
|
281
273
|
return frameLines(bodyLines(inner, w, indent), w, outputPad, options);
|
|
282
274
|
}
|
|
283
275
|
|
|
@@ -307,7 +299,7 @@ export class ToolBlock implements Component {
|
|
|
307
299
|
|
|
308
300
|
render(width: number): string[] {
|
|
309
301
|
const w = Math.max(1, width);
|
|
310
|
-
const indent =
|
|
302
|
+
const indent = this.outputPad + BODY_INDENT;
|
|
311
303
|
const body = bodyLines(this.inner, w, indent);
|
|
312
304
|
return this.cache.lines(w, body, () => frameLines(body, w, this.outputPad, this.options));
|
|
313
305
|
}
|
|
@@ -55,7 +55,12 @@ import { isRewritable, recolorCallHeadline, recolorShellCall, ToolBlock } from "
|
|
|
55
55
|
import { BLOCK_CONFIG, type BlockStatus } from "./config.ts";
|
|
56
56
|
import { callFallback, resultFallback } from "./fallback.ts";
|
|
57
57
|
|
|
58
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Per-row memory of the base renderer's own components, stored in the `state`
|
|
60
|
+
* Pi creates for one tool row and hands to both its call and its result
|
|
61
|
+
* renderer. Keeping the base components there is what lets the row keep them
|
|
62
|
+
* while `lastComponent` points at this frame instead.
|
|
63
|
+
*/
|
|
59
64
|
interface BlockMemory {
|
|
60
65
|
byteflyBaseCall?: Component;
|
|
61
66
|
byteflyBaseResult?: Component;
|
|
@@ -45,7 +45,11 @@ export function registerDoctor(pi: ExtensionAPI, deps: DoctorDeps = {}): DoctorR
|
|
|
45
45
|
// without importing the extension.
|
|
46
46
|
pi.events.on(WEB_SEARCH_PROVIDER_CHANNEL, (facts) => runtime.noteSearchFacts(facts));
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// Bumped when a session starts or ends, so a probe that outlives its session
|
|
49
|
+
// cannot post its notice into the next one (a reload re-registers the surface).
|
|
50
|
+
let probeToken = 0;
|
|
51
|
+
|
|
52
|
+
pi.on("session_start", (_event, ctx) => {
|
|
49
53
|
const config = loadConfig(ctx.cwd);
|
|
50
54
|
runtime.configure({ cwd: ctx.cwd, config });
|
|
51
55
|
if (!config.proactive || !ctx.hasUI) return;
|
|
@@ -55,12 +59,27 @@ export function registerDoctor(pi: ExtensionAPI, deps: DoctorDeps = {}): DoctorR
|
|
|
55
59
|
if (pi.getSettings?.().quietStartup !== true) return;
|
|
56
60
|
|
|
57
61
|
// Only hard requirements can break an enabled extension, so the startup
|
|
58
|
-
// probe is limited to the extensions that declare one, in parallel.
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
// probe is limited to the extensions that declare one, in parallel. Fired
|
|
63
|
+
// without awaiting: Pi awaits every `session_start` handler, and these
|
|
64
|
+
// probes spawn processes, so holding the first prompt on them is the cost
|
|
65
|
+
// this avoids. The notice lands a moment later, in the flow, as before.
|
|
66
|
+
const token = ++probeToken;
|
|
67
|
+
void runtime
|
|
68
|
+
.probeRequired()
|
|
69
|
+
.then((reports) => {
|
|
70
|
+
if (token !== probeToken) return;
|
|
71
|
+
const missing = missingFindings(reports);
|
|
72
|
+
if (missing.length > 0) pi.sendMessage(warningNotice(missing));
|
|
73
|
+
})
|
|
74
|
+
.catch(() => {
|
|
75
|
+
// A background check never surfaces an error, and the report is advice.
|
|
76
|
+
});
|
|
61
77
|
});
|
|
62
78
|
|
|
63
|
-
pi.on("session_shutdown", () =>
|
|
79
|
+
pi.on("session_shutdown", () => {
|
|
80
|
+
probeToken += 1;
|
|
81
|
+
runtime.reset();
|
|
82
|
+
});
|
|
64
83
|
|
|
65
84
|
return runtime;
|
|
66
85
|
}
|
|
@@ -57,7 +57,11 @@ export function registerTools(pi: ExtensionAPI, runtime: DoctorRuntime): void {
|
|
|
57
57
|
],
|
|
58
58
|
parameters: DoctorParams,
|
|
59
59
|
outputSchema: DoctorOutput,
|
|
60
|
+
// `refresh` reaches the configured search endpoint, but that URL comes from
|
|
61
|
+
// config, not from the model, so the tool stays closed-world for the policy.
|
|
62
|
+
// `probeAll` writes the session's report cache, so its calls are ordered.
|
|
60
63
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
64
|
+
executionMode: "sequential",
|
|
61
65
|
|
|
62
66
|
async execute(_toolCallId, params, _signal, onUpdate, _ctx) {
|
|
63
67
|
const args = params as DoctorArgs;
|
|
@@ -48,6 +48,7 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
|
|
|
48
48
|
let delegationTotals = { runs: 0, usage: emptyUsage() };
|
|
49
49
|
let cacheHitRate: number | null = null;
|
|
50
50
|
let context: ContextInfo = { tokens: null, contextWindow: 0, percent: null };
|
|
51
|
+
let sessionName: string | null = null;
|
|
51
52
|
|
|
52
53
|
let last: UsageTotals | undefined;
|
|
53
54
|
|
|
@@ -70,6 +71,10 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
|
|
|
70
71
|
contextWindow: usageInfo?.contextWindow ?? current?.contextWindow ?? 0,
|
|
71
72
|
percent: usageInfo?.percent ?? null,
|
|
72
73
|
};
|
|
74
|
+
// The host walks the file backwards for the latest `session_info`, and a
|
|
75
|
+
// session renamed early makes that the whole transcript. A rename appends
|
|
76
|
+
// an entry, which moves the leaf, so the memo key already covers it.
|
|
77
|
+
sessionName = session.getSessionName() ?? null;
|
|
73
78
|
key = nextKey;
|
|
74
79
|
last = usage;
|
|
75
80
|
}
|
|
@@ -80,7 +85,7 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
|
|
|
80
85
|
|
|
81
86
|
return {
|
|
82
87
|
cwd: ctx.cwd,
|
|
83
|
-
sessionName
|
|
88
|
+
sessionName,
|
|
84
89
|
branch: footerData.getGitBranch(),
|
|
85
90
|
statuses: footerData.getExtensionStatuses(),
|
|
86
91
|
model,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tuning for the two-line status bar.
|
|
3
3
|
*
|
|
4
|
-
* The bar's own content and layout: context thresholds, gauge
|
|
5
|
-
*
|
|
4
|
+
* The bar's own content and layout: context thresholds, gauge widths,
|
|
5
|
+
* separators, icons, status keys, and label limits. Segment weights
|
|
6
6
|
* live on the segments themselves (see `../render/segments.ts`); the price
|
|
7
7
|
* configuration is `bytefly.json`, read by `../price/config.ts`.
|
|
8
8
|
*/
|
|
@@ -12,10 +12,14 @@ import { GLYPHS, SEPARATORS, STATUS_KEYS } from "../../../lib/tui/glyphs.ts";
|
|
|
12
12
|
export const CONFIG = {
|
|
13
13
|
/** Context percentage thresholds. */
|
|
14
14
|
thresholds: { warn: 70, danger: 90 },
|
|
15
|
-
/**
|
|
16
|
-
gauge: {
|
|
17
|
-
/**
|
|
18
|
-
|
|
15
|
+
/** The block counts the context gauge steps down through. */
|
|
16
|
+
gauge: { widths: [10, 5, 3, 0] as const },
|
|
17
|
+
/**
|
|
18
|
+
* Separators printed before grouped and other segments. The group divider is
|
|
19
|
+
* the footer's own solid `│`, not the shared `╎`: that glyph is the transcript
|
|
20
|
+
* rail (and `chrome.rail`'s default), and a status line is not a rail.
|
|
21
|
+
*/
|
|
22
|
+
separators: { group: " │ ", item: SEPARATORS.item },
|
|
19
23
|
icons: {
|
|
20
24
|
branch: GLYPHS.branch,
|
|
21
25
|
detached: GLYPHS.detached,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { emptyUsage } from "../collect/entries.ts";
|
|
12
12
|
import type { ByteflyConfig, ModelRates } from "./types.ts";
|
|
13
13
|
import type { UsageRecord, UsageTotals } from "../types.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { createRateLookup } from "./rates.ts";
|
|
15
15
|
import { isPeak, peakFormatter } from "./schedule.ts";
|
|
16
16
|
|
|
17
17
|
const TOKENS_PER_MILLION = 1_000_000;
|
|
@@ -37,7 +37,9 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
|
|
|
37
37
|
// Both currencies price from `rates` and the tariff; they differ only in what
|
|
38
38
|
// an unmatched model falls back to, and Pi's recorded cost is already dollars.
|
|
39
39
|
const dollars = config.currency.code === "USD";
|
|
40
|
-
//
|
|
40
|
+
// Both are built once per config, never per record: the table splits every key
|
|
41
|
+
// and remembers the last match, and the formatter is the expensive part.
|
|
42
|
+
const lookup = createRateLookup(config.rates);
|
|
41
43
|
const format = config.peak.windows.length > 0 ? peakFormatter(config.peak.timezone) : null;
|
|
42
44
|
|
|
43
45
|
return (records) => {
|
|
@@ -47,7 +49,7 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
|
|
|
47
49
|
totals.output += record.output;
|
|
48
50
|
totals.cacheRead += record.cacheRead;
|
|
49
51
|
totals.cacheWrite += record.cacheWrite;
|
|
50
|
-
const rates =
|
|
52
|
+
const rates = lookup(record.provider, record.model);
|
|
51
53
|
if (rates !== undefined) {
|
|
52
54
|
totals.cost += ratedCost(config, format, record, rates);
|
|
53
55
|
continue;
|
|
@@ -2,13 +2,31 @@
|
|
|
2
2
|
* Rate lookup for a usage record.
|
|
3
3
|
*
|
|
4
4
|
* A key is `provider/model`, and either segment may be a wildcard: `*` matches
|
|
5
|
-
* anything, and a trailing star matches a prefix (`gpt-*`). The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* anything, and a trailing star matches a prefix (`gpt-*`). The key splits at
|
|
6
|
+
* its first slash, so the model segment may itself contain one — an OpenRouter
|
|
7
|
+
* id is `anthropic/claude-fable-5`, and its key is
|
|
8
|
+
* `openrouter/anthropic/claude-fable-5`. The most specific key wins and the
|
|
9
|
+
* model segment outranks the provider one, so the documented order is
|
|
10
|
+
* `provider/model`, then `any/model`, then `provider/any`, then `*`.
|
|
11
|
+
*
|
|
12
|
+
* `createRateLookup` prepares the table once for a config: the keys are split
|
|
13
|
+
* at build time, and the last match is remembered, because a session prices
|
|
14
|
+
* hundreds of records from one model in a row. Splitting every key per record
|
|
15
|
+
* would rebuild the whole table on the footer's hot path, once per record.
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
import type { ModelRates } from "./types.ts";
|
|
11
19
|
|
|
20
|
+
/** A prepared rate table: call once per record, not once per key. */
|
|
21
|
+
export type RateLookup = (provider: string | null, model: string | null) => ModelRates | undefined;
|
|
22
|
+
|
|
23
|
+
/** One compiled key: both segments already split out of the config string. */
|
|
24
|
+
interface CompiledRate {
|
|
25
|
+
provider: string;
|
|
26
|
+
model: string;
|
|
27
|
+
value: ModelRates;
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
/** One segment's score against a value: exact 2, prefix 1, wildcard 0, no match -1. */
|
|
13
31
|
function segmentScore(pattern: string, value: string): number {
|
|
14
32
|
if (pattern === "*") return 0;
|
|
@@ -17,38 +35,57 @@ function segmentScore(pattern: string, value: string): number {
|
|
|
17
35
|
}
|
|
18
36
|
|
|
19
37
|
/**
|
|
20
|
-
* How specifically
|
|
38
|
+
* How specifically a compiled key matches a record, or `-1` when it does not.
|
|
21
39
|
*
|
|
22
40
|
* The model segment dominates: the score is weighted so every model score
|
|
23
41
|
* outranks any provider score.
|
|
24
42
|
*/
|
|
25
|
-
function
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
if (parts.length !== 2) return -1;
|
|
29
|
-
const providerScore = segmentScore(parts[0], provider);
|
|
30
|
-
const modelScore = segmentScore(parts[1], model);
|
|
43
|
+
function matchRate(entry: CompiledRate, provider: string, model: string): number {
|
|
44
|
+
const providerScore = segmentScore(entry.provider, provider);
|
|
45
|
+
const modelScore = segmentScore(entry.model, model);
|
|
31
46
|
if (providerScore < 0 || modelScore < 0) return -1;
|
|
32
47
|
return modelScore * 3 + providerScore;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
|
-
/**
|
|
36
|
-
export function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
model: string | null,
|
|
40
|
-
): ModelRates | undefined {
|
|
41
|
-
// A record that names no model can only be priced by the catch-all.
|
|
42
|
-
if (!provider || !model) return rates["*"];
|
|
43
|
-
let best: ModelRates | undefined;
|
|
44
|
-
let bestScore = -1;
|
|
50
|
+
/** Prepare `rates` for one config; the returned lookup remembers its last match. */
|
|
51
|
+
export function createRateLookup(rates: Readonly<Record<string, ModelRates>>): RateLookup {
|
|
52
|
+
const compiled: CompiledRate[] = [];
|
|
53
|
+
let catchAll: ModelRates | undefined;
|
|
45
54
|
for (const [key, value] of Object.entries(rates)) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
const slash = key.indexOf("/");
|
|
56
|
+
if (slash < 0) {
|
|
57
|
+
// A bare key is the catch-all only when it is `*`; anything else is
|
|
58
|
+
// malformed and never matches.
|
|
59
|
+
if (key === "*") {
|
|
60
|
+
catchAll = value;
|
|
61
|
+
compiled.push({ provider: "*", model: "*", value });
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
51
64
|
}
|
|
65
|
+
compiled.push({ provider: key.slice(0, slash), model: key.slice(slash + 1), value });
|
|
52
66
|
}
|
|
53
|
-
|
|
67
|
+
|
|
68
|
+
let lastProvider: string | null = null;
|
|
69
|
+
let lastModel: string | null = null;
|
|
70
|
+
let last: ModelRates | undefined;
|
|
71
|
+
|
|
72
|
+
return (provider, model) => {
|
|
73
|
+
// A record that names no model can only be priced by the catch-all.
|
|
74
|
+
if (!provider || !model) return catchAll;
|
|
75
|
+
if (provider === lastProvider && model === lastModel) return last;
|
|
76
|
+
let best: ModelRates | undefined;
|
|
77
|
+
let bestScore = -1;
|
|
78
|
+
for (const entry of compiled) {
|
|
79
|
+
const score = matchRate(entry, provider, model);
|
|
80
|
+
// `>` keeps the first key in config order when two are equally specific.
|
|
81
|
+
if (score > bestScore) {
|
|
82
|
+
best = entry.value;
|
|
83
|
+
bestScore = score;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lastProvider = provider;
|
|
87
|
+
lastModel = model;
|
|
88
|
+
last = best;
|
|
89
|
+
return best;
|
|
90
|
+
};
|
|
54
91
|
}
|
|
@@ -12,13 +12,13 @@ import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
|
12
12
|
/**
|
|
13
13
|
* Compact a status to its icon form for a narrow bar, keeping its own colour.
|
|
14
14
|
*
|
|
15
|
-
* An icon plus a label (
|
|
15
|
+
* An icon plus a label (`≡ plan`) keeps only the icon. An icon plus a bare
|
|
16
16
|
* count (`↺ 2`) is a badge: it keeps the count and drops the space (`↺2`),
|
|
17
17
|
* because the number is the state. The leading open and trailing reset SGR
|
|
18
18
|
* sequences are reattached around the compacted text, so a red `✗1` stays red
|
|
19
19
|
* instead of losing its colour exactly when the bar tightens. Requiring exactly
|
|
20
20
|
* two tokens (rather than "the last token is numeric") keeps a numeric label
|
|
21
|
-
* like
|
|
21
|
+
* like `≡ plan · 2024` from being mistaken for a count.
|
|
22
22
|
*/
|
|
23
23
|
export function compactStatus(status: string): string {
|
|
24
24
|
const prefix = status.match(/^(?:\x1b\[[0-9;]*m)*/)?.[0] ?? "";
|
|
@@ -33,7 +33,7 @@ export function compactStatus(status: string): string {
|
|
|
33
33
|
/**
|
|
34
34
|
* Drop a status's trailing ` · detail` while keeping its SGR styling.
|
|
35
35
|
*
|
|
36
|
-
* The `plan-mode` chip carries the plan file name as detail (
|
|
36
|
+
* The `plan-mode` chip carries the plan file name as detail (`≡ plan · name`);
|
|
37
37
|
* the bar shows only the mode. Matching `[^ESC]*` removes the visible suffix
|
|
38
38
|
* without consuming the closing reset, so the chip's color stays balanced.
|
|
39
39
|
*/
|
|
@@ -85,8 +85,9 @@ export class ByteflyHeader implements Component {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* The header is static, so the lines are memoized on everything they read: the
|
|
88
|
-
* width, the theme, the resolved shape, and the key chords
|
|
89
|
-
* `keyText` is re-read every frame, so a live rebind still
|
|
88
|
+
* width, the theme the getter returned, the resolved shape, and the key chords
|
|
89
|
+
* the help rows print. `keyText` is re-read every frame, so a live rebind still
|
|
90
|
+
* shows.
|
|
90
91
|
*/
|
|
91
92
|
render(width: number): string[] {
|
|
92
93
|
const w = Math.max(1, width);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `panel/` (the unified `/bytefly` screen and its
|
|
9
9
|
* `doctor` report), `doctor/` (the `run_doctor` tool and its session-scoped
|
|
10
10
|
* probe runtime), `notify/` (OS notifications for unattended runs), and
|
|
11
|
-
* `update/` (the
|
|
11
|
+
* `update/` (Pi's package list, the host suppression, and the header marker).
|
|
12
12
|
*
|
|
13
13
|
* The header, footer, turn rule, and working line are installed only in
|
|
14
14
|
* interactive (`tui`) sessions, and are always on; the user-message marker,
|
|
@@ -47,7 +47,7 @@ export interface ByteflyDeps {
|
|
|
47
47
|
doctor?: DoctorDeps;
|
|
48
48
|
/** Seams for the notify capability. */
|
|
49
49
|
notify?: NotifyDeps;
|
|
50
|
-
/** Seams for the
|
|
50
|
+
/** Seams for the package-update surface. */
|
|
51
51
|
update?: UpdateDeps;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ export interface ByteflyDeps {
|
|
|
55
55
|
export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void {
|
|
56
56
|
if (!isExtensionEnabled("bytefly")) return;
|
|
57
57
|
|
|
58
|
-
// One store per load: the update check writes
|
|
58
|
+
// One store per load: the update check writes this package's new version and
|
|
59
59
|
// the header reads it, so the marker and every re-install agree.
|
|
60
60
|
const updates = createUpdateState();
|
|
61
61
|
const header = registerHeader(pi, updates);
|
|
@@ -24,14 +24,19 @@ import { NOTICE_CONFIG } from "./config.ts";
|
|
|
24
24
|
/** Severity order, so the rail can carry the worst one. */
|
|
25
25
|
const SEVERITY_RANK: Readonly<Record<NoticeSeverity, number>> = { info: 0, ok: 1, warn: 2, bad: 3 };
|
|
26
26
|
|
|
27
|
+
/** A severity this module knows, or `fallback` for a value from stored data. */
|
|
28
|
+
function knownSeverity(value: NoticeSeverity | undefined, fallback: NoticeSeverity): NoticeSeverity {
|
|
29
|
+
return value !== undefined && Object.hasOwn(SEVERITY_RANK, value) ? value : fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
/** The severity a row draws with, falling back to the notice's own. */
|
|
28
33
|
function severityOf(item: NoticeItem, fallback: NoticeSeverity): NoticeSeverity {
|
|
29
|
-
return item.severity
|
|
34
|
+
return knownSeverity(item.severity, fallback);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/** The worst severity across the headline and its rows; the rail's role. */
|
|
33
38
|
function worstSeverity(details: NoticeDetails): NoticeSeverity {
|
|
34
|
-
const fallback = details.severity
|
|
39
|
+
const fallback = knownSeverity(details.severity, "info");
|
|
35
40
|
let worst: NoticeSeverity = fallback;
|
|
36
41
|
for (const item of details.items ?? []) {
|
|
37
42
|
const severity = severityOf(item, fallback);
|
|
@@ -65,7 +70,7 @@ export function noticeLines(
|
|
|
65
70
|
const title = paint(theme, NOTICE_CONFIG.titleRole, sanitizeLine(details.title));
|
|
66
71
|
const items = details.items ?? [];
|
|
67
72
|
const shown = items.slice(0, NOTICE_CONFIG.maxItems);
|
|
68
|
-
const fallback = details.severity
|
|
73
|
+
const fallback = knownSeverity(details.severity, "info");
|
|
69
74
|
const gap = " ".repeat(GLYPH_GAP);
|
|
70
75
|
|
|
71
76
|
const rows = shown.map((item) => {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* flags then narrow what is announced.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { cleanString, loadConfigSection } from "../../../lib/config.ts";
|
|
17
|
+
import { asBoolean, cleanString, loadConfigSection } from "../../../lib/config.ts";
|
|
18
18
|
import { NOTIFY_METHODS, type NotifyConfig, type NotifyMethod } from "./types.ts";
|
|
19
19
|
|
|
20
20
|
/** A notification title longer than this is clamped; it is a label, not a message. */
|
|
@@ -30,11 +30,6 @@ export const DEFAULT_NOTIFY_CONFIG: NotifyConfig = {
|
|
|
30
30
|
title: "Pi",
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
/** A boolean, or the fallback. */
|
|
34
|
-
function bool(value: unknown, fallback: boolean): boolean {
|
|
35
|
-
return typeof value === "boolean" ? value : fallback;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
/** One of the known methods, or the fallback. */
|
|
39
34
|
function method(value: unknown, fallback: NotifyMethod): NotifyMethod {
|
|
40
35
|
return typeof value === "string" && (NOTIFY_METHODS as readonly string[]).includes(value)
|
|
@@ -46,11 +41,11 @@ function method(value: unknown, fallback: NotifyMethod): NotifyMethod {
|
|
|
46
41
|
export function normalizeNotifyConfig(raw: Record<string, unknown> | undefined, base: NotifyConfig): NotifyConfig {
|
|
47
42
|
if (!raw) return base;
|
|
48
43
|
return {
|
|
49
|
-
enabled:
|
|
50
|
-
turnFinished:
|
|
51
|
-
needsInput:
|
|
52
|
-
jobFinished:
|
|
53
|
-
matchFound:
|
|
44
|
+
enabled: asBoolean(raw.enabled, base.enabled),
|
|
45
|
+
turnFinished: asBoolean(raw.turnFinished, base.turnFinished),
|
|
46
|
+
needsInput: asBoolean(raw.needsInput, base.needsInput),
|
|
47
|
+
jobFinished: asBoolean(raw.jobFinished, base.jobFinished),
|
|
48
|
+
matchFound: asBoolean(raw.matchFound, base.matchFound),
|
|
54
49
|
method: method(raw.method, base.method),
|
|
55
50
|
title: cleanString(raw.title, base.title).slice(0, MAX_TITLE_LENGTH),
|
|
56
51
|
};
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* to smuggle a control character into an escape sequence or a shell argument.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
18
|
import { sanitizeLine } from "../../../lib/format.ts";
|
|
19
19
|
import type { NotifyConfig, NotifyMethod } from "./types.ts";
|
|
20
20
|
|
|
@@ -30,7 +30,7 @@ export interface NotifySeams {
|
|
|
30
30
|
run(command: string, args: string[]): void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** The production seams: this process's platform, environment, stdout, and
|
|
33
|
+
/** The production seams: this process's platform, environment, stdout, and a detached notifier. */
|
|
34
34
|
export function defaultSeams(): NotifySeams {
|
|
35
35
|
return {
|
|
36
36
|
platform: process.platform,
|
|
@@ -38,9 +38,14 @@ export function defaultSeams(): NotifySeams {
|
|
|
38
38
|
stdout: process.stdout,
|
|
39
39
|
run: (command, args) => {
|
|
40
40
|
try {
|
|
41
|
-
|
|
41
|
+
// No pipes and unref'd: the notifier is best-effort and must not keep
|
|
42
|
+
// Pi alive, which `/quit` right after a turn would otherwise wait out
|
|
43
|
+
// (PowerShell's cold start alone is a second).
|
|
44
|
+
const child = spawn(command, args, { windowsHide: true, stdio: "ignore" });
|
|
45
|
+
child.on("error", () => {});
|
|
46
|
+
child.unref();
|
|
42
47
|
} catch {
|
|
43
|
-
//
|
|
48
|
+
// A missing command must not break a turn.
|
|
44
49
|
}
|
|
45
50
|
},
|
|
46
51
|
};
|
|
@@ -89,7 +94,10 @@ function windowsToastScript(title: string, body: string): string {
|
|
|
89
94
|
return [
|
|
90
95
|
`${mgr} > $null`,
|
|
91
96
|
`$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
|
|
92
|
-
`$xml.GetElementsByTagName('text')
|
|
97
|
+
`$nodes = $xml.GetElementsByTagName('text')`,
|
|
98
|
+
// PowerShell 5.1 cannot index the live node list with `[0]`; `.Item(0)`
|
|
99
|
+
// reads the single node without enumerating a collection AppendChild mutates.
|
|
100
|
+
`$nodes.Item(0).AppendChild($xml.CreateTextNode('${powershellLiteral(body)}')) > $null`,
|
|
93
101
|
`[${type}.ToastNotificationManager]::CreateToastNotifier('${powershellLiteral(title)}').Show(${toast})`,
|
|
94
102
|
].join("; ");
|
|
95
103
|
}
|
|
@@ -29,6 +29,7 @@ import type { Discovery } from "../picker/types.ts";
|
|
|
29
29
|
import { formatListing, formatSettingsListing } from "./format.ts";
|
|
30
30
|
import { buildPanelModel, extensionName } from "./join.ts";
|
|
31
31
|
import { createSettingsRuntime, type SettingsRuntime, type SettingsRuntimeDeps } from "./settings-runtime.ts";
|
|
32
|
+
import { shippedThemeNames } from "./themes.ts";
|
|
32
33
|
import { PanelComponent } from "./tui.ts";
|
|
33
34
|
import type { PanelSection } from "./types.ts";
|
|
34
35
|
|
|
@@ -130,6 +131,19 @@ function notifyExtension(
|
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Theme names the row offers: the host's resolved list, narrowed to this
|
|
136
|
+
* package's own palettes. The list is empty outside the TUI (RPC, print, JSON),
|
|
137
|
+
* where the row still shows the stored value.
|
|
138
|
+
*/
|
|
139
|
+
function settingsThemeNames(ctx: ExtensionCommandContext): string[] {
|
|
140
|
+
try {
|
|
141
|
+
return shippedThemeNames(ctx.ui.getAllThemes());
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
/** Register the single `/bytefly` command. */
|
|
134
148
|
export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
135
149
|
const discover = deps.discover ?? discoverExtensions;
|
|
@@ -146,6 +160,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
146
160
|
const cwd = ctx.cwd ?? process.cwd();
|
|
147
161
|
const agentDir = getAgentDir();
|
|
148
162
|
const trusted = ctx.isProjectTrusted();
|
|
163
|
+
const themes = settingsThemeNames(ctx);
|
|
149
164
|
// The mounted screen owns its feedback: a refusal or a failed write belongs in
|
|
150
165
|
// the frame, not appended to the transcript behind it. Before the screen
|
|
151
166
|
// exists (the non-TUI settings path, a pre-mount failure) the transcript is
|
|
@@ -171,7 +186,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
171
186
|
// packages or probing the doctor.
|
|
172
187
|
if (section === "settings" && ctx.mode !== "tui") {
|
|
173
188
|
const stores = createSettingsStores(cwd, agentDir, trusted);
|
|
174
|
-
const settings = makeSettings({ stores, cwd, agentDir, trusted, onError: settingsNotice });
|
|
189
|
+
const settings = makeSettings({ stores, cwd, agentDir, trusted, themes, onError: settingsNotice });
|
|
175
190
|
ctx.ui.notify(formatSettingsListing(settings.views("global")), "info");
|
|
176
191
|
return;
|
|
177
192
|
}
|
|
@@ -189,6 +204,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
189
204
|
cwd,
|
|
190
205
|
agentDir,
|
|
191
206
|
trusted: discovery.trusted,
|
|
207
|
+
themes,
|
|
192
208
|
onError: settingsNotice,
|
|
193
209
|
});
|
|
194
210
|
|
|
@@ -236,6 +252,9 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
236
252
|
await withRailsSuppressed(pi, () =>
|
|
237
253
|
ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
238
254
|
const unsubscribe = runtime.onChange(() => tui.requestRender());
|
|
255
|
+
// The settings runtime re-reads its snapshot after a write; without a
|
|
256
|
+
// subscriber that repaint depends on every write chain's own callback.
|
|
257
|
+
const unsubscribeSettings = settings.onChange(() => tui.requestRender());
|
|
239
258
|
const component = new PanelComponent({
|
|
240
259
|
runtime,
|
|
241
260
|
reports: () => reports,
|
|
@@ -257,6 +276,7 @@ export function registerPanel(pi: ExtensionAPI, deps: PanelCommandDeps): void {
|
|
|
257
276
|
// A late error falls back to the transcript, never a disposed screen.
|
|
258
277
|
panel = undefined;
|
|
259
278
|
unsubscribe();
|
|
279
|
+
unsubscribeSettings();
|
|
260
280
|
done();
|
|
261
281
|
},
|
|
262
282
|
});
|
|
@@ -158,8 +158,10 @@ interface RowBadge {
|
|
|
158
158
|
* The row's badge. Settings is the enablement axis the checkbox already shows,
|
|
159
159
|
* so an off extension gets one verdict (`off`) instead of a health word that
|
|
160
160
|
* contradicts it. `PI_DISABLED_EXTENSIONS` is a different axis, so it is named
|
|
161
|
-
* rather than sharing the word: `env disabled`.
|
|
162
|
-
*
|
|
161
|
+
* rather than sharing the word: `env disabled`. The glyph marks what needs
|
|
162
|
+
* attention — a warning, a miss, a disable — so a row with nothing to report
|
|
163
|
+
* carries no glyph at all, and the column reads as one shape instead of a
|
|
164
|
+
* checkmark that comes and goes.
|
|
163
165
|
*/
|
|
164
166
|
export function rowBadge(row: PanelRow): RowBadge {
|
|
165
167
|
if (row.item && !row.item.enabled) return { glyph: GLYPHS.disabled, role: "faint", text: "off" };
|
|
@@ -167,6 +169,7 @@ export function rowBadge(row: PanelRow): RowBadge {
|
|
|
167
169
|
if (!report) return { role: "faint" };
|
|
168
170
|
if (!report.enabled) return { glyph: GLYPHS.disabled, role: "faint", text: "env disabled" };
|
|
169
171
|
const severity = reportSeverity(report);
|
|
172
|
+
if (severity === "ok") return { role: "faint", text: reportSummary(report) };
|
|
170
173
|
return { glyph: severityGlyphChar(severity), role: severityRole(severity), text: reportSummary(report) };
|
|
171
174
|
}
|
|
172
175
|
|