@gavin-hu/my-pi-agent 0.13.0 → 0.14.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 +14 -11
- package/README.zh-CN.md +12 -12
- package/extensions/bytefly/README.md +163 -61
- package/extensions/bytefly/doctor/config.ts +4 -1
- package/extensions/bytefly/doctor/runtime.ts +2 -0
- package/extensions/bytefly/footer/budget.ts +64 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +22 -4
- package/extensions/bytefly/footer/price/config.ts +18 -1
- package/extensions/bytefly/footer/price/types.ts +5 -0
- package/extensions/bytefly/footer/register.ts +34 -3
- package/extensions/bytefly/header/component.ts +30 -8
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +7 -5
- package/extensions/bytefly/notify/config.ts +1 -1
- package/extensions/bytefly/notify/register.ts +15 -1
- package/extensions/bytefly/panel/commands.ts +1 -1
- package/extensions/bytefly/panel/format.ts +12 -19
- package/extensions/bytefly/panel/settings-runtime.ts +62 -18
- package/extensions/bytefly/panel/settings.ts +269 -42
- package/extensions/bytefly/panel/tui/settings-view.ts +49 -3
- package/extensions/bytefly/panel/tui.ts +87 -19
- package/extensions/bytefly/separator/component.ts +7 -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/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- package/extensions/job/format.ts +4 -7
- package/extensions/memory/README.md +2 -0
- package/extensions/plan/README.md +12 -0
- package/extensions/subagent/README.md +60 -5
- package/extensions/subagent/config.ts +58 -0
- package/extensions/subagent/index.ts +27 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +12 -0
- package/extensions/subagent/run.ts +100 -37
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +12 -0
- package/extensions/todo/nudge.ts +5 -1
- package/extensions/watch/README.md +13 -5
- package/extensions/watch/commands.ts +4 -3
- package/extensions/watch/config.ts +1 -1
- package/extensions/watch/format.ts +4 -7
- package/extensions/watch/runtime.ts +83 -12
- package/extensions/wechat/README.md +2 -0
- package/extensions/worktree/README.md +7 -1
- package/extensions/worktree/enter.ts +10 -3
- package/extensions/worktree/exit.ts +25 -5
- package/extensions/worktree/git.ts +28 -0
- package/extensions/worktree/index.ts +2 -0
- package/extensions/worktree/root-tools.ts +1 -1
- package/extensions/worktree/runtime.ts +5 -0
- package/lib/README.md +5 -3
- package/lib/config.ts +33 -9
- package/lib/format.ts +6 -0
- package/lib/requirements.ts +80 -1
- package/lib/tui/columns.ts +17 -0
- package/lib/tui/tool.ts +6 -2
- package/package.json +2 -1
- package/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
|
@@ -7,12 +7,14 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
7
7
|
import { loadConfig } from "./config.ts";
|
|
8
8
|
import { WorktreeCancelledError } from "./errors.ts";
|
|
9
9
|
import {
|
|
10
|
+
commitsAheadOf,
|
|
10
11
|
currentBranch,
|
|
11
12
|
defaultBranch,
|
|
12
13
|
deleteBranch,
|
|
13
14
|
inspectWork,
|
|
14
15
|
isBranchMerged,
|
|
15
16
|
lockWorktree,
|
|
17
|
+
refExists,
|
|
16
18
|
unlockWorktree,
|
|
17
19
|
worktreeRemove,
|
|
18
20
|
} from "./git.ts";
|
|
@@ -32,17 +34,35 @@ export interface ExitOptions {
|
|
|
32
34
|
* A failed lookup degrades to the facts already in hand: an exit that succeeded
|
|
33
35
|
* must not fail because the default branch could not be read.
|
|
34
36
|
*/
|
|
35
|
-
async function describeHandoff(pi: ExtensionAPI, state: WorktreeState,
|
|
36
|
-
if (!state.branch) return { branch: null, base: null, ahead, merged: false };
|
|
37
|
+
async function describeHandoff(pi: ExtensionAPI, state: WorktreeState, recorded: number): Promise<Handoff> {
|
|
38
|
+
if (!state.branch) return { branch: null, base: null, ahead: recorded, merged: false };
|
|
37
39
|
try {
|
|
38
40
|
// `origin/HEAD` names the merge target when the repository has a remote; a
|
|
39
41
|
// local-only repository has none, and the main checkout's own branch is the
|
|
40
42
|
// branch a merge would actually land on.
|
|
41
43
|
const base = (await defaultBranch(pi, state.repoRoot)) ?? (await currentBranch(pi, state.repoRoot)) ?? null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
if (!base) return { branch: state.branch, base: null, ahead: recorded, merged: false };
|
|
45
|
+
// Count against the resolved base branch, not the commit recorded at entry:
|
|
46
|
+
// a worktree branch rebased onto a newer local branch would otherwise report
|
|
47
|
+
// every commit the base gained since, as its own.
|
|
48
|
+
// Every lookup runs in the main checkout: the handoff is read after the exit,
|
|
49
|
+
// when the worktree directory may already be gone, while the branch refs it
|
|
50
|
+
// describes outlive it.
|
|
51
|
+
const root = state.repoRoot;
|
|
52
|
+
const ref = (await refExists(pi, root, base))
|
|
53
|
+
? base
|
|
54
|
+
: (await refExists(pi, root, `origin/${base}`))
|
|
55
|
+
? `origin/${base}`
|
|
56
|
+
: null;
|
|
57
|
+
if (!ref) return { branch: state.branch, base: null, ahead: recorded, merged: false };
|
|
58
|
+
return {
|
|
59
|
+
branch: state.branch,
|
|
60
|
+
base,
|
|
61
|
+
ahead: await commitsAheadOf(pi, root, state.branch, ref),
|
|
62
|
+
merged: await isBranchMerged(pi, root, state.branch, ref),
|
|
63
|
+
};
|
|
44
64
|
} catch {
|
|
45
|
-
return { branch: state.branch, base: null, ahead, merged: false };
|
|
65
|
+
return { branch: state.branch, base: null, ahead: recorded, merged: false };
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
|
|
@@ -191,6 +191,25 @@ export async function worktreeExists(pi: ExtensionAPI, dir: string): Promise<boo
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
/** Whether a local branch exists. */
|
|
194
|
+
/**
|
|
195
|
+
* Count of commits on `branch` that `base` does not contain.
|
|
196
|
+
*
|
|
197
|
+
* Takes the branch by name rather than reading `HEAD`, because a handoff is read
|
|
198
|
+
* after the worktree may already be gone while its branch refs outlive it.
|
|
199
|
+
*/
|
|
200
|
+
export async function commitsAheadOf(pi: ExtensionAPI, dir: string, branch: string, base: string): Promise<number> {
|
|
201
|
+
const result = await git(pi, ["rev-list", "--count", `${base}..${branch}`], dir);
|
|
202
|
+
if (result.code !== 0) return 0;
|
|
203
|
+
const count = Number.parseInt(result.stdout.trim(), 10);
|
|
204
|
+
return Number.isFinite(count) ? count : 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Whether `ref` resolves here: a branch, a remote-tracking ref, or a tag. */
|
|
208
|
+
export async function refExists(pi: ExtensionAPI, dir: string, ref: string): Promise<boolean> {
|
|
209
|
+
const result = await git(pi, ["rev-parse", "--verify", "--quiet", ref], dir);
|
|
210
|
+
return result.code === 0 && result.stdout.trim() !== "";
|
|
211
|
+
}
|
|
212
|
+
|
|
194
213
|
export async function branchExists(pi: ExtensionAPI, repoRoot: string, branch: string): Promise<boolean> {
|
|
195
214
|
const result = await git(pi, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], repoRoot);
|
|
196
215
|
return result.code === 0;
|
|
@@ -240,6 +259,15 @@ export async function deleteBranch(
|
|
|
240
259
|
return { ok: false, error: outcome.error || `Could not delete branch ${branch}` };
|
|
241
260
|
}
|
|
242
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Whether git ignores `path` in `dir`. Asks git rather than matching ignore
|
|
264
|
+
* text, so a directory covered by a wider rule also counts as ignored.
|
|
265
|
+
*/
|
|
266
|
+
export async function isIgnored(pi: ExtensionAPI, dir: string, path: string): Promise<boolean> {
|
|
267
|
+
const result = await git(pi, ["check-ignore", "-q", "--", path], dir);
|
|
268
|
+
return result.code === 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
243
271
|
/** Porcelain status lines for a working tree; empty when clean. */
|
|
244
272
|
export async function statusEntries(pi: ExtensionAPI, dir: string): Promise<string[]> {
|
|
245
273
|
const result = await git(pi, ["status", "--porcelain"], dir);
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
noteUnverifiableCommand,
|
|
54
54
|
publishWorktree,
|
|
55
55
|
resetDiagnostics,
|
|
56
|
+
resetGitignoreOffer,
|
|
56
57
|
setActive,
|
|
57
58
|
setBaseCwd,
|
|
58
59
|
setInactiveOverrides,
|
|
@@ -91,6 +92,7 @@ export function registerWorktree(pi: ExtensionAPI, { entryPath, createBuiltin }:
|
|
|
91
92
|
setActive(null);
|
|
92
93
|
clearConfigCache();
|
|
93
94
|
resetDiagnostics();
|
|
95
|
+
resetGitignoreOffer();
|
|
94
96
|
|
|
95
97
|
// A child `pi` process (subagent) inherits an active worktree. Bind to
|
|
96
98
|
// it without creating, locking, or persisting anything; the parent owns
|
|
@@ -70,6 +70,11 @@ export function markGitignoreOffered(): void {
|
|
|
70
70
|
gitignoreOffered = true;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Re-arm the offer for the next session; `session_start` owns the reset. */
|
|
74
|
+
export function resetGitignoreOffer(): void {
|
|
75
|
+
gitignoreOffered = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
/**
|
|
74
79
|
* Count a bash command the guard let through without being able to verify it.
|
|
75
80
|
* Session-scoped on purpose: it is the one gap a user cannot see in the
|
package/lib/README.md
CHANGED
|
@@ -4,8 +4,10 @@ Small modules imported by two or more extensions. Pi loads each extension with
|
|
|
4
4
|
an **isolated module cache**, so these are *value-only* libraries: pure
|
|
5
5
|
functions, constants, and classes. Never keep cross-extension state here — a
|
|
6
6
|
module-level variable would have one copy per extension, not one shared copy.
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
`test/structure.test.ts` fails a module-scope `let`, `var`, `Map`, `WeakMap`, or
|
|
8
|
+
empty array, so this is a test and not a convention. The single exception is the
|
|
9
|
+
event-bus pattern in `rails.ts`, where the state lives in the caller's closure,
|
|
10
|
+
not in this module.
|
|
9
11
|
|
|
10
12
|
This directory sits outside `extensions/` on purpose: nothing here is a
|
|
11
13
|
loadable extension entrypoint, and every module is a contract shared *between*
|
|
@@ -16,7 +18,7 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
|
|
|
16
18
|
| Module | Responsibility |
|
|
17
19
|
|---|---|
|
|
18
20
|
| [`browser.ts`](./browser.ts) | Cross-platform default-browser opener shared by `file-browse` and `plan`: pure `browserCommand(url, platform)` plus best-effort `openInBrowser(url, { platform, spawn })` with an injectable spawn. |
|
|
19
|
-
| [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `configValueAt` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection` / `writeConfigValue` / `writeConfigValueAt` (the last
|
|
21
|
+
| [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `configValueAt` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection` / `writeConfigValue` / `writeConfigValueAt` / `unsetConfigValueAt` (the last three edit one leaf, flat or nested, preserving siblings and metadata; the last removes it and prunes what it emptied). Includes `clampInteger` / `clampNumber` / `cleanString` / `asRecord` / `readJson` / `readJsonResult`. |
|
|
20
22
|
| [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `isExtensionEnabled` / `ENV_DISABLED_EXTENSIONS` for the package-wide disable list. |
|
|
21
23
|
| [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters. |
|
|
22
24
|
| [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` / `JobFinishedEvent` (published by `job`, consumed by bytefly's notify capability), `SUBAGENT_FINISHED_CHANNEL` / `SubagentFinishedEvent` (published by `subagent`, consumed by bytefly's footer roll-up), and `WEB_SEARCH_PROVIDER_CHANNEL` / `WebSearchProviderEvent` (published by `web-access` at session start, consumed by bytefly's doctor so it can report the resolved provider without importing the extension — the key itself is never in the payload). |
|
package/lib/config.ts
CHANGED
|
@@ -246,27 +246,51 @@ function setPath(source: Record<string, unknown>, path: readonly string[], value
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
* Delete
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
249
|
+
* Delete the leaf at `keyPath` from one top-level section, preserving every
|
|
250
|
+
* sibling key, every other section, and document metadata. An object left empty
|
|
251
|
+
* by the deletion goes with it, the section included, so a file does not fill
|
|
252
|
+
* with `{}` shells. A no-op when the file, section, or leaf is absent; refuses a
|
|
253
|
+
* malformed document and an empty path.
|
|
253
254
|
*/
|
|
254
|
-
export function
|
|
255
|
+
export function unsetConfigValueAt(path: string, section: string, keyPath: readonly string[]): void {
|
|
256
|
+
if (keyPath.length === 0) throw new Error(`Cannot update ${path}: empty key path`);
|
|
255
257
|
const result = readJsonResult(path);
|
|
256
258
|
if (result.state === "absent") return;
|
|
257
259
|
if (result.state === "error") throw new Error(`Cannot update ${path}: ${result.error}`);
|
|
258
260
|
|
|
259
261
|
const document = result.value;
|
|
260
262
|
const current = configSection(document, section);
|
|
261
|
-
if (!current
|
|
263
|
+
if (!current) return;
|
|
262
264
|
|
|
263
|
-
const next =
|
|
264
|
-
|
|
265
|
-
if (
|
|
265
|
+
const next = withoutPath(current, keyPath);
|
|
266
|
+
if (next === current) return;
|
|
267
|
+
if (next === undefined) delete document[section];
|
|
266
268
|
else document[section] = next;
|
|
267
269
|
writeConfigFile(path, document);
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Copy `source` without the leaf at `path`, or `undefined` once nothing is left.
|
|
274
|
+
* Returns `source` itself when the leaf is absent or sits behind a non-object,
|
|
275
|
+
* so the caller can tell "nothing to delete" from "a deletion happened".
|
|
276
|
+
*/
|
|
277
|
+
function withoutPath(source: Record<string, unknown>, path: readonly string[]): Record<string, unknown> | undefined {
|
|
278
|
+
const [head, ...rest] = path;
|
|
279
|
+
if (head === undefined || !(head in source)) return source;
|
|
280
|
+
const next = { ...source };
|
|
281
|
+
if (rest.length === 0) {
|
|
282
|
+
delete next[head];
|
|
283
|
+
} else {
|
|
284
|
+
const child = asRecord(source[head]);
|
|
285
|
+
if (!child) return source;
|
|
286
|
+
const pruned = withoutPath(child, rest);
|
|
287
|
+
if (pruned === child) return source;
|
|
288
|
+
if (pruned === undefined) delete next[head];
|
|
289
|
+
else next[head] = pruned;
|
|
290
|
+
}
|
|
291
|
+
return Object.keys(next).length === 0 ? undefined : next;
|
|
292
|
+
}
|
|
293
|
+
|
|
270
294
|
/** Write a whole config document atomically (sibling temp file, then rename). */
|
|
271
295
|
export function writeConfigFile(path: string, document: Record<string, unknown>): void {
|
|
272
296
|
mkdirSync(dirname(path), { recursive: true });
|
package/lib/format.ts
CHANGED
|
@@ -49,6 +49,12 @@ export function formatDuration(ms: number): string {
|
|
|
49
49
|
return `${hours}h${String(minutes % 60).padStart(2, "0")}m`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** `1 tool`, `3 tools`: a tool count with the right word. */
|
|
53
|
+
export function formatToolCount(count: number): string {
|
|
54
|
+
const n = Number.isFinite(count) ? Math.max(0, Math.trunc(count)) : 0;
|
|
55
|
+
return `${formatCount(n)} ${n === 1 ? "tool" : "tools"}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
/**
|
|
53
59
|
* Replace terminal control characters (including ESC and the C1 block) with
|
|
54
60
|
* spaces, keeping `\n` and `\t` for callers that render multi-line text.
|
package/lib/requirements.ts
CHANGED
|
@@ -111,6 +111,21 @@ export interface SafetyCheck {
|
|
|
111
111
|
hint: string;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* A fact that ages: something checked by hand on a date, which is still worth
|
|
116
|
+
* trusting only for a while. The date arrives through a seam because `lib/` cannot
|
|
117
|
+
* import the extension that owns the record.
|
|
118
|
+
*/
|
|
119
|
+
export interface AgeCheck {
|
|
120
|
+
kind: "age";
|
|
121
|
+
/** What was verified, phrased for the report, e.g. `the built-in rate tables`. */
|
|
122
|
+
what: string;
|
|
123
|
+
/** Days after which the record counts as stale. */
|
|
124
|
+
maxAgeDays: number;
|
|
125
|
+
enables: string;
|
|
126
|
+
hint: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
114
129
|
export type RequirementCheck =
|
|
115
130
|
| BinaryCheck
|
|
116
131
|
| ModuleCheck
|
|
@@ -118,7 +133,8 @@ export type RequirementCheck =
|
|
|
118
133
|
| InvocationCheck
|
|
119
134
|
| NotifyCheck
|
|
120
135
|
| ProviderCheck
|
|
121
|
-
| SafetyCheck
|
|
136
|
+
| SafetyCheck
|
|
137
|
+
| AgeCheck;
|
|
122
138
|
|
|
123
139
|
/** What an extension needs, split by how badly it needs it. */
|
|
124
140
|
export interface ExtensionRequirements {
|
|
@@ -274,6 +290,13 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
|
|
|
274
290
|
bytefly: {
|
|
275
291
|
configFiles: [SHARED_CONFIG_FILE],
|
|
276
292
|
optional: [
|
|
293
|
+
{
|
|
294
|
+
kind: "age",
|
|
295
|
+
what: "the built-in DeepSeek rate tables",
|
|
296
|
+
maxAgeDays: 90,
|
|
297
|
+
enables: "the footer's cost total tracks the published tariff",
|
|
298
|
+
hint: "re-check https://api-docs.deepseek.com/quick_start/pricing against `footer/price/config.ts`, then update `PRICE_VERIFIED_AT`.",
|
|
299
|
+
},
|
|
277
300
|
{
|
|
278
301
|
kind: "notify",
|
|
279
302
|
enables: "the configured notification delivery",
|
|
@@ -328,6 +351,10 @@ export interface ProbeSeams {
|
|
|
328
351
|
reach?: (url: string) => Promise<{ ok: boolean; error?: string }>;
|
|
329
352
|
/** True when the caller asked for an explicit refresh, which is when a probe may touch the network. */
|
|
330
353
|
refresh?: boolean;
|
|
354
|
+
/** The day a hand-checked record was verified, as `YYYY-MM-DD`, when the extension records one. */
|
|
355
|
+
priceVerifiedAt?: () => string | undefined;
|
|
356
|
+
/** The clock the age check reads; defaults to the process clock. */
|
|
357
|
+
now?: () => number;
|
|
331
358
|
}
|
|
332
359
|
|
|
333
360
|
/**
|
|
@@ -494,6 +521,56 @@ function safetyFindings(
|
|
|
494
521
|
return [{ extension, check: label, severity: "ok", detail: check.enables }];
|
|
495
522
|
}
|
|
496
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Whole days between an ISO `YYYY-MM-DD` date and `now`, or undefined for a date
|
|
526
|
+
* that does not parse or does not exist on the calendar.
|
|
527
|
+
*
|
|
528
|
+
* A date up to a day in the future reads as zero days old: a record made in a
|
|
529
|
+
* timezone ahead of UTC is not a typo, and reporting it as missing would hide a
|
|
530
|
+
* fresh record. Anything further ahead is a typo, so it is not a date.
|
|
531
|
+
*/
|
|
532
|
+
export function ageInDays(date: string, now: number): number | undefined {
|
|
533
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return undefined;
|
|
534
|
+
const stamp = Date.parse(`${date}T00:00:00Z`);
|
|
535
|
+
if (!Number.isFinite(stamp)) return undefined;
|
|
536
|
+
// `Date.parse` accepts 2026-02-31 by rolling it into March; reject the roll.
|
|
537
|
+
if (new Date(stamp).toISOString().slice(0, 10) !== date) return undefined;
|
|
538
|
+
const days = Math.floor((now - stamp) / 86_400_000);
|
|
539
|
+
if (days >= 0) return days;
|
|
540
|
+
// One day of skew is a timezone, not a mistake.
|
|
541
|
+
return days >= -1 ? 0 : undefined;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** The findings for a fact that ages. */
|
|
545
|
+
function ageFindings(extension: string, check: AgeCheck, failSeverity: FindingSeverity, seams: ProbeSeams): Finding[] {
|
|
546
|
+
const label = check.what;
|
|
547
|
+
const date = seams.priceVerifiedAt?.();
|
|
548
|
+
const age = date === undefined ? undefined : ageInDays(date, (seams.now ?? Date.now)());
|
|
549
|
+
if (date === undefined || age === undefined) {
|
|
550
|
+
return [
|
|
551
|
+
{
|
|
552
|
+
extension,
|
|
553
|
+
check: label,
|
|
554
|
+
severity: "info",
|
|
555
|
+
detail: `no verification date is recorded for ${label}`,
|
|
556
|
+
},
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
const days = `${age} day${age === 1 ? "" : "s"} ago`;
|
|
560
|
+
if (age <= check.maxAgeDays) {
|
|
561
|
+
return [{ extension, check: label, severity: "ok", detail: `${label} last checked ${days}` }];
|
|
562
|
+
}
|
|
563
|
+
return [
|
|
564
|
+
{
|
|
565
|
+
extension,
|
|
566
|
+
check: label,
|
|
567
|
+
severity: failSeverity,
|
|
568
|
+
detail: `${label} were last checked ${days}, beyond the ${check.maxAgeDays}-day window — ${check.enables}`,
|
|
569
|
+
hint: check.hint,
|
|
570
|
+
},
|
|
571
|
+
];
|
|
572
|
+
}
|
|
573
|
+
|
|
497
574
|
/** Run one check. `failSeverity` comes from which list the check sits in. */
|
|
498
575
|
async function runCheck(
|
|
499
576
|
extension: string,
|
|
@@ -611,6 +688,8 @@ async function runCheck(
|
|
|
611
688
|
return providerFindings(extension, check, failSeverity, seams);
|
|
612
689
|
case "safety":
|
|
613
690
|
return safetyFindings(extension, check, failSeverity, seams);
|
|
691
|
+
case "age":
|
|
692
|
+
return ageFindings(extension, check, failSeverity, seams);
|
|
614
693
|
}
|
|
615
694
|
}
|
|
616
695
|
|
package/lib/tui/columns.ts
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { sliceByColumn, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
import { sanitizeLine } from "../format.ts";
|
|
14
|
+
|
|
15
|
+
/** The columns a label gets before it stops reading as a label. */
|
|
16
|
+
export const LABEL_WIDTH = 48;
|
|
13
17
|
|
|
14
18
|
/** A usable origin: terminal arithmetic starts from one column, never zero. */
|
|
15
19
|
function columns(width: number): number {
|
|
@@ -45,6 +49,19 @@ export function clipMiddle(text: string, width: number, ellipsis = "…"): strin
|
|
|
45
49
|
return clip(`${head}${ellipsis}${tail}`, w, ellipsis);
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/**
|
|
53
|
+
* A single-line label clipped to {@link LABEL_WIDTH}.
|
|
54
|
+
*
|
|
55
|
+
* The sanitizer runs first, then the clip: cutting the raw text first would put
|
|
56
|
+
* a cut inside an escape sequence a sanitizer can no longer recognize, and a
|
|
57
|
+
* newline would be left to break the row in two. The default is
|
|
58
|
+
* {@link sanitizeLine}; an extension with its own log- or file-authored text
|
|
59
|
+
* passes that sanitizer instead.
|
|
60
|
+
*/
|
|
61
|
+
export function labelLine(text: string, sanitize: (text: string) => string = sanitizeLine): string {
|
|
62
|
+
return clip(sanitize(text), LABEL_WIDTH);
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
/** Pad `text` on the right to exactly `width` columns. Never truncates. */
|
|
49
66
|
export function padTo(text: string, width: number): string {
|
|
50
67
|
const missing = columns(width) - visibleWidth(text);
|
package/lib/tui/tool.ts
CHANGED
|
@@ -136,7 +136,8 @@ export function moreHint(theme: Theme, hidden: number, noun = "lines"): string {
|
|
|
136
136
|
|
|
137
137
|
/** The render context fields the elapsed timer needs. */
|
|
138
138
|
export interface ToolRenderClock {
|
|
139
|
-
state:
|
|
139
|
+
/** Pi's per-call render state, opaque here: this module only ever casts it. */
|
|
140
|
+
state: unknown;
|
|
140
141
|
invalidate(): void;
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -191,7 +192,10 @@ export function elapsedSuffix(theme: Theme, durationMs: number | undefined, minM
|
|
|
191
192
|
* (the renderer's `isPartial` branch draws its own running line) and the host
|
|
192
193
|
* replaces it with the final result.
|
|
193
194
|
*/
|
|
194
|
-
export function startToolProgress(
|
|
195
|
+
export function startToolProgress(
|
|
196
|
+
// allow:any Pi's partial-result type is generic over each tool's details, so a shared helper cannot name it.
|
|
197
|
+
onUpdate: ((partial: any) => void) | undefined,
|
|
198
|
+
): void {
|
|
195
199
|
onUpdate?.({ content: [], details: undefined });
|
|
196
200
|
}
|
|
197
201
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gavin-hu/my-pi-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, document, memory, wechat, and bytefly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
"config:migrate": "bun run scripts/migrate-config.ts",
|
|
106
106
|
"compat:versions": "bun run scripts/peer-versions.ts",
|
|
107
107
|
"bench:render": "bun run scripts/render-bench.ts",
|
|
108
|
+
"screenshot:file-browse": "bun run scripts/file-browse-shot.ts",
|
|
108
109
|
"screenshot:plan": "bun run scripts/plan-review-shot.ts",
|
|
109
110
|
"screenshot:tools": "bun run scripts/tool-renderings.ts gallery assets/tool-renderings.png",
|
|
110
111
|
"screenshot:tool-pngs": "bun run scripts/tool-renderings.ts tools assets/tools",
|
package/themes/bytefly-dark.json
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"purple": "#bc8cff",
|
|
18
18
|
"pink": "#f778ba",
|
|
19
19
|
"selectionBg": "#17273a",
|
|
20
|
-
"customBg": "#26233a",
|
|
21
20
|
"successBg": "#12271a",
|
|
22
21
|
"errorBg": "#3d1a21",
|
|
23
22
|
"searchBg": "#3d331a",
|
|
@@ -43,9 +42,9 @@
|
|
|
43
42
|
"searchMatchText": "text",
|
|
44
43
|
"userMessageBg": "",
|
|
45
44
|
"userMessageText": "text",
|
|
46
|
-
"customMessageBg": "
|
|
45
|
+
"customMessageBg": "",
|
|
47
46
|
"customMessageText": "muted",
|
|
48
|
-
"customMessageLabel": "
|
|
47
|
+
"customMessageLabel": "blue",
|
|
49
48
|
"toolPendingBg": "canvasSubtle",
|
|
50
49
|
"toolSuccessBg": "successBg",
|
|
51
50
|
"toolErrorBg": "errorBg",
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"yellow": "#9a6700",
|
|
17
17
|
"purple": "#8250df",
|
|
18
18
|
"pink": "#bf3989",
|
|
19
|
-
"customBg": "#f5f1fd",
|
|
20
19
|
"successBg": "#e9f6ed",
|
|
21
20
|
"errorBg": "#fcf0f1",
|
|
22
21
|
"selectionBg": "#e8f2fc",
|
|
@@ -43,9 +42,9 @@
|
|
|
43
42
|
"searchMatchText": "text",
|
|
44
43
|
"userMessageBg": "",
|
|
45
44
|
"userMessageText": "text",
|
|
46
|
-
"customMessageBg": "
|
|
45
|
+
"customMessageBg": "",
|
|
47
46
|
"customMessageText": "muted",
|
|
48
|
-
"customMessageLabel": "
|
|
47
|
+
"customMessageLabel": "blue",
|
|
49
48
|
"toolPendingBg": "canvasSubtle",
|
|
50
49
|
"toolSuccessBg": "successBg",
|
|
51
50
|
"toolErrorBg": "errorBg",
|