@gavin-hu/my-pi-agent 0.11.4 → 0.12.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 +13 -8
- package/README.zh-CN.md +5 -3
- package/assets/chrome.png +0 -0
- package/assets/plan-review.png +0 -0
- package/extensions/bytefly/README.md +291 -207
- package/extensions/bytefly/doctor/runtime.ts +16 -0
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/segments.ts +3 -2
- package/extensions/bytefly/footer/types.ts +1 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +13 -3
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +201 -38
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +32 -0
- package/extensions/bytefly/update/register.ts +72 -0
- package/extensions/bytefly/update/state.ts +28 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +2 -2
- package/extensions/document/tool.ts +1 -3
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +39 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/memory/README.md +1 -1
- package/extensions/plan/README.md +24 -8
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +18 -12
- package/extensions/plan/plans.ts +1 -1
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +243 -0
- package/extensions/plan/runtime.ts +4 -0
- package/extensions/plan/steps.ts +34 -23
- package/extensions/plan/todos.ts +5 -58
- package/extensions/plan/tools.ts +66 -222
- package/extensions/plan/tui.ts +17 -9
- package/extensions/plan/types.ts +3 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +5 -0
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +100 -36
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/subagent/index.ts +0 -2
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/fetch/tool.ts +1 -3
- package/extensions/web-access/search/tool.ts +1 -3
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/lib/README.md +2 -1
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/requirements.ts +76 -2
- package/lib/todo-state.ts +62 -0
- package/lib/tui/glyphs.ts +1 -0
- package/package.json +1 -1
- package/assets/bytefly.schema.json +0 -785
|
@@ -19,11 +19,25 @@ import {
|
|
|
19
19
|
type ExtensionReport,
|
|
20
20
|
type ProbeSeams,
|
|
21
21
|
} from "../../../lib/requirements.ts";
|
|
22
|
+
import { loadNotifyConfig } from "../notify/config.ts";
|
|
22
23
|
import { DEFAULT_CONFIG, type DoctorConfig } from "./config.ts";
|
|
23
24
|
|
|
24
25
|
/** Seams a test may replace; everything else is built from the host. */
|
|
25
26
|
export type DoctorSeamOverrides = Partial<ProbeSeams>;
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The notify capability's configured delivery, read fresh from the document.
|
|
30
|
+
*
|
|
31
|
+
* Reading at probe time rather than from the session's snapshot means a
|
|
32
|
+
* corrected method shows up on the next `r` instead of only after a reload.
|
|
33
|
+
*/
|
|
34
|
+
export function notifySeam(cwd: string): () => { enabled: boolean; method: string } {
|
|
35
|
+
return () => {
|
|
36
|
+
const notify = loadNotifyConfig(cwd);
|
|
37
|
+
return { enabled: notify.enabled, method: notify.method };
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
export interface DoctorRuntime {
|
|
28
42
|
/** Point the runtime at a session: its cwd and effective config. */
|
|
29
43
|
configure(options: { cwd: string; config: DoctorConfig }): void;
|
|
@@ -56,6 +70,8 @@ export function createDoctorRuntime(
|
|
|
56
70
|
dirs: { agentDir: getAgentDir(), cwd, configDirName: CONFIG_DIR_NAME },
|
|
57
71
|
platform: process.platform,
|
|
58
72
|
isEnabled: (extension) => isExtensionEnabled(extension) && !config.skip.includes(extension),
|
|
73
|
+
notify: notifySeam(cwd),
|
|
74
|
+
isTTY: process.stdout.isTTY === true,
|
|
59
75
|
...deps.seams,
|
|
60
76
|
});
|
|
61
77
|
|
|
@@ -57,9 +57,7 @@ export function registerTools(pi: ExtensionAPI, runtime: DoctorRuntime): void {
|
|
|
57
57
|
],
|
|
58
58
|
parameters: DoctorParams,
|
|
59
59
|
outputSchema: DoctorOutput,
|
|
60
|
-
|
|
61
|
-
defaultActive: true,
|
|
62
|
-
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
60
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
63
61
|
|
|
64
62
|
async execute(_toolCallId, params, _signal, onUpdate, _ctx) {
|
|
65
63
|
const args = params as DoctorArgs;
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The built-in table is DeepSeek's published yuan pricing, with the peak
|
|
11
11
|
* schedule that doubles the price during peak hours. It is the default value of
|
|
12
|
-
* `rates
|
|
13
|
-
*
|
|
12
|
+
* `rates`: `currency.code` chooses which table that is, and the peak schedule
|
|
13
|
+
* applies to both. Only an unmatched model differs — `CNY` contributes no cost
|
|
14
|
+
* for it, while `USD` adds Pi's recorded dollars.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { asRecord, clampInteger, clampNumber, cleanString, loadConfigSection } from "../../../../lib/config.ts";
|
|
17
18
|
import { parseWindow } from "./schedule.ts";
|
|
18
|
-
import type { ByteflyConfig, Currency, ModelRates, PeakSchedule, PeakWindow } from "./types.ts";
|
|
19
|
+
import type { ByteflyConfig, Currency, CurrencyCode, ModelRates, PeakSchedule, PeakWindow } from "./types.ts";
|
|
19
20
|
|
|
20
21
|
/** Monday to Friday, the days DeepSeek charges its peak rate. */
|
|
21
22
|
const WEEKDAYS: readonly number[] = [1, 2, 3, 4, 5];
|
|
@@ -26,8 +27,8 @@ const WEEKDAYS: readonly number[] = [1, 2, 3, 4, 5];
|
|
|
26
27
|
* Pi's catalog prices every call in dollars at one flat rate, which is wrong
|
|
27
28
|
* twice over for DeepSeek: the bill is in yuan, and peak hours cost double.
|
|
28
29
|
* These are the published peak figures; `DEFAULT_PEAK` supplies the tariff. A
|
|
29
|
-
* `bytefly.json` entry with the same key replaces one of these
|
|
30
|
-
*
|
|
30
|
+
* `bytefly.json` entry with the same key replaces one of these. A model with no
|
|
31
|
+
* matching entry contributes no cost; USD mode uses Pi's recorded cost instead.
|
|
31
32
|
*
|
|
32
33
|
* Source: <https://api-docs.deepseek.com/quick_start/pricing>.
|
|
33
34
|
*/
|
|
@@ -36,6 +37,24 @@ export const DEFAULT_RATES: Readonly<Record<string, ModelRates>> = {
|
|
|
36
37
|
"deepseek/deepseek-v4-pro": { input: 9, output: 27, cacheRead: 0.3, cacheWrite: 0, offPeakMultiplier: 0.5 },
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The same two models in DeepSeek's published dollars, per million tokens at the
|
|
42
|
+
* peak rate; `offPeakMultiplier` halves them off-peak. The published table has no
|
|
43
|
+
* cache-write line, so `cacheWrite` is 0 here exactly as in the yuan table.
|
|
44
|
+
*
|
|
45
|
+
* Source: <https://api-docs.deepseek.com/quick_start/pricing> (USD table).
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_RATES_USD: Readonly<Record<string, ModelRates>> = {
|
|
48
|
+
"deepseek/deepseek-flash": { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0, offPeakMultiplier: 0.5 },
|
|
49
|
+
"deepseek/deepseek-v4-pro": { input: 1.32, output: 3.96, cacheRead: 0.044, cacheWrite: 0, offPeakMultiplier: 0.5 },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The built-in table for each currency: `rates` are stated in the selected one. */
|
|
53
|
+
export const DEFAULT_RATES_BY_CURRENCY: Readonly<Record<CurrencyCode, Readonly<Record<string, ModelRates>>>> = {
|
|
54
|
+
CNY: DEFAULT_RATES,
|
|
55
|
+
USD: DEFAULT_RATES_USD,
|
|
56
|
+
};
|
|
57
|
+
|
|
39
58
|
/**
|
|
40
59
|
* DeepSeek's peak hours, in the zone the provider states them in: 01:00-04:00
|
|
41
60
|
* and 06:00-10:00 UTC on weekdays, which is 09:00-12:00 and 14:00-18:00 in
|
|
@@ -51,17 +70,24 @@ export const DEFAULT_PEAK: PeakSchedule = {
|
|
|
51
70
|
],
|
|
52
71
|
};
|
|
53
72
|
|
|
54
|
-
/**
|
|
55
|
-
export const
|
|
73
|
+
/** The symbol each algorithm prints. It is not configurable, so it can never disagree with the code. */
|
|
74
|
+
export const CURRENCY_SYMBOLS: Readonly<Record<CurrencyCode, string>> = { CNY: "¥", USD: "$" };
|
|
75
|
+
|
|
76
|
+
/** The default precision each algorithm supplies when a config sets only its code. */
|
|
77
|
+
export const CURRENCY_DEFAULTS: Readonly<Record<CurrencyCode, Currency>> = {
|
|
78
|
+
CNY: { code: "CNY", decimals: 2 },
|
|
79
|
+
USD: { code: "USD", decimals: 3 },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Yuan, the currency `DEFAULT_RATES` is stated in. */
|
|
83
|
+
export const DEFAULT_CURRENCY: Currency = CURRENCY_DEFAULTS.CNY;
|
|
56
84
|
|
|
57
85
|
export const DEFAULT_BYTEFLY_CONFIG: ByteflyConfig = {
|
|
58
86
|
currency: DEFAULT_CURRENCY,
|
|
59
|
-
fallbackToRecordedCost: true,
|
|
60
87
|
rates: DEFAULT_RATES,
|
|
61
88
|
peak: DEFAULT_PEAK,
|
|
62
89
|
};
|
|
63
90
|
|
|
64
|
-
const MAX_SYMBOL_LENGTH = 4;
|
|
65
91
|
const MAX_DECIMALS = 4;
|
|
66
92
|
const MAX_RATE = 1_000_000;
|
|
67
93
|
const MAX_RATE_KEYS = 200;
|
|
@@ -77,12 +103,22 @@ function acceptsTimeZone(timezone: string): boolean {
|
|
|
77
103
|
}
|
|
78
104
|
}
|
|
79
105
|
|
|
106
|
+
/** A supported ISO code, or the base code for a missing/mistyped value. */
|
|
107
|
+
function currencyCode(raw: unknown, base: CurrencyCode): CurrencyCode {
|
|
108
|
+
const text = typeof raw === "string" ? raw.trim().toUpperCase() : "";
|
|
109
|
+
return text === "CNY" || text === "USD" ? text : base;
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
function currencyValue(raw: unknown, base: Currency): Currency {
|
|
81
113
|
const value = asRecord(raw);
|
|
82
114
|
if (!value) return base;
|
|
115
|
+
const code = currencyCode(value.code, base.code);
|
|
116
|
+
// A changed code supplies its own default precision unless this layer
|
|
117
|
+
// overrides it; an unchanged code keeps the precision customized above it.
|
|
118
|
+
const display = code !== base.code ? CURRENCY_DEFAULTS[code] : base;
|
|
83
119
|
return {
|
|
84
|
-
|
|
85
|
-
decimals: clampInteger(value.decimals,
|
|
120
|
+
code,
|
|
121
|
+
decimals: clampInteger(value.decimals, display.decimals, 0, MAX_DECIMALS),
|
|
86
122
|
};
|
|
87
123
|
}
|
|
88
124
|
|
|
@@ -137,11 +173,14 @@ function peakValue(raw: unknown, base: PeakSchedule): PeakSchedule {
|
|
|
137
173
|
/** Validate a raw config object over `base`. Total: never throws, never guesses. */
|
|
138
174
|
export function normalizeByteflyConfig(raw: Record<string, unknown> | undefined, base: ByteflyConfig): ByteflyConfig {
|
|
139
175
|
if (!raw) return base;
|
|
176
|
+
const currency = currencyValue(raw.currency, base.currency);
|
|
177
|
+
// `rates` are stated in the selected currency, so a layer that switches the
|
|
178
|
+
// code starts from that currency's table instead of reinterpreting the old
|
|
179
|
+
// numbers in the new unit. A layer that sets both merges onto the new table.
|
|
180
|
+
const rateBase = currency.code === base.currency.code ? base.rates : DEFAULT_RATES_BY_CURRENCY[currency.code];
|
|
140
181
|
return {
|
|
141
|
-
currency
|
|
142
|
-
|
|
143
|
-
typeof raw.fallbackToRecordedCost === "boolean" ? raw.fallbackToRecordedCost : base.fallbackToRecordedCost,
|
|
144
|
-
rates: ratesValue(raw.rates, base.rates),
|
|
182
|
+
currency,
|
|
183
|
+
rates: ratesValue(raw.rates, rateBase),
|
|
145
184
|
peak: peakValue(raw.peak, base.peak),
|
|
146
185
|
};
|
|
147
186
|
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Price usage records from configured rates.
|
|
3
3
|
*
|
|
4
4
|
* `createPricer` owns the two things that must not be rebuilt per record: the
|
|
5
|
-
* matching rates and the timezone formatter.
|
|
6
|
-
*
|
|
5
|
+
* matching rates and the timezone formatter. `currency.code` chooses the
|
|
6
|
+
* algorithm: CNY prices matched records from `rates` and `peak`, while USD
|
|
7
|
+
* totals Pi's recorded dollar cost and ignores both. A CNY-mode record with no
|
|
8
|
+
* matching rate contributes nothing: an unpriced model shows no price at all.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { emptyUsage } from "../collect/entries.ts";
|
|
@@ -32,6 +34,9 @@ function tokenCost(record: UsageRecord, rates: ModelRates): number {
|
|
|
32
34
|
* its price always describe the same set of records.
|
|
33
35
|
*/
|
|
34
36
|
export function createPricer(config: ByteflyConfig): (records: readonly UsageRecord[]) => UsageTotals {
|
|
37
|
+
// Both currencies price from `rates` and the tariff; they differ only in what
|
|
38
|
+
// an unmatched model falls back to, and Pi's recorded cost is already dollars.
|
|
39
|
+
const dollars = config.currency.code === "USD";
|
|
35
40
|
// Building a formatter is the expensive part, and only a schedule needs one.
|
|
36
41
|
const format = config.peak.windows.length > 0 ? peakFormatter(config.peak.timezone) : null;
|
|
37
42
|
|
|
@@ -42,16 +47,24 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
|
|
|
42
47
|
totals.output += record.output;
|
|
43
48
|
totals.cacheRead += record.cacheRead;
|
|
44
49
|
totals.cacheWrite += record.cacheWrite;
|
|
45
|
-
|
|
50
|
+
const rates = findRates(config.rates, record.provider, record.model);
|
|
51
|
+
if (rates !== undefined) {
|
|
52
|
+
totals.cost += ratedCost(config, format, record, rates);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (dollars) totals.cost += record.recordedCost;
|
|
46
56
|
}
|
|
47
57
|
return totals;
|
|
48
58
|
};
|
|
49
59
|
}
|
|
50
60
|
|
|
51
|
-
/** One record's cost
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
/** One record's cost at its matched rates, after the peak schedule. */
|
|
62
|
+
function ratedCost(
|
|
63
|
+
config: ByteflyConfig,
|
|
64
|
+
format: Intl.DateTimeFormat | null,
|
|
65
|
+
record: UsageRecord,
|
|
66
|
+
rates: ModelRates,
|
|
67
|
+
): number {
|
|
55
68
|
const cost = tokenCost(record, rates);
|
|
56
69
|
// An undated record is never discounted: we cannot tell which tariff applied.
|
|
57
70
|
if (format === null || record.at === null) return cost;
|
|
@@ -6,15 +6,23 @@
|
|
|
6
6
|
* `../types.ts`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/** The two pricing algorithms, named by the currency they total. */
|
|
10
|
+
export type CurrencyCode = "CNY" | "USD";
|
|
11
|
+
|
|
12
|
+
/** How a cost is calculated and rendered. */
|
|
10
13
|
export interface Currency {
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Which table `rates` starts from, and what an unmatched model contributes:
|
|
16
|
+
* both currencies price a matched record from `rates` and the peak schedule,
|
|
17
|
+
* while `CNY` gives an unmatched model no cost and `USD` adds Pi's recorded
|
|
18
|
+
* dollars for it.
|
|
19
|
+
*/
|
|
20
|
+
code: CurrencyCode;
|
|
21
|
+
/** Digits after the decimal point. The symbol follows from `code`. */
|
|
14
22
|
decimals: number;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
/** Per-million-token rates for one model,
|
|
25
|
+
/** Per-million-token rates for one model, in yuan; used by the CNY algorithm. */
|
|
18
26
|
export interface ModelRates {
|
|
19
27
|
input: number;
|
|
20
28
|
output: number;
|
|
@@ -45,8 +53,6 @@ export interface PeakSchedule {
|
|
|
45
53
|
/** Everything the price calculation is configured with. */
|
|
46
54
|
export interface ByteflyConfig {
|
|
47
55
|
currency: Currency;
|
|
48
|
-
/** Fall back to Pi's recorded cost for a record that no rate matches. */
|
|
49
|
-
fallbackToRecordedCost: boolean;
|
|
50
56
|
/** Rates keyed by `provider/model`, matched through wildcards. */
|
|
51
57
|
rates: Readonly<Record<string, ModelRates>>;
|
|
52
58
|
peak: PeakSchedule;
|
|
@@ -11,7 +11,7 @@ import { shortenPath } from "../../../../lib/format.ts";
|
|
|
11
11
|
import { gaugeRole } from "../../../../lib/tui/gauge.ts";
|
|
12
12
|
import type { TuiRole } from "../../../../lib/tui/tokens.ts";
|
|
13
13
|
import { CONFIG } from "../config.ts";
|
|
14
|
-
import { DEFAULT_CURRENCY } from "../price/config.ts";
|
|
14
|
+
import { CURRENCY_SYMBOLS, DEFAULT_CURRENCY } from "../price/config.ts";
|
|
15
15
|
import type { Currency } from "../price/types.ts";
|
|
16
16
|
|
|
17
17
|
/** Theme tokens for the reasoning-effort indicator. */
|
|
@@ -31,7 +31,8 @@ export type ThinkingColor =
|
|
|
31
31
|
* `¥<0.01` — so a small cost is never reported as free.
|
|
32
32
|
*/
|
|
33
33
|
export function formatCost(cost: number, currency: Currency = DEFAULT_CURRENCY): string {
|
|
34
|
-
const {
|
|
34
|
+
const { decimals } = currency;
|
|
35
|
+
const symbol = CURRENCY_SYMBOLS[currency.code];
|
|
35
36
|
if (!Number.isFinite(cost) || cost <= 0) return `${symbol}${(0).toFixed(decimals)}`;
|
|
36
37
|
const minimum = 10 ** -decimals;
|
|
37
38
|
if (cost < minimum) return `${symbol}<${minimum.toFixed(decimals)}`;
|
|
@@ -171,13 +171,14 @@ export function windowSegment(snapshot: StatusSnapshot, theme: Theme): Segment |
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export function costSegment(snapshot: StatusSnapshot, theme: Theme): Segment | null {
|
|
174
|
-
|
|
174
|
+
const { cost } = snapshot.usage;
|
|
175
|
+
if (cost === 0) return null;
|
|
175
176
|
return {
|
|
176
177
|
id: "cost",
|
|
177
178
|
weight: 2,
|
|
178
179
|
droppable: true,
|
|
179
180
|
separator: faint(theme, CONFIG.separators.item),
|
|
180
|
-
forms: [paint(theme, "meta", formatCost(
|
|
181
|
+
forms: [paint(theme, "meta", formatCost(cost, snapshot.currency))],
|
|
181
182
|
};
|
|
182
183
|
}
|
|
183
184
|
|
|
@@ -17,6 +17,7 @@ import { keyHintText } from "../../../lib/tui/glyphs.ts";
|
|
|
17
17
|
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
|
+
import { updateMarker } from "../update/format.ts";
|
|
20
21
|
import { HEADER_CONFIG, type HelpEntry } from "./config.ts";
|
|
21
22
|
|
|
22
23
|
/** The header's shape, resolved once per apply so `render` stays cheap. */
|
|
@@ -27,6 +28,8 @@ export interface HeaderView {
|
|
|
27
28
|
halfBlocks: boolean;
|
|
28
29
|
/** Columns of left margin: Pi's output pad, so the nameplate sits on the transcript margin. */
|
|
29
30
|
pad: number;
|
|
31
|
+
/** A newer published version of this package, when one is known. */
|
|
32
|
+
latest?: string;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** The mark block is four columns on both rows, so the identity column aligns. */
|
|
@@ -78,7 +81,7 @@ export class ByteflyHeader implements Component {
|
|
|
78
81
|
const w = Math.max(1, width);
|
|
79
82
|
const theme = this.getTheme();
|
|
80
83
|
const view = this.view();
|
|
81
|
-
const viewKey = `${view.showVersion}:${view.halfBlocks}:${view.pad}`;
|
|
84
|
+
const viewKey = `${view.showVersion}:${view.halfBlocks}:${view.pad}:${view.latest ?? ""}`;
|
|
82
85
|
const keys = this.keySignature();
|
|
83
86
|
if (
|
|
84
87
|
this.cachedWidth === w &&
|
|
@@ -106,7 +109,8 @@ export class ByteflyHeader implements Component {
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
private build(w: number, theme: Theme, view: HeaderView): string[] {
|
|
109
|
-
const { nameRole, chromeRole, detailRole, chromeTagline, chromeName, chromeVersion, help } =
|
|
112
|
+
const { nameRole, chromeRole, detailRole, updateRole, chromeTagline, chromeName, chromeVersion, help } =
|
|
113
|
+
HEADER_CONFIG;
|
|
110
114
|
const padWidth = Math.max(0, Math.min(Math.floor(view.pad), Math.max(0, w - 1)));
|
|
111
115
|
const pad = " ".repeat(padWidth);
|
|
112
116
|
const [markTop, markBottom] = markLines(theme, view.halfBlocks);
|
|
@@ -120,11 +124,18 @@ export class ByteflyHeader implements Component {
|
|
|
120
124
|
: theme.bold(paint(theme, nameRole, HEADER_CONFIG.appName))
|
|
121
125
|
: version;
|
|
122
126
|
const chrome = `${paint(theme, detailRole, chromeTagline)} ${paint(theme, chromeRole, chromeName)}`;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
const chromeWithVersion = chromeVersion ? `${chrome} ${paint(theme, detailRole, `v${chromeVersion}`)}` : chrome;
|
|
128
|
+
// The published version follows the loaded one: `v0.11.4 ↑0.12.0`.
|
|
129
|
+
const chromeWithMarker =
|
|
130
|
+
chromeVersion && view.latest
|
|
131
|
+
? `${chromeWithVersion} ${paint(theme, updateRole, updateMarker(view.latest))}`
|
|
132
|
+
: chromeWithVersion;
|
|
133
|
+
|
|
134
|
+
// The identity rows shed as one block: the update marker first, then the
|
|
135
|
+
// chrome version, the chrome line, and finally the agent.
|
|
126
136
|
const states: Array<readonly [string, string]> = [
|
|
127
|
-
[agent,
|
|
137
|
+
[agent, chromeWithMarker],
|
|
138
|
+
[agent, chromeWithVersion],
|
|
128
139
|
[agent, chrome],
|
|
129
140
|
[agent, ""],
|
|
130
141
|
["", ""],
|
|
@@ -132,7 +143,11 @@ export class ByteflyHeader implements Component {
|
|
|
132
143
|
const lineWidth = (cell: string): number => pad.length + MARK_WIDTH + (cell ? 1 + visibleWidth(cell) : 0);
|
|
133
144
|
const [agentCell, chromeCell] = states.find(([a, c]) => Math.max(lineWidth(a), lineWidth(c)) <= w) ?? ["", ""];
|
|
134
145
|
|
|
146
|
+
// The header owns its leading blank: Pi mounts the built-in header inside
|
|
147
|
+
// `Spacer(1)`s, but the `quietStartup: true` branch that hands this header the
|
|
148
|
+
// screen adds none, and `setHeader` swaps only the header child.
|
|
135
149
|
return [
|
|
150
|
+
"",
|
|
136
151
|
this.identityLine(pad, markTop, agentCell, w),
|
|
137
152
|
this.identityLine(pad, markBottom, chromeCell, w),
|
|
138
153
|
this.rule(theme, w, pad),
|
|
@@ -35,6 +35,8 @@ export interface HeaderConfig {
|
|
|
35
35
|
readonly chromeRole: TuiRole;
|
|
36
36
|
/** Role for the versions, the tagline, and the help descriptions. */
|
|
37
37
|
readonly detailRole: TuiRole;
|
|
38
|
+
/** Role for the update marker: an action, so the accent rather than a warning colour. */
|
|
39
|
+
readonly updateRole: TuiRole;
|
|
38
40
|
/** Role for the rule's accent lead under the mark. */
|
|
39
41
|
readonly ruleAccentRole: TuiRole;
|
|
40
42
|
/** Role for the rest of the rule. */
|
|
@@ -57,6 +59,7 @@ export const HEADER_CONFIG: HeaderConfig = {
|
|
|
57
59
|
nameRole: "label",
|
|
58
60
|
chromeRole: "meta",
|
|
59
61
|
detailRole: "faint",
|
|
62
|
+
updateRole: "accent",
|
|
60
63
|
ruleAccentRole: "accent",
|
|
61
64
|
ruleRole: "rule",
|
|
62
65
|
chromeTagline: "enhanced by",
|
|
@@ -9,21 +9,40 @@
|
|
|
9
9
|
* `pi config` remains the way to turn all of bytefly's chrome off. The header
|
|
10
10
|
* is installed only in interactive (`tui`) sessions; every other mode leaves
|
|
11
11
|
* Pi's built-in header in place, which does not exist there.
|
|
12
|
+
*
|
|
13
|
+
* The update check resolves after this install, so the header reads the newest
|
|
14
|
+
* known version through `UpdateSource` and `refresh()` re-applies the last
|
|
15
|
+
* context's header once one arrives. That is the only reason the header is ever
|
|
16
|
+
* installed twice.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
20
|
import { isAppleTerminalSession } from "@earendil-works/pi-tui";
|
|
16
21
|
import { DEFAULT_HOST_PAD } from "../pad.ts";
|
|
22
|
+
import { createUpdateState, type UpdateSource } from "../update/state.ts";
|
|
17
23
|
import { createHeader, type HeaderView } from "./component.ts";
|
|
18
24
|
import { HEADER_CONFIG } from "./config.ts";
|
|
19
25
|
|
|
20
|
-
/**
|
|
21
|
-
export
|
|
26
|
+
/** The installed header, and the way to re-install it after a change. */
|
|
27
|
+
export interface HeaderHandle {
|
|
28
|
+
/** Re-apply the last session's header, picking up the current update marker. */
|
|
29
|
+
refresh(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register the install/restore lifecycle for the custom header.
|
|
34
|
+
*
|
|
35
|
+
* `updates` is the store the update check writes, so the marker appears when a
|
|
36
|
+
* newer version is known. A test that does not exercise the marker may omit it.
|
|
37
|
+
*/
|
|
38
|
+
export function registerHeader(pi: ExtensionAPI, updates: UpdateSource = createUpdateState()): HeaderHandle {
|
|
22
39
|
// Half blocks misalign in Apple Terminal, so the mark falls back to a wordmark
|
|
23
40
|
// there, mirroring Pi's own built-in header.
|
|
24
41
|
const halfBlocks = !isAppleTerminalSession();
|
|
42
|
+
let last: ExtensionContext | undefined;
|
|
25
43
|
|
|
26
44
|
const apply = (ctx: ExtensionContext): void => {
|
|
45
|
+
last = ctx;
|
|
27
46
|
if (ctx.mode !== "tui") return;
|
|
28
47
|
// Bytefly's startup TUI is on only when Pi's `quietStartup` is true, which
|
|
29
48
|
// hides Pi's built-in startup header. Every other value (`false`,
|
|
@@ -36,14 +55,26 @@ export function registerHeader(pi: ExtensionAPI): void {
|
|
|
36
55
|
// value the tool frame and the notice inset by. An older host without
|
|
37
56
|
// `getSettings` keeps the default margin.
|
|
38
57
|
const pad = pi.getSettings?.().outputPad ?? DEFAULT_HOST_PAD;
|
|
39
|
-
const view = (): HeaderView => ({
|
|
58
|
+
const view = (): HeaderView => ({
|
|
59
|
+
showVersion: HEADER_CONFIG.showVersion,
|
|
60
|
+
halfBlocks,
|
|
61
|
+
pad,
|
|
62
|
+
latest: updates.latest(),
|
|
63
|
+
});
|
|
40
64
|
ctx.ui.setHeader(createHeader(() => ctx.ui.theme, view));
|
|
41
65
|
};
|
|
42
66
|
|
|
43
67
|
pi.on("session_start", (_event, ctx) => apply(ctx));
|
|
44
68
|
pi.on("session_tree", (_event, ctx) => apply(ctx));
|
|
45
69
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
70
|
+
last = undefined;
|
|
46
71
|
if (ctx.mode !== "tui") return;
|
|
47
72
|
ctx.ui.setHeader(undefined);
|
|
48
73
|
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
refresh() {
|
|
77
|
+
if (last) apply(last);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
49
80
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bytefly — the package's terminal chrome and diagnostics, composed from
|
|
2
|
+
* bytefly — the package's terminal chrome and diagnostics, composed from eleven
|
|
3
3
|
* feature folders: `header/` (the themed startup mark and version), `footer/`
|
|
4
4
|
* (the two-line, width-adaptive status bar with its price calculation),
|
|
5
5
|
* `message/` (the user-message nameplate), `notice/` (the shared transcript
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* transcript turn rule), `working/` (the live elapsed counter on Pi's working
|
|
8
8
|
* row), `panel/` (the unified `/bytefly` screen and its
|
|
9
9
|
* `doctor` report), `doctor/` (the `run_doctor` tool and its session-scoped
|
|
10
|
-
* probe runtime),
|
|
10
|
+
* probe runtime), `notify/` (OS notifications for unattended runs), and
|
|
11
|
+
* `update/` (the published-version check behind the header's update marker).
|
|
11
12
|
*
|
|
12
13
|
* The header, footer, turn rule, and working timer are installed only in
|
|
13
14
|
* interactive (`tui`) sessions, and are always on; the user-message marker,
|
|
@@ -34,6 +35,8 @@ import { type NotifyDeps, registerNotify } from "./notify/register.ts";
|
|
|
34
35
|
import { createPadStore } from "./pad.ts";
|
|
35
36
|
import { type PanelCommandDeps, registerPanel } from "./panel/commands.ts";
|
|
36
37
|
import { registerTurnSeparator } from "./separator/register.ts";
|
|
38
|
+
import { registerUpdate, type UpdateDeps } from "./update/register.ts";
|
|
39
|
+
import { createUpdateState } from "./update/state.ts";
|
|
37
40
|
import { registerWorkingTimer } from "./working/register.ts";
|
|
38
41
|
|
|
39
42
|
/** Host seams, injected by tests. */
|
|
@@ -44,13 +47,18 @@ export interface ByteflyDeps {
|
|
|
44
47
|
doctor?: DoctorDeps;
|
|
45
48
|
/** Seams for the notify capability. */
|
|
46
49
|
notify?: NotifyDeps;
|
|
50
|
+
/** Seams for the published-version check. */
|
|
51
|
+
update?: UpdateDeps;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
/** Register every bytefly surface; the factory only registers. */
|
|
50
55
|
export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void {
|
|
51
56
|
if (!isExtensionEnabled("bytefly")) return;
|
|
52
57
|
|
|
53
|
-
|
|
58
|
+
// One store per load: the update check writes the newest published version and
|
|
59
|
+
// the header reads it, so the marker and every re-install agree.
|
|
60
|
+
const updates = createUpdateState();
|
|
61
|
+
const header = registerHeader(pi, updates);
|
|
54
62
|
registerFooter(pi);
|
|
55
63
|
registerUserMessage(pi);
|
|
56
64
|
// One store per load: the tool frame and the notice record Pi's output pad and
|
|
@@ -66,4 +74,6 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
|
|
|
66
74
|
const doctor = registerDoctor(pi, deps.doctor);
|
|
67
75
|
registerPanel(pi, { doctor, discover: deps.extensions?.discover });
|
|
68
76
|
registerNotify(pi, deps.notify);
|
|
77
|
+
// Last, so a version that arrives after startup can re-install the header.
|
|
78
|
+
registerUpdate(pi, updates, { ...deps.update, onUpdate: () => header.refresh() });
|
|
69
79
|
}
|