@gavin-hu/my-pi-agent 0.13.1 → 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 +66 -41
- package/README.zh-CN.md +53 -36
- package/extensions/bytefly/README.md +321 -186
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/config.ts +4 -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/config.ts +3 -2
- 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 +33 -10
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +6 -6
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +7 -12
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +22 -2
- package/extensions/bytefly/panel/format.ts +17 -21
- package/extensions/bytefly/panel/settings-runtime.ts +86 -22
- package/extensions/bytefly/panel/settings.ts +279 -46
- 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 +98 -13
- package/extensions/bytefly/panel/tui.ts +176 -166
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +17 -6
- package/extensions/bytefly/separator/config.ts +15 -0
- package/extensions/bytefly/separator/format.ts +60 -6
- package/extensions/bytefly/separator/register.ts +68 -9
- package/extensions/bytefly/separator/types.ts +8 -0
- 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/bytefly/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- 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 +3 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +106 -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 +118 -8
- 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 +62 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +116 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +25 -0
- package/extensions/subagent/run.ts +113 -49
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +23 -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 +16 -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 +3 -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 +83 -12
- package/lib/errors.ts +12 -0
- package/lib/format.ts +56 -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 +23 -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/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
- 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;
|
|
@@ -14,6 +14,9 @@ import { clampInteger, loadConfigSection } from "../../../lib/config.ts";
|
|
|
14
14
|
/** How long a single probe may take before it counts as unusable. */
|
|
15
15
|
const DEFAULT_TIMEOUT_MS = 2_000;
|
|
16
16
|
|
|
17
|
+
/** The probe deadline range, in milliseconds; the panel's editor offers the same bounds. */
|
|
18
|
+
export const TIMEOUT_MS_RANGE = { min: 100, max: 30_000 } as const;
|
|
19
|
+
|
|
17
20
|
export interface DoctorConfig {
|
|
18
21
|
/** Probe enabled extensions at session start and warn when one is broken. */
|
|
19
22
|
proactive: boolean;
|
|
@@ -34,7 +37,7 @@ export function normalizeConfig(raw: Record<string, unknown> | undefined, base:
|
|
|
34
37
|
if (!raw) return base;
|
|
35
38
|
return {
|
|
36
39
|
proactive: typeof raw.proactive === "boolean" ? raw.proactive : base.proactive,
|
|
37
|
-
timeoutMs: clampInteger(raw.timeoutMs, base.timeoutMs,
|
|
40
|
+
timeoutMs: clampInteger(raw.timeoutMs, base.timeoutMs, TIMEOUT_MS_RANGE.min, TIMEOUT_MS_RANGE.max),
|
|
38
41
|
skip: Array.isArray(raw.skip)
|
|
39
42
|
? raw.skip.filter((name): name is string => typeof name === "string" && name.trim().length > 0)
|
|
40
43
|
: base.skip,
|
|
@@ -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,
|
|
@@ -101,9 +101,10 @@ export const DEFAULT_BYTEFLY_CONFIG: ByteflyConfig = {
|
|
|
101
101
|
budget: 0,
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
/** The highest `currency.decimals` the screen and the loader both accept. */
|
|
105
|
+
export const MAX_DECIMALS = 4;
|
|
105
106
|
/** Upper bound for `budget`; large enough for any real session, small enough to clamp a typo. */
|
|
106
|
-
const MAX_BUDGET = 10_000_000;
|
|
107
|
+
export const MAX_BUDGET = 10_000_000;
|
|
107
108
|
const MAX_RATE = 1_000_000;
|
|
108
109
|
const MAX_RATE_KEYS = 200;
|
|
109
110
|
const MAX_WINDOWS = 16;
|
|
@@ -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
|
*/
|
|
@@ -18,9 +18,9 @@ import { BODY_INDENT } from "../../../lib/tui/indent.ts";
|
|
|
18
18
|
import { RULE_GLYPH } from "../../../lib/tui/rules.ts";
|
|
19
19
|
import { paint } from "../../../lib/tui/tokens.ts";
|
|
20
20
|
import { updateMarker } from "../update/format.ts";
|
|
21
|
-
import { HEADER_CONFIG, type HelpEntry } from "./config.ts";
|
|
21
|
+
import { HEADER_CONFIG, type HeaderHeight, type HelpEntry } from "./config.ts";
|
|
22
22
|
|
|
23
|
-
/** The header's shape
|
|
23
|
+
/** The header's shape: everything but the live terminal height is resolved once per apply. */
|
|
24
24
|
export interface HeaderView {
|
|
25
25
|
/** Print Pi's version on the agent line. */
|
|
26
26
|
showVersion: boolean;
|
|
@@ -28,6 +28,8 @@ export interface HeaderView {
|
|
|
28
28
|
halfBlocks: boolean;
|
|
29
29
|
/** Columns of left margin: Pi's output pad, so the nameplate sits on the transcript margin. */
|
|
30
30
|
pad: number;
|
|
31
|
+
/** Terminal rows, when the host reports them; short terminals shed help rows. */
|
|
32
|
+
rows?: number;
|
|
31
33
|
/** A newer published version of this package, when one is known. */
|
|
32
34
|
latest?: string;
|
|
33
35
|
}
|
|
@@ -35,6 +37,15 @@ export interface HeaderView {
|
|
|
35
37
|
/** The mark block is four columns on both rows, so the identity column aligns. */
|
|
36
38
|
const MARK_WIDTH = 4;
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The tallest shape whose threshold the terminal meets. An unknown or impossible
|
|
42
|
+
* height keeps the full header, so a host without terminal rows still reads well.
|
|
43
|
+
*/
|
|
44
|
+
function heightShape(rows: number | undefined): HeaderHeight {
|
|
45
|
+
if (rows === undefined || !Number.isFinite(rows) || rows <= 0) return HEADER_CONFIG.heights[0];
|
|
46
|
+
return HEADER_CONFIG.heights.find((shape) => rows >= shape.minRows) ?? HEADER_CONFIG.heights[0];
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
/** Pad a themed mark line to the block width with plain spaces. */
|
|
39
50
|
function markBlock(line: string): string {
|
|
40
51
|
return line + " ".repeat(Math.max(0, MARK_WIDTH - visibleWidth(line)));
|
|
@@ -74,14 +85,15 @@ export class ByteflyHeader implements Component {
|
|
|
74
85
|
|
|
75
86
|
/**
|
|
76
87
|
* The header is static, so the lines are memoized on everything they read: the
|
|
77
|
-
* width, the theme, the resolved shape, and the key chords
|
|
78
|
-
* `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.
|
|
79
91
|
*/
|
|
80
92
|
render(width: number): string[] {
|
|
81
93
|
const w = Math.max(1, width);
|
|
82
94
|
const theme = this.getTheme();
|
|
83
95
|
const view = this.view();
|
|
84
|
-
const viewKey = `${view.showVersion}:${view.halfBlocks}:${view.pad}:${view.latest ?? ""}`;
|
|
96
|
+
const viewKey = `${view.showVersion}:${view.halfBlocks}:${view.pad}:${view.latest ?? ""}:${view.rows ?? ""}`;
|
|
85
97
|
const keys = this.keySignature();
|
|
86
98
|
if (
|
|
87
99
|
this.cachedWidth === w &&
|
|
@@ -111,6 +123,7 @@ export class ByteflyHeader implements Component {
|
|
|
111
123
|
private build(w: number, theme: Theme, view: HeaderView): string[] {
|
|
112
124
|
const { nameRole, chromeRole, detailRole, updateRole, chromeTagline, chromeName, chromeVersion, help } =
|
|
113
125
|
HEADER_CONFIG;
|
|
126
|
+
const shape = heightShape(view.rows);
|
|
114
127
|
const padWidth = Math.max(0, Math.min(Math.floor(view.pad), Math.max(0, w - 1)));
|
|
115
128
|
const pad = " ".repeat(padWidth);
|
|
116
129
|
const [markTop, markBottom] = markLines(theme, view.halfBlocks);
|
|
@@ -145,13 +158,17 @@ export class ByteflyHeader implements Component {
|
|
|
145
158
|
|
|
146
159
|
// The header owns its leading blank: Pi mounts the built-in header inside
|
|
147
160
|
// `Spacer(1)`s, but the `quietStartup: true` branch that hands this header the
|
|
148
|
-
// screen adds none, and `setHeader` swaps only the header child.
|
|
161
|
+
// screen adds none, and `setHeader` swaps only the header child. A short
|
|
162
|
+
// terminal gives that row back, then sheds whole help groups.
|
|
149
163
|
return [
|
|
150
|
-
"",
|
|
164
|
+
...(shape.blank ? [""] : []),
|
|
151
165
|
this.identityLine(pad, markTop, agentCell, w),
|
|
152
166
|
this.identityLine(pad, markBottom, chromeCell, w),
|
|
153
167
|
this.rule(theme, w, pad),
|
|
154
|
-
...
|
|
168
|
+
...shape.helpRows.flatMap((index) => {
|
|
169
|
+
const group = help[index];
|
|
170
|
+
return group ? [this.helpLine(theme, w, group, padWidth)] : [];
|
|
171
|
+
}),
|
|
155
172
|
];
|
|
156
173
|
}
|
|
157
174
|
|
|
@@ -193,6 +210,12 @@ export class ByteflyHeader implements Component {
|
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
/** Header factory passed to `ctx.ui.setHeader`. */
|
|
196
|
-
export function createHeader(
|
|
197
|
-
|
|
213
|
+
export function createHeader(
|
|
214
|
+
getTheme: () => Theme,
|
|
215
|
+
view: (rows: number | undefined) => HeaderView,
|
|
216
|
+
): (tui: TUI, theme: Theme) => Component {
|
|
217
|
+
// Read the height on every render, so a terminal resize reflows the header
|
|
218
|
+
// instead of keeping the shape it was installed with. Tests may pass a bare
|
|
219
|
+
// object as the host, so the terminal is optional at the boundary.
|
|
220
|
+
return (tui) => new ByteflyHeader(getTheme, () => view(tui.terminal?.rows), VERSION);
|
|
198
221
|
}
|
|
@@ -19,6 +19,16 @@ export type HelpEntry =
|
|
|
19
19
|
| { readonly binding: AppKeybinding; readonly description: string }
|
|
20
20
|
| { readonly raw: string; readonly description: string };
|
|
21
21
|
|
|
22
|
+
/** How much of the help block a terminal height can carry. */
|
|
23
|
+
export interface HeaderHeight {
|
|
24
|
+
/** Smallest terminal height, in rows, this shape applies to. Taller tiers come first. */
|
|
25
|
+
readonly minRows: number;
|
|
26
|
+
/** Help groups to print, by index in `help`; a group is dropped whole. */
|
|
27
|
+
readonly helpRows: readonly number[];
|
|
28
|
+
/** Open with a blank row above the nameplate. */
|
|
29
|
+
readonly blank: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
/** The header's shape and content. */
|
|
23
33
|
export interface HeaderConfig {
|
|
24
34
|
/** The agent's name, shown before its version. */
|
|
@@ -49,6 +59,12 @@ export interface HeaderConfig {
|
|
|
49
59
|
readonly chromeVersion: string;
|
|
50
60
|
/** Grouped help rows, printed one line each under the rule. */
|
|
51
61
|
readonly help: readonly (readonly HelpEntry[])[];
|
|
62
|
+
/**
|
|
63
|
+
* The height ladder, tallest first: the first tier the terminal meets decides
|
|
64
|
+
* which help groups print and whether the leading blank row survives. The
|
|
65
|
+
* floor is `minRows: 0`, so a short terminal keeps the nameplate and rule.
|
|
66
|
+
*/
|
|
67
|
+
readonly heights: readonly HeaderHeight[];
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
export const HEADER_CONFIG: HeaderConfig = {
|
|
@@ -89,4 +105,10 @@ export const HEADER_CONFIG: HeaderConfig = {
|
|
|
89
105
|
{ binding: "app.clipboard.pasteImage", description: "paste files" },
|
|
90
106
|
],
|
|
91
107
|
],
|
|
108
|
+
heights: [
|
|
109
|
+
{ minRows: 28, helpRows: [0, 1, 2], blank: true },
|
|
110
|
+
{ minRows: 22, helpRows: [0, 2], blank: true },
|
|
111
|
+
{ minRows: 16, helpRows: [2], blank: true },
|
|
112
|
+
{ minRows: 0, helpRows: [], blank: false },
|
|
113
|
+
],
|
|
92
114
|
};
|
|
@@ -55,11 +55,12 @@ export function registerHeader(pi: ExtensionAPI, updates: UpdateSource = createU
|
|
|
55
55
|
// value the tool frame and the notice inset by. An older host without
|
|
56
56
|
// `getSettings` keeps the default margin.
|
|
57
57
|
const pad = pi.getSettings?.().outputPad ?? DEFAULT_HOST_PAD;
|
|
58
|
-
const view = (): HeaderView => ({
|
|
58
|
+
const view = (rows: number | undefined): HeaderView => ({
|
|
59
59
|
showVersion: HEADER_CONFIG.showVersion,
|
|
60
60
|
halfBlocks,
|
|
61
61
|
pad,
|
|
62
62
|
latest: updates.latest(),
|
|
63
|
+
rows,
|
|
63
64
|
});
|
|
64
65
|
ctx.ui.setHeader(createHeader(() => ctx.ui.theme, view));
|
|
65
66
|
};
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* (the two-line, width-adaptive status bar with its price calculation),
|
|
5
5
|
* `message/` (the user-message nameplate), `notice/` (the shared transcript
|
|
6
6
|
* notice renderer), `block/` (the unified tool-block frame), `separator/` (the
|
|
7
|
-
* transcript turn rule), `working/` (the live
|
|
8
|
-
*
|
|
7
|
+
* transcript turn rule), `working/` (the live run line on Pi's working row),
|
|
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
|
-
* The header, footer, turn rule, and working
|
|
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,
|
|
15
15
|
* notice, and tool blocks register at load and take effect wherever a
|
|
16
16
|
* transcript renders. `pi config` turns bytefly's chrome off. The panel and doctor work
|
|
@@ -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) => {
|