@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
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import pkg from "../../../package.json";
|
|
12
|
-
import {
|
|
12
|
+
import { sanitizeLine } from "../../../lib/format.ts";
|
|
13
|
+
import { noticeMessage, type NoticeItem, type NoticeMessage } from "../../../lib/notice.ts";
|
|
13
14
|
import { GLYPHS } from "../../../lib/tui/glyphs.ts";
|
|
14
15
|
import type { UpdateOutcome } from "./state.ts";
|
|
15
|
-
import type { PackageUpdate } from "./types.ts";
|
|
16
|
+
import type { PackageUpdate, PiRelease } from "./types.ts";
|
|
16
17
|
|
|
17
18
|
/** The header marker: an up arrow and this package's newest version. */
|
|
18
19
|
export function updateMarker(latest: string): string {
|
|
@@ -22,6 +23,12 @@ export function updateMarker(latest: string): string {
|
|
|
22
23
|
/** The command that resolves every row in the notice. */
|
|
23
24
|
export const UPDATE_COMMAND = "pi update --extensions";
|
|
24
25
|
|
|
26
|
+
/** The command that updates Pi itself. */
|
|
27
|
+
export const PI_UPDATE_COMMAND = "pi update";
|
|
28
|
+
|
|
29
|
+
/** Where Pi publishes its own changelog; Pi hardcodes the same URL in its box. */
|
|
30
|
+
export const PI_CHANGELOG_URL = "https://pi.dev/changelog";
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* The release page for a version, or `undefined` when this checkout does not name
|
|
27
34
|
* a GitHub repository. Derived from `package.json` so a rename cannot leave a dead
|
|
@@ -73,14 +80,66 @@ function ownRelease(ownLatest?: string): string | undefined {
|
|
|
73
80
|
return ownLatest ? releaseUrl(ownLatest) : undefined;
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
/** The row that names the command resolving an update. */
|
|
84
|
+
function commandRow(command: string): NoticeItem {
|
|
85
|
+
return { label: "update", detail: command };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The row that names a page the answer points at, e.g. a release or a changelog. */
|
|
89
|
+
function linkRow(label: string, url: string): NoticeItem {
|
|
90
|
+
return { label, detail: url };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The tail both answers end with: the command to run, then where the notes are
|
|
95
|
+
* when they are known. Each caller supplies its own sentence, because the notice
|
|
96
|
+
* is read in the transcript and the re-check answer is read as one printed line.
|
|
97
|
+
*/
|
|
98
|
+
function actionParts(action: string, release?: string): string[] {
|
|
99
|
+
return release ? [action, `Release notes: ${release}`] : [action];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The notice that replaces Pi's own `Update Available` box.
|
|
104
|
+
*
|
|
105
|
+
* Pi resolves the release and then draws a bordered warning block; bytefly takes
|
|
106
|
+
* the same release and draws the shared transcript notice, so the news reads like
|
|
107
|
+
* every other bytefly message and states the version movement, the command, and
|
|
108
|
+
* the changelog. `note` is Pi's changelog excerpt when the release API sends one;
|
|
109
|
+
* it is a free-form paragraph, so it is sanitized to a single row rather than
|
|
110
|
+
* given a layout of its own.
|
|
111
|
+
*/
|
|
112
|
+
export function piReleaseNotice(release: PiRelease, current: string): NoticeMessage {
|
|
113
|
+
const note = release.note?.trim();
|
|
114
|
+
const parts = [
|
|
115
|
+
`Pi v${release.version} is available; this session runs v${current}.`,
|
|
116
|
+
`Run \`${PI_UPDATE_COMMAND}\`.`,
|
|
117
|
+
`Changelog: ${PI_CHANGELOG_URL}`,
|
|
118
|
+
];
|
|
119
|
+
if (note) parts.push(`Release notes: ${note}`);
|
|
120
|
+
return noticeMessage({
|
|
121
|
+
content: parts.join(" "),
|
|
122
|
+
details: {
|
|
123
|
+
source: "bytefly",
|
|
124
|
+
title: `pi v${release.version} is available`,
|
|
125
|
+
severity: "info",
|
|
126
|
+
items: [
|
|
127
|
+
{ label: "pi", detail: `v${current} → v${release.version}` },
|
|
128
|
+
commandRow(PI_UPDATE_COMMAND),
|
|
129
|
+
linkRow("changelog", PI_CHANGELOG_URL),
|
|
130
|
+
...(note ? [{ label: "release notes", detail: sanitizeLine(note) }] : []),
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
76
136
|
/** The single notice a session posts when Pi reports package updates. */
|
|
77
137
|
export function updateNotice(updates: readonly PackageUpdate[], current: string, ownLatest?: string): NoticeMessage {
|
|
78
138
|
const own = updates.find(isOwnPackage);
|
|
79
139
|
const release = own ? ownRelease(ownLatest) : undefined;
|
|
80
140
|
const parts = [namesLine(updates)];
|
|
81
141
|
if (own && ownLatest) parts.push(`${pkg.name} v${current} → v${ownLatest}.`);
|
|
82
|
-
parts.push(`Run \`${UPDATE_COMMAND}\` to update
|
|
83
|
-
if (release) parts.push(`Release notes: ${release}`);
|
|
142
|
+
parts.push(...actionParts(`Run \`${UPDATE_COMMAND}\` to update.`, release));
|
|
84
143
|
return noticeMessage({
|
|
85
144
|
content: parts.join(" "),
|
|
86
145
|
details: {
|
|
@@ -89,8 +148,8 @@ export function updateNotice(updates: readonly PackageUpdate[], current: string,
|
|
|
89
148
|
severity: "info",
|
|
90
149
|
items: [
|
|
91
150
|
...updates.map((update) => packageRow(update, current, ownLatest)),
|
|
92
|
-
|
|
93
|
-
...(release ? [
|
|
151
|
+
commandRow(UPDATE_COMMAND),
|
|
152
|
+
...(release ? [linkRow("release", release)] : []),
|
|
94
153
|
],
|
|
95
154
|
},
|
|
96
155
|
});
|
|
@@ -120,8 +179,7 @@ export function updateOutcomeText(
|
|
|
120
179
|
const release = own ? ownRelease(outcome.ownLatest) : undefined;
|
|
121
180
|
const parts = [`bytefly: ${namesLine(outcome.updates)}`];
|
|
122
181
|
if (own && outcome.ownLatest) parts.push(`This package is v${current} → v${outcome.ownLatest}.`);
|
|
123
|
-
parts.push(`Run ${UPDATE_COMMAND}
|
|
124
|
-
if (release) parts.push(`Release notes: ${release}`);
|
|
182
|
+
parts.push(...actionParts(`Run ${UPDATE_COMMAND}.`, release));
|
|
125
183
|
return { text: parts.join(" "), severity: "info" };
|
|
126
184
|
}
|
|
127
185
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way bytefly replaces a host method it has to own.
|
|
3
|
+
*
|
|
4
|
+
* Pi draws two update blocks from methods an extension can reach but cannot turn
|
|
5
|
+
* off: its package list from `DefaultPackageManager.prototype`, and its
|
|
6
|
+
* `Update Available` box from `InteractiveMode.prototype`. Both replacements are
|
|
7
|
+
* the same operation, so the rules that make it safe live here once instead of
|
|
8
|
+
* being restated per surface:
|
|
9
|
+
*
|
|
10
|
+
* - The patch is process-wide, so it is installed at `session_start` and restored
|
|
11
|
+
* at `session_shutdown`; the record travels on the target under a per-method
|
|
12
|
+
* `Symbol.for` key, which survives the isolated module caches `lib/` is loaded
|
|
13
|
+
* with and makes a second registration reuse the first patch.
|
|
14
|
+
* - A restore only puts the original back while the patch is still the method
|
|
15
|
+
* installed, so a host or another extension that replaced it later keeps its
|
|
16
|
+
* own version.
|
|
17
|
+
* - A method the host does not have makes the patch inert rather than an error.
|
|
18
|
+
* The caller's surface then falls back to what the host draws, which is always
|
|
19
|
+
* correct and merely less styled.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** One installed patch, and the method it replaced. */
|
|
23
|
+
export interface MethodPatch<F> {
|
|
24
|
+
/** The method as it was before the patch; `undefined` when the host lacks it. */
|
|
25
|
+
readonly original: F | undefined;
|
|
26
|
+
/** Put the original back while this patch is still installed; safe to call more than once. */
|
|
27
|
+
restore(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The marker for one method's patch, stable across isolated module caches. */
|
|
31
|
+
function markerFor(method: string): symbol {
|
|
32
|
+
return Symbol.for(`@gavin-hu/my-pi-agent/host-patch/${method}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Replace `target[method]` with `replacement`.
|
|
37
|
+
*
|
|
38
|
+
* `target` is a prototype, a seam a test can supply as a plain object. The
|
|
39
|
+
* returned patch carries the captured original, because a caller usually needs it
|
|
40
|
+
* for something: the package check still serves bytefly's own query, and the
|
|
41
|
+
* release notification is the fallback when bytefly's rendering throws.
|
|
42
|
+
*/
|
|
43
|
+
export function patchPrototypeMethod<T extends object, K extends keyof T & string>(
|
|
44
|
+
target: T,
|
|
45
|
+
method: K,
|
|
46
|
+
replacement: T[K],
|
|
47
|
+
): MethodPatch<T[K]> {
|
|
48
|
+
const marker = markerFor(method);
|
|
49
|
+
const store = target as unknown as Record<symbol, MethodPatch<T[K]> | undefined>;
|
|
50
|
+
const existing = store[marker];
|
|
51
|
+
if (existing) return existing;
|
|
52
|
+
|
|
53
|
+
const original = target[method];
|
|
54
|
+
if (typeof original !== "function") return { original: undefined, restore: () => {} };
|
|
55
|
+
|
|
56
|
+
const patch: MethodPatch<T[K]> = {
|
|
57
|
+
original,
|
|
58
|
+
restore() {
|
|
59
|
+
if (target[method] === replacement) target[method] = original;
|
|
60
|
+
delete store[marker];
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
target[method] = replacement;
|
|
64
|
+
store[marker] = patch;
|
|
65
|
+
return patch;
|
|
66
|
+
}
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "./cache.ts";
|
|
29
29
|
import { fetchAvailableUpdates, fetchLatestVersion } from "./check.ts";
|
|
30
30
|
import { isOwnPackage, updateNotice } from "./format.ts";
|
|
31
|
-
import type
|
|
31
|
+
import { paintLatest, type UpdateOutcome, type UpdateSource } from "./state.ts";
|
|
32
32
|
import { suppressPiPackageUpdates, type UpdateSuppression } from "./suppress.ts";
|
|
33
33
|
import type { PackageUpdate, PackageUpdateCheck } from "./types.ts";
|
|
34
34
|
import { isNewerVersion } from "./version.ts";
|
|
@@ -125,13 +125,6 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
|
|
|
125
125
|
return entry.updates.some(isOwnPackage) ? entry.latest : undefined;
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
-
/** Paint the header marker, or clear it when this package is current. */
|
|
129
|
-
const paint = (latest?: string): void => {
|
|
130
|
-
if (updates.latest() === latest) return;
|
|
131
|
-
updates.set(latest);
|
|
132
|
-
deps.onUpdate?.();
|
|
133
|
-
};
|
|
134
|
-
|
|
135
128
|
/** The once-per-load transcript notice, posted only for a non-empty list. */
|
|
136
129
|
const announce = (entry: UpdateCacheEntry): void => {
|
|
137
130
|
if (announced || entry.updates.length === 0) return;
|
|
@@ -141,13 +134,13 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
|
|
|
141
134
|
|
|
142
135
|
/** One snapshot: paint the marker and, when it has rows, announce it once. */
|
|
143
136
|
const record = (entry: UpdateCacheEntry): void => {
|
|
144
|
-
|
|
137
|
+
paintLatest(updates, ownLatestOf(entry), deps.onUpdate);
|
|
145
138
|
announce(entry);
|
|
146
139
|
};
|
|
147
140
|
|
|
148
141
|
/** Offline: the cached marker is advisory, but no notice is posted. */
|
|
149
142
|
const recordMarkerOnly = (entry: UpdateCacheEntry): void => {
|
|
150
|
-
|
|
143
|
+
paintLatest(updates, ownLatestOf(entry), deps.onUpdate);
|
|
151
144
|
};
|
|
152
145
|
|
|
153
146
|
/** Resolve this package's version label, dropping an equal or older answer. */
|
|
@@ -238,7 +231,7 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
|
|
|
238
231
|
};
|
|
239
232
|
writeCache(entry);
|
|
240
233
|
// The caller prints the answer, so this path never posts a notice.
|
|
241
|
-
|
|
234
|
+
paintLatest(updates, latest, deps.onUpdate);
|
|
242
235
|
return list.length > 0
|
|
243
236
|
? { kind: "available", updates: list, ...(latest ? { ownLatest: latest } : {}) }
|
|
244
237
|
: { kind: "current" };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi's own release notice, re-drawn as bytefly's.
|
|
3
|
+
*
|
|
4
|
+
* Pi resolves its new version from `pi.dev`, then draws a bordered `Update
|
|
5
|
+
* Available` block. This lifecycle replaces the method that draws it, so the
|
|
6
|
+
* release Pi already fetched becomes one shared transcript notice and a `↑`
|
|
7
|
+
* marker on the header's Pi line. No request of bytefly's own is made, and the
|
|
8
|
+
* release's changelog note, when Pi's API sends one, travels with it.
|
|
9
|
+
*
|
|
10
|
+
* The patch is installed at `session_start` in `tui` mode — `run()` starts the
|
|
11
|
+
* version check only after the session is up — and restored at
|
|
12
|
+
* `session_shutdown`, the same shape as the package-update surface. A release
|
|
13
|
+
* that lands after the session is gone is dropped rather than posted into it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { type ExtensionAPI, VERSION } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { piReleaseNotice } from "./format.ts";
|
|
18
|
+
import { paintLatest, type UpdateSource } from "./state.ts";
|
|
19
|
+
import { suppressPiReleaseNotice, type ReleaseNoticeShim } from "./suppress.ts";
|
|
20
|
+
import type { PiRelease, ReleaseNoticePrototype } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
/** Test seams; production patches Pi's interactive mode and reads its version. */
|
|
23
|
+
export interface ReleaseDeps {
|
|
24
|
+
/** Replace the host method bytefly patches; defaults to `InteractiveMode.prototype`. */
|
|
25
|
+
prototype?: ReleaseNoticePrototype;
|
|
26
|
+
/** Replace the running Pi version; defaults to the host's `VERSION`. */
|
|
27
|
+
currentVersion?: string;
|
|
28
|
+
/** Called once a version is recorded, so a mounted header repaints. */
|
|
29
|
+
onUpdate?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register the release-notice lifecycle. `latest` is the store the header reads,
|
|
34
|
+
* so recording Pi's new version is what puts the marker on the agent line.
|
|
35
|
+
*/
|
|
36
|
+
export function registerPiRelease(pi: ExtensionAPI, latest: UpdateSource, deps: ReleaseDeps = {}): void {
|
|
37
|
+
const current = deps.currentVersion ?? VERSION;
|
|
38
|
+
let shim: ReleaseNoticeShim | undefined;
|
|
39
|
+
let live = false;
|
|
40
|
+
// Pi checks once per process, but a reload re-registers this surface: a second
|
|
41
|
+
// notice in the same load would be a duplicate, not news.
|
|
42
|
+
let announced = false;
|
|
43
|
+
|
|
44
|
+
const record = (release: PiRelease): void => {
|
|
45
|
+
if (!live) return;
|
|
46
|
+
paintLatest(latest, release.version, deps.onUpdate);
|
|
47
|
+
if (announced) return;
|
|
48
|
+
announced = true;
|
|
49
|
+
pi.sendMessage(piReleaseNotice(release, current));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
pi.on("session_start", (_event, ctx) => {
|
|
53
|
+
// Only the interactive mode draws Pi's box, so only there is there one to replace.
|
|
54
|
+
if (ctx.mode !== "tui") return;
|
|
55
|
+
live = true;
|
|
56
|
+
shim ??= suppressPiReleaseNotice(record, deps.prototype);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
pi.on("session_shutdown", () => {
|
|
60
|
+
live = false;
|
|
61
|
+
shim?.restore();
|
|
62
|
+
shim = undefined;
|
|
63
|
+
latest.set();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -40,3 +40,17 @@ export function createUpdateState(): UpdateSource {
|
|
|
40
40
|
},
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Record a version in the store and repaint the surface that reads it.
|
|
46
|
+
*
|
|
47
|
+
* The comparison is what keeps this cheap and quiet: a check that finds the same
|
|
48
|
+
* version again must not re-install the header, and clearing an unknown version
|
|
49
|
+
* must still clear the marker. `onUpdate` is the surface's repaint hook, so a
|
|
50
|
+
* registration that has no mounted surface can omit it.
|
|
51
|
+
*/
|
|
52
|
+
export function paintLatest(store: UpdateSource, latest: string | undefined, onUpdate?: () => void): void {
|
|
53
|
+
if (store.latest() === latest) return;
|
|
54
|
+
store.set(latest);
|
|
55
|
+
onUpdate?.();
|
|
56
|
+
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pi
|
|
2
|
+
* Pi draws two update blocks bytefly replaces, and this module expresses both
|
|
3
|
+
* through the install/restore rules in `patch.ts`.
|
|
3
4
|
*
|
|
4
5
|
* Pi draws `Package Updates Available` from `InteractiveMode.run()` after the
|
|
5
6
|
* session starts, with no setting or extension event to turn it off. The one
|
|
6
7
|
* public seam is the package manager it calls: replacing
|
|
7
8
|
* `checkForAvailableUpdates` with an empty result suppresses that block, while
|
|
8
|
-
* the captured original still serves bytefly's own query.
|
|
9
|
-
* process-wide, so it is installed at `session_start` and restored at
|
|
10
|
-
* `session_shutdown`.
|
|
9
|
+
* the captured original still serves bytefly's own query.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Pi draws `Update Available` from the same `run()`, through a public method on
|
|
12
|
+
* `InteractiveMode`; replacing that method hands bytefly the release Pi resolved
|
|
13
|
+
* (its version and notes) so it can draw the shared notice instead.
|
|
14
|
+
*
|
|
15
|
+
* A future host that changes either shape makes that patch a no-op rather than an
|
|
16
|
+
* error; the update surface then falls back to Pi's own block.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
|
-
import { DefaultPackageManager } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import
|
|
19
|
+
import { DefaultPackageManager, InteractiveMode } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { patchPrototypeMethod } from "./patch.ts";
|
|
21
|
+
import type { PackageUpdate, PackageUpdateCheck, PiRelease, ReleaseNoticePrototype } from "./types.ts";
|
|
18
22
|
|
|
19
23
|
/** A patchable class prototype, kept narrow so a test needs no host object. */
|
|
20
24
|
export interface PackageManagerPrototype {
|
|
@@ -29,12 +33,8 @@ export interface UpdateSuppression {
|
|
|
29
33
|
restore(): void;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
/**
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
interface MarkedPrototype extends PackageManagerPrototype {
|
|
36
|
-
[SUPPRESSION]?: UpdateSuppression;
|
|
37
|
-
}
|
|
36
|
+
/** The replacement the patch installs, so a test can tell it from Pi's method. */
|
|
37
|
+
const SUPPRESSED: PackageUpdateCheck = async () => [];
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Replace Pi's package-update check with one that reports nothing.
|
|
@@ -46,21 +46,39 @@ interface MarkedPrototype extends PackageManagerPrototype {
|
|
|
46
46
|
export function suppressPiPackageUpdates(
|
|
47
47
|
prototype: PackageManagerPrototype = DefaultPackageManager.prototype as PackageManagerPrototype,
|
|
48
48
|
): UpdateSuppression {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const patch = patchPrototypeMethod(prototype, "checkForAvailableUpdates", SUPPRESSED);
|
|
50
|
+
if (!patch.original) return { check: async () => [], restore: patch.restore };
|
|
51
|
+
|
|
52
|
+
const originalCheck = patch.original as unknown as (this: DefaultPackageManager) => Promise<PackageUpdate[]>;
|
|
53
|
+
return { check: (manager) => originalCheck.call(manager), restore: patch.restore };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The installed release-notice shim, and how to give Pi its method back. */
|
|
57
|
+
export interface ReleaseNoticeShim {
|
|
58
|
+
/** Put Pi's method back; safe to call more than once. */
|
|
59
|
+
restore(): void;
|
|
60
|
+
}
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Hand Pi's `Update Available` release to `notify` instead of letting Pi draw it.
|
|
64
|
+
*
|
|
65
|
+
* `prototype` is a seam for tests; production passes `InteractiveMode.prototype`.
|
|
66
|
+
* When the host has no such method the shim is inert and Pi's box is what shows,
|
|
67
|
+
* which is exactly the state this replaces. A `notify` that throws is also a
|
|
68
|
+
* fallback to Pi's box: a release nobody hears about is worse than Pi's own
|
|
69
|
+
* rendering of it, and the error itself is not allowed to escape into `run()`.
|
|
70
|
+
*/
|
|
71
|
+
export function suppressPiReleaseNotice(
|
|
72
|
+
notify: (release: PiRelease) => void,
|
|
73
|
+
prototype: ReleaseNoticePrototype = InteractiveMode.prototype as ReleaseNoticePrototype,
|
|
74
|
+
): ReleaseNoticeShim {
|
|
75
|
+
const original = prototype.showNewVersionNotification as (this: ReleaseNoticePrototype, release: PiRelease) => void;
|
|
76
|
+
const patched = function (this: ReleaseNoticePrototype, release: PiRelease): void {
|
|
77
|
+
try {
|
|
78
|
+
notify(release);
|
|
79
|
+
} catch {
|
|
80
|
+
if (typeof original === "function") original.call(this, release);
|
|
81
|
+
}
|
|
61
82
|
};
|
|
62
|
-
|
|
63
|
-
prototype.checkForAvailableUpdates = patched;
|
|
64
|
-
marked[SUPPRESSION] = suppression;
|
|
65
|
-
return suppression;
|
|
83
|
+
return patchPrototypeMethod(prototype, "showNewVersionNotification", patched);
|
|
66
84
|
}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The update shapes Pi publishes, derived from the public methods that carry
|
|
3
|
+
* them.
|
|
3
4
|
*
|
|
4
|
-
* Pi exports the manager
|
|
5
|
-
*
|
|
6
|
-
* without reaching into a dist path or
|
|
5
|
+
* Pi exports the package manager and the interactive mode but not the interfaces
|
|
6
|
+
* they pass around, so each type here is read off a public signature. That keeps
|
|
7
|
+
* the extension on the same contract without reaching into a dist path or
|
|
8
|
+
* restating the fields a host version may change.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
import type { DefaultPackageManager } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { DefaultPackageManager, InteractiveMode } from "@earendil-works/pi-coding-agent";
|
|
10
12
|
|
|
11
13
|
/** One package Pi reports as updateable. */
|
|
12
14
|
export type PackageUpdate = Awaited<ReturnType<DefaultPackageManager["checkForAvailableUpdates"]>>[number];
|
|
13
15
|
|
|
14
16
|
/** Pi's check, captured before suppression and called with a manager. */
|
|
15
17
|
export type PackageUpdateCheck = (manager: DefaultPackageManager) => Promise<PackageUpdate[]>;
|
|
18
|
+
|
|
19
|
+
/** A newer Pi release as Pi itself resolved it: the version, and its notes when any. */
|
|
20
|
+
export type PiRelease = Parameters<InteractiveMode["showNewVersionNotification"]>[0];
|
|
21
|
+
|
|
22
|
+
/** The notification method bytefly replaces, narrow enough for a test to fake. */
|
|
23
|
+
export interface ReleaseNoticePrototype {
|
|
24
|
+
showNewVersionNotification(release: PiRelease): void;
|
|
25
|
+
}
|
package/extensions/check/tui.ts
CHANGED
|
@@ -79,6 +79,8 @@ export class CheckRunComponent implements Component {
|
|
|
79
79
|
});
|
|
80
80
|
if (body.length === 0) body.push(...emptyState(this.theme, w, { label: "Starting…" }));
|
|
81
81
|
return dockListFrame({
|
|
82
|
+
theme: this.theme,
|
|
83
|
+
width: w,
|
|
82
84
|
header: titleRule(this.theme, w, "Check"),
|
|
83
85
|
summary: clip(
|
|
84
86
|
` ${paint(this.theme, "meta", `running${SEPARATORS.item}${sanitizeLine(this.options.command)}${SEPARATORS.item}${formatDuration(this.options.elapsedMs())}`)}`,
|
|
@@ -16,15 +16,19 @@ pi install ./ # install the package
|
|
|
16
16
|
- Registers one command, `/context`. In the TUI it opens a docked screen; in
|
|
17
17
|
`rpc`, `json`, and `print` it notifies the same facts as plain text.
|
|
18
18
|
- Reports Pi's own estimate first — tokens, window, percentage, free space, the
|
|
19
|
-
auto-compaction threshold, and the headroom left before it
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
auto-compaction threshold, and the headroom left before it while compaction is
|
|
20
|
+
on — then the composition under a header that names the composition's own
|
|
21
|
+
estimate: the system prompt, the active tool definitions, the conversation by
|
|
22
|
+
role, injected pack context grouped by type, summaries, and shell output.
|
|
22
23
|
- Names the difference between Pi's total and the composition estimate instead
|
|
23
|
-
of forcing the parts to add up:
|
|
24
|
-
the
|
|
24
|
+
of forcing the parts to add up: one line with both figures, `unattributed` when
|
|
25
|
+
the host's total is the larger one, and `host counts messages only` before the
|
|
26
|
+
first reply, when Pi's fallback leaves the prompt and the tools out.
|
|
27
|
+
- Rounds each bar to the nearest cell, so a bar sits within half a cell of the
|
|
28
|
+
percentage printed beside it; the percentage itself is exact.
|
|
25
29
|
- Reads the branch's compaction history: how many compactions there have been,
|
|
26
30
|
what the newest one cut from and to, and how much its summary costs.
|
|
27
|
-
- Lists the five largest single
|
|
31
|
+
- Lists the five largest single messages, so a report that is mostly one tool
|
|
28
32
|
result says so.
|
|
29
33
|
- Compacts from the report: `c` on the screen asks first, `/context compact
|
|
30
34
|
[instructions]` runs directly. Both refuse while the agent is busy — a
|
|
@@ -58,7 +62,7 @@ for.
|
|
|
58
62
|
|
|
59
63
|
`/context` works everywhere. The screen needs `ctx.mode === "tui"`; the other
|
|
60
64
|
modes notify the same report as text, including the composition rows and the
|
|
61
|
-
largest
|
|
65
|
+
largest messages. `/context compact` is the same in every mode and reports through
|
|
62
66
|
`ctx.ui.notify` when it finishes or fails.
|
|
63
67
|
|
|
64
68
|
## How the numbers are computed
|
|
@@ -67,13 +71,14 @@ largest blocks. `/context compact` is the same in every mode and reports through
|
|
|
67
71
|
|---|---|
|
|
68
72
|
| Headline (`tokens`, `percent`) | `ctx.getContextUsage()`: the last valid provider usage plus Pi's estimate of the messages after it. `tokens` is `null` between a compaction and the next successful reply, because the last usage describes the pre-compaction context. |
|
|
69
73
|
| Window | `getContextUsage().contextWindow`, then the active model's `contextWindow`. |
|
|
74
|
+
| Headroom | `window - reserve - tokens`, `null` when the total is unknown or automatic compaction is off — there is no threshold in effect. |
|
|
70
75
|
| System prompt | A `system` message in the projection when one exists, otherwise `ctx.getSystemPrompt()` at characters/4. |
|
|
71
76
|
| Tool definitions | `pi.getAllTools()` filtered to `pi.getActiveTools()`, over each active tool's name, description, and parameter schema at characters/4. `promptGuidelines` are counted in the system prompt instead, because Pi renders them into its rules section. |
|
|
72
77
|
| Conversation rows | `sessionManager.buildSessionProjection().messages`, each counted with Pi's own `estimateTokens` (characters/4; an image is 4800 characters; a tool call is its name plus its JSON arguments). |
|
|
73
78
|
| Injected context | `custom` messages grouped by `customType`, biggest first; the five largest types are named and the rest fold into one row. |
|
|
74
79
|
| Threshold | `compaction.modelOverrides["provider/id"]`, then `compaction`, then `DEFAULT_COMPACTION_SETTINGS` — the same three-step fallback Pi uses. |
|
|
75
80
|
| Compaction history | `getLatestCompactionEntry(branch)` and the branch's `compaction` count. The summary's cost is the summary text at characters/4. |
|
|
76
|
-
| Drift | `tokens - estimate
|
|
81
|
+
| Drift | `tokens - estimate`, printed with both figures. A positive value appends `+… unattributed` — usage the rows do not explain; a negative one appends `host counts messages only` while the projection holds no assistant message, because Pi's message-only fallback omits the system prompt and the tool definitions. |
|
|
77
82
|
|
|
78
83
|
## Limitations
|
|
79
84
|
|
|
@@ -92,8 +97,13 @@ largest blocks. `/context compact` is the same in every mode and reports through
|
|
|
92
97
|
Once a reply exists the host total includes them.
|
|
93
98
|
- Without a model whose `contextWindow` is known there is no threshold to show;
|
|
94
99
|
the report says so and leaves compaction to Pi.
|
|
95
|
-
-
|
|
96
|
-
|
|
100
|
+
- A percentage is computed from unrounded tokens and shown to one decimal, while
|
|
101
|
+
the token column is compact (`5.1k`), so re-deriving a percentage from the
|
|
102
|
+
printed figures can differ by a couple of points. A bar rounds to the nearest
|
|
103
|
+
cell and a non-zero share always gets one, so a `0.2%` row draws one cell
|
|
104
|
+
beside its label.
|
|
105
|
+
- Descriptors in **Largest messages** are one line, clipped to 40 characters; a
|
|
106
|
+
long tool result is represented by its tool name, not its content.
|
|
97
107
|
|
|
98
108
|
## Non-goals
|
|
99
109
|
|
|
@@ -146,7 +156,7 @@ largest blocks. `/context compact` is the same in every mode and reports through
|
|
|
146
156
|
| `index.ts` | Factory: the `PI_DISABLED_EXTENSIONS` guard and command registration. |
|
|
147
157
|
| `commands.ts` | `/context` and `/context compact`: the TUI mount, the text fallback, and the idle guard. |
|
|
148
158
|
| `collect.ts` | The host boundary: one read of usage, prompt, tools, settings, projection, and branch. |
|
|
149
|
-
| `report.ts` | The pure builder: composition, reserve resolution, compaction history, largest
|
|
159
|
+
| `report.ts` | The pure builder: composition, reserve resolution, compaction history, largest messages. |
|
|
150
160
|
| `format.ts` | The plain-text report for non-TUI modes. |
|
|
151
161
|
| `tui.ts` | `ContextComponent`: gauge, rows, scroll, confirmation, feedback. |
|
|
152
162
|
| `types.ts` | `ContextInputs`, `ContextReport`, buckets, and the host types named through `ReturnType<…>`. |
|
|
@@ -48,11 +48,11 @@ export function formatCompaction(report: ContextReport): string {
|
|
|
48
48
|
|
|
49
49
|
/** The difference between the host's total and the composition estimate, when it is not zero. */
|
|
50
50
|
export function formatDrift(report: ContextReport): string | undefined {
|
|
51
|
-
|
|
52
|
-
if (report.drift
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return
|
|
51
|
+
const tokens = report.tokens;
|
|
52
|
+
if (report.drift === null || report.drift === 0 || tokens === null) return undefined;
|
|
53
|
+
const base = `Estimate drift · rows ${formatTokens(report.estimate)} vs host ${formatTokens(tokens)}`;
|
|
54
|
+
if (report.drift > 0) return `${base} · +${formatTokens(report.drift)} unattributed`;
|
|
55
|
+
return report.beforeFirstReply ? `${base} · host counts messages only` : base;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** One composition row, as plain text. */
|
|
@@ -65,10 +65,12 @@ export function formatBucket(bucket: ContextBucket): string {
|
|
|
65
65
|
return `${indent}${label}${tokens}${share}${note}`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** The report as a notification body: headline, budget, rows, then what happened. */
|
|
68
|
+
/** The report as a notification body: headline, budget, composition, rows, then what happened. */
|
|
69
69
|
export function formatReport(report: ContextReport): string {
|
|
70
70
|
const lines = [formatHeadline(report), formatBudget(report)];
|
|
71
|
-
if (report.buckets.length > 0)
|
|
71
|
+
if (report.buckets.length > 0) {
|
|
72
|
+
lines.push("", `Composition · ${formatTokens(report.estimate)} estimate`, ...report.buckets.map(formatBucket));
|
|
73
|
+
}
|
|
72
74
|
const drift = formatDrift(report);
|
|
73
75
|
if (drift) lines.push("", drift);
|
|
74
76
|
lines.push("", `Compaction · ${formatCompaction(report)}`);
|
|
@@ -76,7 +78,7 @@ export function formatReport(report: ContextReport): string {
|
|
|
76
78
|
const blocks = report.largest
|
|
77
79
|
.map((block) => `${formatTokens(block.tokens)} ${block.role} ${block.label}`)
|
|
78
80
|
.join(" · ");
|
|
79
|
-
lines.push(`Largest · ${blocks}`);
|
|
81
|
+
lines.push("", `Largest · ${blocks}`);
|
|
80
82
|
}
|
|
81
83
|
return lines.join("\n");
|
|
82
84
|
}
|
|
@@ -94,7 +94,7 @@ function firstText(content: unknown): string {
|
|
|
94
94
|
return "";
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/** A one-line, bounded descriptor for the "largest
|
|
97
|
+
/** A one-line, bounded descriptor for the "largest messages" list. */
|
|
98
98
|
function boundedLabel(text: string, maxLength = 40): string {
|
|
99
99
|
const line = sanitizeLine(text);
|
|
100
100
|
return line.length > maxLength ? `${line.slice(0, maxLength - 1)}…` : line;
|
|
@@ -184,7 +184,10 @@ export function buildReport(inputs: ContextInputs): ContextReport {
|
|
|
184
184
|
const percent = inputs.usage?.percent ?? (tokens !== null && window > 0 ? (tokens / window) * 100 : null);
|
|
185
185
|
const free = tokens !== null && window > 0 ? window - tokens : null;
|
|
186
186
|
const autoCompactAt = window > 0 ? Math.max(0, window - resolved.reserve) : 0;
|
|
187
|
-
const headroom =
|
|
187
|
+
const headroom = resolved.enabled && tokens !== null && window > 0 ? autoCompactAt - tokens : null;
|
|
188
|
+
// Pi's total is a message-only estimate until an assistant reply reports real
|
|
189
|
+
// usage, so the drift row names that cause instead of calling the gap noise.
|
|
190
|
+
const beforeFirstReply = !inputs.messages.some((message) => roleOf(message) === "assistant");
|
|
188
191
|
|
|
189
192
|
const messageCounts = new Map<string, number>();
|
|
190
193
|
const messageTokens = new Map<string, number>();
|
|
@@ -267,12 +270,21 @@ export function buildReport(inputs: ContextInputs): ContextReport {
|
|
|
267
270
|
const children = [...injectedTokens.entries()].sort((a, b) => b[1] - a[1]);
|
|
268
271
|
const shown = children.slice(0, 5);
|
|
269
272
|
const rest = children.slice(5);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
273
|
+
// A child's count is informative only when it divides the parent's; one
|
|
274
|
+
// injected type would otherwise repeat the same `1 message` twice.
|
|
275
|
+
const childNote = (customType: string): string | undefined => {
|
|
276
|
+
const count = injectedCounts.get(customType) ?? 0;
|
|
277
|
+
return count > 0 && count < injectedCount ? countNote(count) : undefined;
|
|
278
|
+
};
|
|
279
|
+
const childDrafts: BucketDraft[] = shown.map(([customType, estimateForType]) => {
|
|
280
|
+
const note = childNote(customType);
|
|
281
|
+
return {
|
|
282
|
+
id: `injected:${customType}`,
|
|
283
|
+
label: customType,
|
|
284
|
+
tokens: estimateForType,
|
|
285
|
+
...(note ? { note } : {}),
|
|
286
|
+
};
|
|
287
|
+
});
|
|
276
288
|
if (rest.length > 0) {
|
|
277
289
|
childDrafts.push({
|
|
278
290
|
id: "injected:other",
|
|
@@ -313,6 +325,7 @@ export function buildReport(inputs: ContextInputs): ContextReport {
|
|
|
313
325
|
buckets,
|
|
314
326
|
estimate,
|
|
315
327
|
drift: tokens !== null ? tokens - estimate : null,
|
|
328
|
+
beforeFirstReply,
|
|
316
329
|
compaction: compactionHistory(inputs),
|
|
317
330
|
largest,
|
|
318
331
|
};
|