@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* built-in footer.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { UsageRecord
|
|
11
|
+
import type { UsageRecord } from "../types.ts";
|
|
12
12
|
|
|
13
13
|
interface UsageLike {
|
|
14
14
|
input: number;
|
|
@@ -43,11 +43,6 @@ interface UsageSource {
|
|
|
43
43
|
isAssistant: boolean;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/** Zeroed totals; the starting point for every accumulation. */
|
|
47
|
-
export function emptyUsage(): UsageTotals {
|
|
48
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
46
|
/**
|
|
52
47
|
* Every usage-bearing entry, as a record.
|
|
53
48
|
*
|
|
@@ -17,8 +17,9 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
17
17
|
import type { DelegationLedger } from "../delegation.ts";
|
|
18
18
|
import { createPricer } from "../price/price.ts";
|
|
19
19
|
import type { ByteflyConfig } from "../price/types.ts";
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
20
|
+
import { zeroUsage } from "../../../../lib/usage.ts";
|
|
21
|
+
import type { ContextInfo, FooterData, PricedUsage, StatusSnapshot, UsageTotals } from "../types.ts";
|
|
22
|
+
import { usageRecords } from "./entries.ts";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Build a reader that memoizes both scans by branch leaf and model.
|
|
@@ -36,21 +37,33 @@ export interface SnapshotReader {
|
|
|
36
37
|
*
|
|
37
38
|
* The budget guardrail reads the number the footer already priced instead of
|
|
38
39
|
* walking the session a second time; if no frame has rendered yet there is
|
|
39
|
-
* nothing stale to act on.
|
|
40
|
+
* nothing stale to act on. The unpriced count rides along for the same reason.
|
|
40
41
|
*/
|
|
41
|
-
peek():
|
|
42
|
+
peek(): PricedUsage | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The five counters, without the unpriced count that only the notice reads. */
|
|
46
|
+
function countsOf(priced: PricedUsage): UsageTotals {
|
|
47
|
+
return {
|
|
48
|
+
input: priced.input,
|
|
49
|
+
output: priced.output,
|
|
50
|
+
cacheRead: priced.cacheRead,
|
|
51
|
+
cacheWrite: priced.cacheWrite,
|
|
52
|
+
cost: priced.cost,
|
|
53
|
+
};
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
export function createSnapshotReader(config: ByteflyConfig, delegation: DelegationLedger): SnapshotReader {
|
|
45
57
|
const price = createPricer(config);
|
|
46
58
|
let key: string | null = null;
|
|
47
|
-
let usage =
|
|
48
|
-
let
|
|
59
|
+
let usage: PricedUsage = { ...zeroUsage(), unpriced: 0 };
|
|
60
|
+
let usageCounts: UsageTotals = zeroUsage();
|
|
61
|
+
let delegationTotals = { runs: 0, usage: zeroUsage() };
|
|
49
62
|
let cacheHitRate: number | null = null;
|
|
50
63
|
let context: ContextInfo = { tokens: null, contextWindow: 0, percent: null };
|
|
51
64
|
let sessionName: string | null = null;
|
|
52
65
|
|
|
53
|
-
let last:
|
|
66
|
+
let last: PricedUsage | undefined;
|
|
54
67
|
|
|
55
68
|
const reader = ((ctx: ExtensionContext, footerData: FooterData): StatusSnapshot => {
|
|
56
69
|
const session = ctx.sessionManager;
|
|
@@ -60,10 +73,11 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
|
|
|
60
73
|
const active = current ? { provider: current.provider, model: current.id } : undefined;
|
|
61
74
|
const scanned = usageRecords(session.getEntries(), active);
|
|
62
75
|
usage = price(scanned.records);
|
|
76
|
+
usageCounts = countsOf(usage);
|
|
63
77
|
cacheHitRate = scanned.cacheHitRate;
|
|
64
78
|
// The child runs are priced by the same pricer, so a currency or rate
|
|
65
79
|
// change moves both numbers together — and neither is folded into the other.
|
|
66
|
-
delegationTotals = { runs: delegation.runs(), usage: price(delegation.records()) };
|
|
80
|
+
delegationTotals = { runs: delegation.runs(), usage: countsOf(price(delegation.records())) };
|
|
67
81
|
|
|
68
82
|
const usageInfo = ctx.getContextUsage();
|
|
69
83
|
context = {
|
|
@@ -91,7 +105,7 @@ export function createSnapshotReader(config: ByteflyConfig, delegation: Delegati
|
|
|
91
105
|
model,
|
|
92
106
|
thinkingLevel: ctx.thinkingLevel ?? null,
|
|
93
107
|
context,
|
|
94
|
-
usage,
|
|
108
|
+
usage: usageCounts,
|
|
95
109
|
currency: config.currency,
|
|
96
110
|
cacheHitRate,
|
|
97
111
|
providerCount: footerData.getAvailableProviderCount(),
|
|
@@ -2,15 +2,16 @@
|
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* matching rates and the timezone formatter. Both currencies price a record
|
|
6
|
+
* from `rates` and `peak` when a key matches. They differ only for a model no
|
|
7
|
+
* key matches: `USD` adds Pi's recorded dollar amount (its catalog is already
|
|
8
|
+
* per model), while `CNY` adds nothing and counts the record as unpriced, so the
|
|
9
|
+
* session can say what its total leaves out.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
+
import { zeroUsage } from "../../../../lib/usage.ts";
|
|
12
13
|
import type { ByteflyConfig, ModelRates } from "./types.ts";
|
|
13
|
-
import type {
|
|
14
|
+
import type { PricedUsage, UsageRecord } from "../types.ts";
|
|
14
15
|
import { createRateLookup } from "./rates.ts";
|
|
15
16
|
import { isPeak, peakFormatter } from "./schedule.ts";
|
|
16
17
|
|
|
@@ -33,7 +34,7 @@ function tokenCost(record: UsageRecord, rates: ModelRates): number {
|
|
|
33
34
|
* The returned function totals tokens as well as cost, so the bar's meters and
|
|
34
35
|
* its price always describe the same set of records.
|
|
35
36
|
*/
|
|
36
|
-
export function createPricer(config: ByteflyConfig): (records: readonly UsageRecord[]) =>
|
|
37
|
+
export function createPricer(config: ByteflyConfig): (records: readonly UsageRecord[]) => PricedUsage {
|
|
37
38
|
// Both currencies price from `rates` and the tariff; they differ only in what
|
|
38
39
|
// an unmatched model falls back to, and Pi's recorded cost is already dollars.
|
|
39
40
|
const dollars = config.currency.code === "USD";
|
|
@@ -43,7 +44,7 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
|
|
|
43
44
|
const format = config.peak.windows.length > 0 ? peakFormatter(config.peak.timezone) : null;
|
|
44
45
|
|
|
45
46
|
return (records) => {
|
|
46
|
-
const totals =
|
|
47
|
+
const totals: PricedUsage = { ...zeroUsage(), unpriced: 0 };
|
|
47
48
|
for (const record of records) {
|
|
48
49
|
totals.input += record.input;
|
|
49
50
|
totals.output += record.output;
|
|
@@ -54,7 +55,10 @@ export function createPricer(config: ByteflyConfig): (records: readonly UsageRec
|
|
|
54
55
|
totals.cost += ratedCost(config, format, record, rates);
|
|
55
56
|
continue;
|
|
56
57
|
}
|
|
58
|
+
// No rate: USD still has Pi's own per-model amount, CNY has nothing to add
|
|
59
|
+
// and only a count to report.
|
|
57
60
|
if (dollars) totals.cost += record.recordedCost;
|
|
61
|
+
else totals.unpriced += 1;
|
|
58
62
|
}
|
|
59
63
|
return totals;
|
|
60
64
|
};
|
|
@@ -17,6 +17,7 @@ import { createSnapshotReader, type SnapshotReader } from "./collect/snapshot.ts
|
|
|
17
17
|
import { createDelegationLedger } from "./delegation.ts";
|
|
18
18
|
import { loadByteflyConfig } from "./price/config.ts";
|
|
19
19
|
import { createFooter } from "./render/footer.ts";
|
|
20
|
+
import { unpricedBody, unpricedNotice } from "./unpriced.ts";
|
|
20
21
|
|
|
21
22
|
/** Host seams, injected by the composition root and by tests. */
|
|
22
23
|
export interface FooterDeps {
|
|
@@ -35,6 +36,8 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
|
|
|
35
36
|
let budget: { limit: number; read: SnapshotReader } | undefined;
|
|
36
37
|
/** One warning per session: the notice stays in scrollback, and a re-install must not repeat it. */
|
|
37
38
|
let warned = false;
|
|
39
|
+
/** The unpriced notice is once per session too, and independent of the budget. */
|
|
40
|
+
let warnedUnpriced = false;
|
|
38
41
|
|
|
39
42
|
const install = (ctx: ExtensionContext): void => {
|
|
40
43
|
if (ctx.mode !== "tui") return;
|
|
@@ -58,6 +61,7 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
|
|
|
58
61
|
// and a budget that was already reported is unreported again.
|
|
59
62
|
delegation.reset();
|
|
60
63
|
warned = false;
|
|
64
|
+
warnedUnpriced = false;
|
|
61
65
|
install(ctx);
|
|
62
66
|
});
|
|
63
67
|
pi.on("session_tree", (_event, ctx) => install(ctx));
|
|
@@ -65,9 +69,17 @@ export function registerFooter(pi: ExtensionAPI, deps: FooterDeps = {}): void {
|
|
|
65
69
|
// A turn's cost is final when the run settles, and that is when the user can act
|
|
66
70
|
// on it. Rendering never posts anything: the footer is derived state.
|
|
67
71
|
pi.on("agent_settled", (_event, ctx) => {
|
|
68
|
-
if (
|
|
72
|
+
if (!budget || ctx.mode !== "tui") return;
|
|
69
73
|
const usage = budget.read.peek();
|
|
70
74
|
if (!usage) return;
|
|
75
|
+
// CNY records no rate for a model, so its spend is simply absent from the
|
|
76
|
+
// total. USD never counts one: Pi's recorded amount covers every model.
|
|
77
|
+
if (!warnedUnpriced && usage.unpriced > 0) {
|
|
78
|
+
warnedUnpriced = true;
|
|
79
|
+
pi.sendMessage(unpricedNotice(usage.unpriced));
|
|
80
|
+
deps.notify?.(unpricedBody(usage.unpriced));
|
|
81
|
+
}
|
|
82
|
+
if (warned || budget.limit <= 0) return;
|
|
71
83
|
const state = budgetState(budget.limit, usage.cost);
|
|
72
84
|
if (!state.exceeded) return;
|
|
73
85
|
warned = true;
|
|
@@ -36,6 +36,17 @@ export interface UsageTotals {
|
|
|
36
36
|
cost: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The totals plus how many records the mode had no rate for.
|
|
41
|
+
*
|
|
42
|
+
* CNY has no fallback, so those records contribute no cost at all; the count is
|
|
43
|
+
* what lets the session say so instead of quietly showing a total that omits
|
|
44
|
+
* them. USD falls back to Pi's recorded amount, so it never counts any.
|
|
45
|
+
*/
|
|
46
|
+
export interface PricedUsage extends UsageTotals {
|
|
47
|
+
unpriced: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
/** Context-window usage for the active model. */
|
|
40
51
|
export interface ContextInfo {
|
|
41
52
|
/** Estimated tokens, or `null` when unknown (for example right after compaction). */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one notice a CNY session posts when records had no rate.
|
|
3
|
+
*
|
|
4
|
+
* CNY mode has no fallback, so a model the table cannot price contributes no
|
|
5
|
+
* cost at all; the footer's total is then smaller than the work done, with
|
|
6
|
+
* nothing on screen saying why. This is the one sentence that says it, once per
|
|
7
|
+
* session, and names the two honest fixes rather than picking one: switch to
|
|
8
|
+
* `USD` (Pi's catalog is already per model, tiers included) or state yuan rates
|
|
9
|
+
* under `price.rates`.
|
|
10
|
+
*
|
|
11
|
+
* Pure: the caller has the count, this shapes the answer. No figure is quoted,
|
|
12
|
+
* because there is none to quote.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { noticeMessage, type NoticeMessage } from "../../../lib/notice.ts";
|
|
16
|
+
|
|
17
|
+
/** The notice naming what the total leaves out, and how to price it. */
|
|
18
|
+
export function unpricedNotice(count: number): NoticeMessage {
|
|
19
|
+
const subject = count === 1 ? "1 record has" : `${count} records have`;
|
|
20
|
+
return noticeMessage({
|
|
21
|
+
content:
|
|
22
|
+
`${subject} no CNY rate, so ${count === 1 ? "it is" : "they are"} not in this total. ` +
|
|
23
|
+
"Switch `price.currency.code` to USD to use Pi's recorded cost, or add yuan `price.rates` entries.",
|
|
24
|
+
details: {
|
|
25
|
+
source: "bytefly",
|
|
26
|
+
title: "Records with no CNY rate",
|
|
27
|
+
severity: "info",
|
|
28
|
+
items: [{ label: "records", detail: String(count) }],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The one-line body an OS notification carries. */
|
|
34
|
+
export function unpricedBody(count: number): string {
|
|
35
|
+
return `${count} record${count === 1 ? "" : "s"} with no CNY rate · use USD or add price.rates`;
|
|
36
|
+
}
|
|
@@ -17,8 +17,8 @@ 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";
|
|
21
20
|
import { HEADER_CONFIG, type HeaderHeight, type HelpEntry } from "./config.ts";
|
|
21
|
+
import { identityCells } from "./identity.ts";
|
|
22
22
|
|
|
23
23
|
/** The header's shape: everything but the live terminal height is resolved once per apply. */
|
|
24
24
|
export interface HeaderView {
|
|
@@ -32,6 +32,8 @@ export interface HeaderView {
|
|
|
32
32
|
rows?: number;
|
|
33
33
|
/** A newer published version of this package, when one is known. */
|
|
34
34
|
latest?: string;
|
|
35
|
+
/** A newer published version of Pi itself, when one is known. */
|
|
36
|
+
piLatest?: string;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** The mark block is four columns on both rows, so the identity column aligns. */
|
|
@@ -121,40 +123,24 @@ export class ByteflyHeader implements Component {
|
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
private build(w: number, theme: Theme, view: HeaderView): string[] {
|
|
124
|
-
const {
|
|
125
|
-
HEADER_CONFIG;
|
|
126
|
+
const { help } = HEADER_CONFIG;
|
|
126
127
|
const shape = heightShape(view.rows);
|
|
127
128
|
const padWidth = Math.max(0, Math.min(Math.floor(view.pad), Math.max(0, w - 1)));
|
|
128
129
|
const pad = " ".repeat(padWidth);
|
|
129
130
|
const [markTop, markBottom] = markLines(theme, view.halfBlocks);
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
// The wordmark mark spells the app name, so the agent cell carries only the
|
|
133
|
-
// version there; the half-block mark does not, so the name stays beside it.
|
|
134
|
-
const agent = view.halfBlocks
|
|
135
|
-
? version
|
|
136
|
-
? `${theme.bold(paint(theme, nameRole, HEADER_CONFIG.appName))} ${version}`
|
|
137
|
-
: theme.bold(paint(theme, nameRole, HEADER_CONFIG.appName))
|
|
138
|
-
: version;
|
|
139
|
-
const chrome = `${paint(theme, detailRole, chromeTagline)} ${paint(theme, chromeRole, chromeName)}`;
|
|
140
|
-
const chromeWithVersion = chromeVersion ? `${chrome} ${paint(theme, detailRole, `v${chromeVersion}`)}` : chrome;
|
|
141
|
-
// The published version follows the loaded one: `v0.11.4 ↑0.12.0`.
|
|
142
|
-
const chromeWithMarker =
|
|
143
|
-
chromeVersion && view.latest
|
|
144
|
-
? `${chromeWithVersion} ${paint(theme, updateRole, updateMarker(view.latest))}`
|
|
145
|
-
: chromeWithVersion;
|
|
146
|
-
|
|
147
|
-
// The identity rows shed as one block: the update marker first, then the
|
|
148
|
-
// chrome version, the chrome line, and finally the agent.
|
|
149
|
-
const states: Array<readonly [string, string]> = [
|
|
150
|
-
[agent, chromeWithMarker],
|
|
151
|
-
[agent, chromeWithVersion],
|
|
152
|
-
[agent, chrome],
|
|
153
|
-
[agent, ""],
|
|
154
|
-
["", ""],
|
|
155
|
-
];
|
|
132
|
+
// The identity rows shed as one block, widest pair first: see `identity.ts`.
|
|
156
133
|
const lineWidth = (cell: string): number => pad.length + MARK_WIDTH + (cell ? 1 + visibleWidth(cell) : 0);
|
|
157
|
-
const [agentCell, chromeCell] =
|
|
134
|
+
const [agentCell, chromeCell] = identityCells(
|
|
135
|
+
{
|
|
136
|
+
halfBlocks: view.halfBlocks,
|
|
137
|
+
showVersion: view.showVersion,
|
|
138
|
+
version: this.version,
|
|
139
|
+
piLatest: view.piLatest,
|
|
140
|
+
latest: view.latest,
|
|
141
|
+
},
|
|
142
|
+
theme,
|
|
143
|
+
).find(([a, c]) => Math.max(lineWidth(a), lineWidth(c)) <= w) ?? ["", ""];
|
|
158
144
|
|
|
159
145
|
// The header owns its leading blank: Pi mounts the built-in header inside
|
|
160
146
|
// `Spacer(1)`s, but the `quietStartup: true` branch that hands this header the
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The header's two identity cells and the ladder that sheds them.
|
|
3
|
+
*
|
|
4
|
+
* The nameplate's first row names Pi and its version, the second the tagline,
|
|
5
|
+
* bytefly, and its version, and either row may end with the `↑<version>` marker
|
|
6
|
+
* of a newer published release. The cells shed as one block when the terminal
|
|
7
|
+
* narrows, so the order is data here rather than a state table inside `build()`:
|
|
8
|
+
* the widest pair first, then the chrome marker, the chrome version, the chrome
|
|
9
|
+
* line, and finally the agent's own marker with the agent row itself.
|
|
10
|
+
*
|
|
11
|
+
* Pure: the theme is passed in and every returned string is painted, so a caller
|
|
12
|
+
* only measures and mounts them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { paint } from "../../../lib/tui/tokens.ts";
|
|
17
|
+
import { updateMarker } from "../update/format.ts";
|
|
18
|
+
import { HEADER_CONFIG } from "./config.ts";
|
|
19
|
+
|
|
20
|
+
/** What the cells are built from: the live view's switches and the known versions. */
|
|
21
|
+
export interface IdentityInput {
|
|
22
|
+
/** Whether the terminal renders half blocks; the wordmark mark spells the app name. */
|
|
23
|
+
halfBlocks: boolean;
|
|
24
|
+
/** Print Pi's version on the agent line. */
|
|
25
|
+
showVersion: boolean;
|
|
26
|
+
/** Pi's running version. */
|
|
27
|
+
version: string;
|
|
28
|
+
/** Pi's newer published version, when one is known. */
|
|
29
|
+
piLatest?: string;
|
|
30
|
+
/** This package's newer published version, when one is known. */
|
|
31
|
+
latest?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A marker cell suffix, or nothing when no newer version is known. */
|
|
35
|
+
function markerSuffix(theme: Theme, version: string | undefined): string {
|
|
36
|
+
return version ? ` ${paint(theme, HEADER_CONFIG.updateRole, updateMarker(version))}` : "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The identity pairs, widest first, with each pair that repeats its predecessor
|
|
41
|
+
* dropped so the ladder names a state only when it is actually narrower.
|
|
42
|
+
*/
|
|
43
|
+
export function identityCells(input: IdentityInput, theme: Theme): Array<readonly [string, string]> {
|
|
44
|
+
const { nameRole, chromeRole, detailRole, appName, chromeTagline, chromeName, chromeVersion } = HEADER_CONFIG;
|
|
45
|
+
const version = input.showVersion && input.version ? paint(theme, detailRole, `v${input.version}`) : "";
|
|
46
|
+
// The wordmark mark spells the app name, so the agent cell carries only the
|
|
47
|
+
// version there; the half-block mark does not, so the name stays beside it.
|
|
48
|
+
const agent = input.halfBlocks
|
|
49
|
+
? version
|
|
50
|
+
? `${theme.bold(paint(theme, nameRole, appName))} ${version}`
|
|
51
|
+
: theme.bold(paint(theme, nameRole, appName))
|
|
52
|
+
: version;
|
|
53
|
+
const chrome = `${paint(theme, detailRole, chromeTagline)} ${paint(theme, chromeRole, chromeName)}`;
|
|
54
|
+
const chromeWithVersion = chromeVersion ? `${chrome} ${paint(theme, detailRole, `v${chromeVersion}`)}` : chrome;
|
|
55
|
+
const chromeWithMarker = `${chromeWithVersion}${markerSuffix(theme, input.latest)}`;
|
|
56
|
+
const agentWithMarker = `${agent}${markerSuffix(theme, input.piLatest)}`;
|
|
57
|
+
|
|
58
|
+
const states: Array<readonly [string, string]> = [];
|
|
59
|
+
const shed = (agentCell: string, chromeCell: string): void => {
|
|
60
|
+
const last = states.at(-1);
|
|
61
|
+
if (last && last[0] === agentCell && last[1] === chromeCell) return;
|
|
62
|
+
states.push([agentCell, chromeCell]);
|
|
63
|
+
};
|
|
64
|
+
shed(agentWithMarker, chromeWithMarker);
|
|
65
|
+
shed(agentWithMarker, chromeWithVersion);
|
|
66
|
+
shed(agentWithMarker, chrome);
|
|
67
|
+
shed(agentWithMarker, "");
|
|
68
|
+
shed(agent, "");
|
|
69
|
+
shed("", "");
|
|
70
|
+
return states;
|
|
71
|
+
}
|
|
@@ -32,10 +32,15 @@ export interface HeaderHandle {
|
|
|
32
32
|
/**
|
|
33
33
|
* Register the install/restore lifecycle for the custom header.
|
|
34
34
|
*
|
|
35
|
-
* `updates` is the store the update check writes
|
|
36
|
-
*
|
|
35
|
+
* `updates` is the store the package-update check writes and `piUpdates` the one
|
|
36
|
+
* Pi's own release notice writes, so either marker appears when a newer version
|
|
37
|
+
* is known. A test that does not exercise a marker may omit its store.
|
|
37
38
|
*/
|
|
38
|
-
export function registerHeader(
|
|
39
|
+
export function registerHeader(
|
|
40
|
+
pi: ExtensionAPI,
|
|
41
|
+
updates: UpdateSource = createUpdateState(),
|
|
42
|
+
piUpdates?: UpdateSource,
|
|
43
|
+
): HeaderHandle {
|
|
39
44
|
// Half blocks misalign in Apple Terminal, so the mark falls back to a wordmark
|
|
40
45
|
// there, mirroring Pi's own built-in header.
|
|
41
46
|
const halfBlocks = !isAppleTerminalSession();
|
|
@@ -60,6 +65,7 @@ export function registerHeader(pi: ExtensionAPI, updates: UpdateSource = createU
|
|
|
60
65
|
halfBlocks,
|
|
61
66
|
pad,
|
|
62
67
|
latest: updates.latest(),
|
|
68
|
+
piLatest: piUpdates?.latest(),
|
|
63
69
|
rows,
|
|
64
70
|
});
|
|
65
71
|
ctx.ui.setHeader(createHeader(() => ctx.ui.theme, view));
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* probe runtime), `notify/` (OS notifications for unattended runs), and
|
|
11
11
|
* `update/` (Pi's package list, the host suppression, and the header marker).
|
|
12
12
|
*
|
|
13
|
-
* The header, footer, turn rule, and working line are installed
|
|
14
|
-
* interactive (`tui`) sessions, and are always on; the user-message
|
|
15
|
-
* notice, and tool blocks register at load and take effect wherever a
|
|
13
|
+
* The header, footer, turn rule, and working line are installed
|
|
14
|
+
* only in interactive (`tui`) sessions, and are always on; the user-message
|
|
15
|
+
* marker, 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
|
|
17
17
|
* in every mode, and notify works wherever the environment can deliver it. The
|
|
18
18
|
* footer is priced from DeepSeek's published rates by default and from
|
|
@@ -35,6 +35,7 @@ import { type NotifyDeps, registerNotify } from "./notify/register.ts";
|
|
|
35
35
|
import { createPadStore } from "./pad.ts";
|
|
36
36
|
import { type PanelCommandDeps, registerPanel } from "./panel/commands.ts";
|
|
37
37
|
import { registerTurnSeparator } from "./separator/register.ts";
|
|
38
|
+
import { registerPiRelease, type ReleaseDeps } from "./update/release.ts";
|
|
38
39
|
import { registerUpdate, type UpdateDeps } from "./update/register.ts";
|
|
39
40
|
import { createUpdateState } from "./update/state.ts";
|
|
40
41
|
import { registerWorkingTimer } from "./working/register.ts";
|
|
@@ -49,6 +50,8 @@ export interface ByteflyDeps {
|
|
|
49
50
|
notify?: NotifyDeps;
|
|
50
51
|
/** Seams for the package-update surface. */
|
|
51
52
|
update?: UpdateDeps;
|
|
53
|
+
/** Seams for Pi's own release notice. */
|
|
54
|
+
release?: ReleaseDeps;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/** Register every bytefly surface; the factory only registers. */
|
|
@@ -56,9 +59,11 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
|
|
|
56
59
|
if (!isExtensionEnabled("bytefly")) return;
|
|
57
60
|
|
|
58
61
|
// One store per load: the update check writes this package's new version and
|
|
59
|
-
// the header reads it, so the marker and every re-install agree.
|
|
62
|
+
// the header reads it, so the marker and every re-install agree. Pi's own
|
|
63
|
+
// release notice writes the second store the same way.
|
|
60
64
|
const updates = createUpdateState();
|
|
61
|
-
const
|
|
65
|
+
const piUpdates = createUpdateState();
|
|
66
|
+
const header = registerHeader(pi, updates, piUpdates);
|
|
62
67
|
// Notify first, so the footer's budget guardrail can raise one OS notification
|
|
63
68
|
// through the same capability the triggers use instead of a second dispatch path.
|
|
64
69
|
const notifications = registerNotify(pi, deps.notify);
|
|
@@ -78,5 +83,8 @@ export default function bytefly(pi: ExtensionAPI, deps: ByteflyDeps = {}): void
|
|
|
78
83
|
// The returned handle is what `/bytefly update` re-runs, so the update surface is
|
|
79
84
|
// registered before the panel that offers the command.
|
|
80
85
|
const update = registerUpdate(pi, updates, { ...deps.update, onUpdate: () => header.refresh() });
|
|
86
|
+
// Pi's own release notice is the second source of a header marker, so it shares
|
|
87
|
+
// the one repaint hook rather than installing a header of its own.
|
|
88
|
+
registerPiRelease(pi, piUpdates, { ...deps.release, onUpdate: () => header.refresh() });
|
|
81
89
|
registerPanel(pi, { doctor, update, discover: deps.extensions?.discover });
|
|
82
90
|
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* that spans the width, above the message.
|
|
2
|
+
* The message transformer: the user-message nameplate.
|
|
4
3
|
*
|
|
5
|
-
* Pi's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
4
|
+
* Pi's message components parse the transformed string as Markdown and own their
|
|
5
|
+
* colors, so shaping text is the only lever: this surface adds no component and
|
|
6
|
+
* no color of its own. The nameplate is a level-2 heading, which Pi renders
|
|
7
|
+
* bold in `mdHeading` and prints without its `#` prefix; the bytefly themes point
|
|
8
|
+
* `mdHeading` at the accent, so the label and rule carry the accent while the
|
|
9
|
+
* message itself stays in the default text color. Markdown cannot express a
|
|
10
|
+
* two-line heading, and Pi puts a blank line after a heading block, so the label
|
|
11
|
+
* is padded to the width and the rule is appended: the heading's text wraps,
|
|
12
|
+
* putting the rule on its own row with no blank between. A blank line separates
|
|
13
|
+
* the nameplate from the message, so the message's own Markdown (a fence, a list,
|
|
14
|
+
* a second paragraph) parses exactly as it would without it.
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
import type { MarkdownTransformContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -40,3 +39,11 @@ export function styleUserMessage(markdown: string, context: MarkdownTransformCon
|
|
|
40
39
|
const head = nameplate(context.availableWidth);
|
|
41
40
|
return head ? `${head}\n\n${markdown}` : markdown;
|
|
42
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Dispatch the single transformer Pi keeps per extension on the message type: a
|
|
45
|
+
* user message gains the nameplate, and every other message passes through.
|
|
46
|
+
*/
|
|
47
|
+
export function styleMessage(markdown: string, context: MarkdownTransformContext): string {
|
|
48
|
+
return context.messageType === "user" ? styleUserMessage(markdown, context) : markdown;
|
|
49
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registers bytefly's
|
|
2
|
+
* Registers bytefly's single Markdown transformer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Pi keeps one transformer per extension (`loader.js`), and the extension
|
|
5
|
+
* spends it on the user-message nameplate. A second
|
|
6
|
+
* `registerMarkdownTransformer` call would silently replace the first, so there
|
|
7
|
+
* is exactly one, here.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
11
|
+
import { styleMessage } from "./format.ts";
|
|
11
12
|
|
|
12
|
-
/** Register the transformer that marks
|
|
13
|
+
/** Register the transformer that marks user messages. */
|
|
13
14
|
export function registerUserMessage(pi: ExtensionAPI): void {
|
|
14
|
-
pi.registerMarkdownTransformer((markdown, context) =>
|
|
15
|
+
pi.registerMarkdownTransformer((markdown, context) => styleMessage(markdown, context));
|
|
15
16
|
}
|
|
@@ -21,7 +21,7 @@ export interface NotifyConfig {
|
|
|
21
21
|
needsInput: boolean;
|
|
22
22
|
/** Notify when a background `job` finishes. */
|
|
23
23
|
jobFinished: boolean;
|
|
24
|
-
/** Notify when a `watch` wake match fires
|
|
24
|
+
/** Notify when a `watch` wake match fires, once the agent is idle. */
|
|
25
25
|
matchFound: boolean;
|
|
26
26
|
/** Delivery method; `auto` chooses by terminal and platform. */
|
|
27
27
|
method: NotifyMethod;
|