@mgiles/perk 2.3.0 → 3.0.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 +1 -1
- package/extension/adapters/planAdapterPlannotator.ts +132 -91
- package/extension/adapters/planAdapterTombell.ts +5 -3
- package/extension/doors/address.ts +235 -37
- package/extension/doors/annotationPush.ts +924 -0
- package/extension/doors/auditWaveTools.ts +352 -0
- package/extension/doors/ciExecutor.ts +220 -55
- package/extension/doors/draftReviewWaveTools.ts +384 -0
- package/extension/doors/harvestWaveTools.ts +279 -0
- package/extension/doors/land.ts +1 -0
- package/extension/doors/learn.ts +24 -6
- package/extension/doors/learnFactory.ts +33 -7
- package/extension/doors/lifecycleGates.ts +1 -1
- package/extension/doors/objectiveReviewBrowser.ts +514 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +453 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +26 -16
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +25 -18
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +374 -0
- package/extension/doors/selfcheck.ts +1 -1
- package/extension/doors/submit.ts +82 -7
- package/extension/factories/gistAuthor.ts +4 -4
- package/extension/factories/gistDraft.ts +1 -1
- package/extension/factories/gistSave.ts +2 -2
- package/extension/factories/objective.ts +7 -7
- package/extension/factories/objectiveAuthor.ts +5 -4
- package/extension/factories/objectiveDraft.ts +61 -6
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +16 -1
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +131 -36
- package/extension/factories/planTitle.ts +27 -9
- package/extension/hunkFeedback/inbox.ts +389 -0
- package/extension/hunkFeedback/perkFeedback.ts +364 -0
- package/extension/hunkFeedback/receiver.ts +296 -0
- package/extension/hunkFeedback/store.ts +533 -0
- package/extension/index.ts +109 -44
- package/extension/substrate/cache.ts +75 -22
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/config.ts +36 -12
- package/extension/substrate/git.ts +10 -0
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +13 -5
- package/extension/substrate/sessionData.ts +3 -3
- package/extension/substrate/sessionPointers.ts +3 -3
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +170 -23
- package/extension/substrate/workflowState.ts +6 -0
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/surfaces.ts +98 -282
- package/extension/vendor/btw/btw.ts +57 -12
- package/extension/waves/adversarialReviewWave.ts +164 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +189 -0
- package/extension/waves/harvestWave.ts +399 -0
- package/extension/waves/memoryAdapter.ts +14 -1
- package/extension/waves/objectiveExplorerWave.ts +120 -0
- package/extension/waves/prReviewDynamicWave.ts +291 -61
- package/extension/waves/prReviewWave.ts +58 -13
- package/extension/waves/reportWave.ts +406 -106
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +3 -3
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +15 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +66 -65
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +1 -2
- package/prompts/contexts/adapters/plannotator-plan.md +1 -2
- package/prompts/contexts/gist-authoring.md +6 -20
- package/prompts/contexts/objective-authoring.md +6 -18
- package/prompts/stages/address/action.md +3 -14
- package/prompts/stages/address/preview.md +2 -13
- package/prompts/stages/audit.md +18 -0
- package/prompts/stages/gist-author/seed.md +4 -4
- package/prompts/stages/gist-save.md +1 -1
- package/prompts/stages/implement.md +3 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-harvest.md +15 -0
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +5 -3
- package/prompts/stages/objective-author/file.md +5 -4
- package/prompts/stages/objective-author/seed.md +5 -4
- package/prompts/stages/objective-land.md +7 -0
- package/prompts/stages/objective-plan/guidance.md +2 -13
- package/prompts/stages/objective-plan/seed.md +5 -12
- package/prompts/stages/objective-recover.md +8 -0
- package/prompts/stages/objective-replan.md +4 -2
- package/prompts/stages/objective-review-browser.md +9 -0
- package/prompts/stages/objective-sync.md +6 -0
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- package/prompts/stages/plan-review-browser.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +6 -14
- package/prompts/stages/pr-review-browser/foreign.md +6 -14
- package/prompts/stages/pr-review-dynamic.md +3 -3
- package/prompts/stages/pr-review-terminal/active.md +7 -15
- package/prompts/stages/pr-review-terminal/foreign.md +7 -15
- package/prompts/stages/pr-review.md +2 -2
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +12 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +3652 -236
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +32 -8
- package/shared/schemas/outputs/objective-doctor.schema.json +449 -0
- package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
- package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
- package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
- package/shared/schemas/outputs/plan-save.schema.json +12 -0
- package/shared/schemas/outputs/pr-submit.schema.json +177 -1
- package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
- package/extension/checkpoints/checkpoints.ts +0 -550
- package/extension/checkpoints/planSteps.ts +0 -108
- package/extension/doors/askUser.ts +0 -198
- package/prompts/common/output-schemas/objective-explorer.md +0 -36
- package/prompts/common/output-schemas/review-classifier.md +0 -47
- package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
|
@@ -33,6 +33,12 @@ export interface WorkflowState {
|
|
|
33
33
|
mode?: string;
|
|
34
34
|
/** The registry stage id this run is acting on (recorded at cold claim from the handoff). */
|
|
35
35
|
stage?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The running @mgiles/perk version stamped when run identity is established (§8.3) —
|
|
38
|
+
* claim/fork/adopt/mint in session_start. The session-audit vintage layer's exact basis;
|
|
39
|
+
* omitted when only the perkVersion() failure sentinel is available. Best-effort tier.
|
|
40
|
+
*/
|
|
41
|
+
perk_version?: string;
|
|
36
42
|
predecessor?: string;
|
|
37
43
|
active_plan_ref?: PlanRef | null;
|
|
38
44
|
active_objective?: string | null;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// The footer provider seam (INSTALL-SITE / runtime vacating). The footer is
|
|
2
|
-
// seam (no durable artifact to bridge)
|
|
1
|
+
// The footer provider seam (INSTALL-SITE / runtime vacating). The footer is an interface
|
|
2
|
+
// seam (no durable artifact to bridge) with a runtime vacating mechanism:
|
|
3
3
|
// perk installs its own footer (`installPerkFooter`) inside `index.ts`'s `session_start` event
|
|
4
4
|
// handler, so under a foreign `[providers] footer` selection perk simply does NOT call
|
|
5
5
|
// `installPerkFooter` — a runtime guard at that single install site, keyed off `ctx.cwd`. That
|
|
6
6
|
// leaves the foreign footer (`pi-powerline-footer` or `pi-bar`) as the sole footer surface;
|
|
7
|
-
// perk's objective
|
|
8
|
-
//
|
|
7
|
+
// perk's objective progress still reaches it automatically via the already-publishing
|
|
8
|
+
// single-value `perk` `setStatus` slot (both foreign footers render extension statuses), so the
|
|
9
9
|
// adapter is vacate-only (`adapter: null`, no shim, no injected context).
|
|
10
10
|
//
|
|
11
11
|
// This module reads config (via `resolveProviders`/`loadPerkConfig`) so it does NOT belong in the
|
|
12
12
|
// dependency-free `surfaces.ts`; it contains no rich-UI calls, so `surfacesGuard.test.ts` passes
|
|
13
|
-
// it cleanly. Mirror of `
|
|
13
|
+
// it cleanly. Mirror of `planMode.ts`'s `resolvedPlanProviderId` / `isPerkPlanReferenceSelected`.
|
|
14
14
|
|
|
15
15
|
import { loadPerkConfig } from "../substrate/config.ts";
|
|
16
16
|
import {
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
/**
|
|
23
23
|
* The resolved `[providers] footer` selection id for `cwd`. Fail-safe to the perk-footer
|
|
24
24
|
* reference: any load/resolution failure returns the reference id so perk keeps installing its
|
|
25
|
-
* own footer — the default path is the hard guarantee. Mirror of `
|
|
25
|
+
* own footer — the default path is the hard guarantee. Mirror of `resolvedPlanProviderId`: the
|
|
26
26
|
* catch narrows to genuine file-read/parse failures (the resolver is per-seam fail-open) and is
|
|
27
27
|
* logged, never silent.
|
|
28
28
|
*/
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
// The perk surfaces module. The one module that owns perk's UI
|
|
2
|
-
// vocabulary per the TUI charter (`docs/design/tui-charter.md` §3–§5): the
|
|
3
|
-
//
|
|
4
|
-
// bounds, the `createPerkStatus`
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// surfaces guard).
|
|
2
|
+
// vocabulary per the TUI charter (`docs/design/tui-charter.md` §3–§5): the single-value `perk`
|
|
3
|
+
// status slot key, footer identity marks, the §5 glyph + theming vocabulary, the §4 height
|
|
4
|
+
// bounds, the `createPerkStatus` status handle, and the pure format helpers the standing
|
|
5
|
+
// surfaces render with. The notify seam itself stays in `report.ts` (re-exported here so "the
|
|
6
|
+
// surfaces module" is surfaces.ts + report.ts for the surfaces guard).
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
// `perk` slot — the objective
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// RPC no-op). A regression guard enforces this composition.
|
|
8
|
+
// Perk status (charter §6 D2): perk presents ONE footer status under the single
|
|
9
|
+
// `perk` slot — the single-value objective segment (the checkpoint substrate is retired, so
|
|
10
|
+
// there is no composition step). The per-feature status slots and widgets are retired (D8
|
|
11
|
+
// sanctioned). The perk-owned footer (`perkFooter`/`installPerkFooter` below) renders the value
|
|
12
|
+
// directly; the `perk` status slot keeps publishing — it is the RPC-visible surface (setFooter
|
|
13
|
+
// is an RPC no-op).
|
|
16
14
|
|
|
17
15
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
18
16
|
|
|
@@ -24,17 +22,13 @@ export { Key } from "@earendil-works/pi-tui";
|
|
|
24
22
|
export { type ReportTarget, report, type Severity } from "./report.ts";
|
|
25
23
|
|
|
26
24
|
// --- standing-surface slot keys (charter §2) ---
|
|
27
|
-
// The ONE
|
|
25
|
+
// The ONE perk status slot (D2): perk's single status value renders under this key.
|
|
28
26
|
export const STATUS_SLOT_PERK = "perk";
|
|
29
|
-
// NOTE: same string as providers.ts PERK_CHECKPOINTS_PROVIDER_ID but a different concept
|
|
30
|
-
// (UI slot vs provider id) — deliberately NOT merged.
|
|
31
|
-
export const WIDGET_SLOT_CHECKPOINTS = "perk-checkpoints";
|
|
32
27
|
|
|
33
28
|
// --- footer identity marks (charter §5 / D3: emoji are footer-only identity, 2 cells wide) ---
|
|
34
|
-
export const MARK_CHECKPOINTS = "📋";
|
|
35
29
|
export const MARK_OBJECTIVE = "🎯";
|
|
36
30
|
|
|
37
|
-
// --- glyph vocabulary (charter §5 / D3) — data
|
|
31
|
+
// --- glyph vocabulary (charter §5 / D3) — charter-law data, pinned by tests ---
|
|
38
32
|
export type GlyphKind = "done" | "current" | "pending" | "warning" | "failure";
|
|
39
33
|
export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> = {
|
|
40
34
|
done: { glyph: "✓", themeColor: "success" },
|
|
@@ -44,10 +38,9 @@ export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> =
|
|
|
44
38
|
failure: { glyph: "✗", themeColor: "error" },
|
|
45
39
|
};
|
|
46
40
|
|
|
47
|
-
// --- height bounds (charter §4 / D1/D8) — enforced by the report() notify budget and the footer
|
|
41
|
+
// --- height bounds (charter §4 / D1/D8) — enforced by the report() notify budget and the footer builder ---
|
|
48
42
|
export const NOTIFY_MAX_LINES = 1;
|
|
49
43
|
export const FOOTER_MAX_LINES = 1;
|
|
50
|
-
export const CHECKPOINTS_WIDGET_MAX_LINES = 4;
|
|
51
44
|
|
|
52
45
|
// --- the standing-surface setter -----------------------------------------------------------------
|
|
53
46
|
|
|
@@ -61,48 +54,18 @@ export interface ThemeLike {
|
|
|
61
54
|
}
|
|
62
55
|
|
|
63
56
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* factory widgets (only string[] forwards) — an accepted trade-off recorded in contracts.md.
|
|
68
|
-
*/
|
|
69
|
-
export type StandingWidgetFactory = (
|
|
70
|
-
tui: unknown,
|
|
71
|
-
theme: ThemeLike,
|
|
72
|
-
) => { render(width: number): string[]; invalidate(): void };
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* The minimal headless-aware surface the standing setters need. `ExtensionContext` satisfies it;
|
|
76
|
-
* tests fake it (the same minimal-structural-interface recipe as report.ts's `ReportTarget` — see
|
|
57
|
+
* The minimal headless-aware surface the status handle needs — the `perk` status slot is the one
|
|
58
|
+
* standing surface it serves. `ExtensionContext` satisfies it; tests fake it (the same
|
|
59
|
+
* minimal-structural-interface recipe as report.ts's `ReportTarget` — see
|
|
77
60
|
* `docs/learned/pi/extension-seams.md`).
|
|
78
61
|
*/
|
|
79
62
|
export interface StandingTarget {
|
|
80
63
|
hasUI: boolean;
|
|
81
64
|
ui: {
|
|
82
65
|
setStatus(slot: string, value: string | undefined): void;
|
|
83
|
-
setWidget(
|
|
84
|
-
slot: string,
|
|
85
|
-
value: string[] | StandingWidgetFactory | undefined,
|
|
86
|
-
options?: { placement?: "aboveEditor" | "belowEditor" },
|
|
87
|
-
): void;
|
|
88
66
|
};
|
|
89
67
|
}
|
|
90
68
|
|
|
91
|
-
/** Set (or clear, with undefined) a standing widget slot; no-op headless. */
|
|
92
|
-
export function setStandingWidget(
|
|
93
|
-
target: StandingTarget,
|
|
94
|
-
slot: string,
|
|
95
|
-
widget: string[] | StandingWidgetFactory | undefined,
|
|
96
|
-
options?: { placement?: "aboveEditor" | "belowEditor" },
|
|
97
|
-
): void {
|
|
98
|
-
if (!target.hasUI) return;
|
|
99
|
-
if (options?.placement) {
|
|
100
|
-
target.ui.setWidget(slot, widget, { placement: options.placement });
|
|
101
|
-
} else {
|
|
102
|
-
target.ui.setWidget(slot, widget);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
69
|
// --- the working-message seam (vendored `whimsical`; charter §6 permitted text-only) ------------
|
|
107
70
|
|
|
108
71
|
/**
|
|
@@ -128,61 +91,44 @@ export function setWorkingMessage(target: WorkingMessageTarget, message?: string
|
|
|
128
91
|
target.ui.setWorkingMessage(message);
|
|
129
92
|
}
|
|
130
93
|
|
|
131
|
-
// --- the
|
|
132
|
-
|
|
133
|
-
/** A named segment of the composed `perk` status. Order is fixed by `PERK_SEGMENT_ORDER` (D2). */
|
|
134
|
-
export type PerkSegmentKey = "objective" | "checkpoints";
|
|
135
|
-
|
|
136
|
-
/** The charter D2 segment order: objective first, checkpoints second — never insertion order. */
|
|
137
|
-
export const PERK_SEGMENT_ORDER = ["objective", "checkpoints"] as const;
|
|
138
|
-
|
|
139
|
-
/** The two-space segment separator: `🎯 251 · 12.3k tok · 5m 📋 3/7 · ▸4`. */
|
|
140
|
-
const PERK_SEGMENT_SEPARATOR = " ";
|
|
94
|
+
// --- the perk status (charter D2) --------------------------------------------
|
|
141
95
|
|
|
142
96
|
/**
|
|
143
|
-
* The
|
|
144
|
-
* handle
|
|
145
|
-
*
|
|
146
|
-
*
|
|
97
|
+
* The single-value status handle: the objective publisher sets the one value through `set`, and
|
|
98
|
+
* the handle republishes the single `perk` status slot. The footer reads the value back via `get`
|
|
99
|
+
* and repaints via `subscribe` — the slot's `setStatus` dual-publish is deliberate (RPC clients
|
|
100
|
+
* see the slot; setFooter is an RPC no-op).
|
|
147
101
|
*/
|
|
148
102
|
export interface PerkStatusHandle {
|
|
149
|
-
/** Set (or clear, with undefined) one
|
|
150
|
-
set(target: StandingTarget,
|
|
151
|
-
/** The current text
|
|
152
|
-
get(
|
|
103
|
+
/** Set (or clear, with undefined) the one value; publishes the slot. No-op headless. */
|
|
104
|
+
set(target: StandingTarget, text: string | undefined): void;
|
|
105
|
+
/** The current text (undefined when unset). */
|
|
106
|
+
get(): string | undefined;
|
|
153
107
|
/**
|
|
154
|
-
* Subscribe to
|
|
108
|
+
* Subscribe to publishes: the listener fires after every headful `set` (headless `set`
|
|
155
109
|
* calls are full no-ops, so nothing fires). Returns an unsubscribe.
|
|
156
110
|
*/
|
|
157
111
|
subscribe(listener: () => void): () => void;
|
|
158
112
|
}
|
|
159
113
|
|
|
160
114
|
/**
|
|
161
|
-
* Create the
|
|
162
|
-
* passed to the
|
|
163
|
-
* the
|
|
164
|
-
*
|
|
165
|
-
* slot. No width handling: pi's footer truncates the joined status line itself.
|
|
115
|
+
* Create the single-value `perk` status handle (one per extension instance — created in index.ts
|
|
116
|
+
* and passed to the objective publisher; no hidden module state). Headless calls are full no-ops
|
|
117
|
+
* (never record the text, so headless-era text can't resurrect in a later headful render).
|
|
118
|
+
* `undefined` clears the slot. No width handling: pi's footer truncates the status line itself.
|
|
166
119
|
*/
|
|
167
120
|
export function createPerkStatus(): PerkStatusHandle {
|
|
168
|
-
|
|
121
|
+
let value: string | undefined;
|
|
169
122
|
const listeners = new Set<() => void>();
|
|
170
123
|
return {
|
|
171
|
-
set(target,
|
|
124
|
+
set(target, text) {
|
|
172
125
|
if (!target.hasUI) return;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
} else {
|
|
176
|
-
segments.set(segment, text);
|
|
177
|
-
}
|
|
178
|
-
const composed = PERK_SEGMENT_ORDER.filter((key) => segments.has(key))
|
|
179
|
-
.map((key) => segments.get(key) as string)
|
|
180
|
-
.join(PERK_SEGMENT_SEPARATOR);
|
|
181
|
-
target.ui.setStatus(STATUS_SLOT_PERK, composed === "" ? undefined : composed);
|
|
126
|
+
value = text;
|
|
127
|
+
target.ui.setStatus(STATUS_SLOT_PERK, text);
|
|
182
128
|
for (const listener of listeners) listener();
|
|
183
129
|
},
|
|
184
|
-
get(
|
|
185
|
-
return
|
|
130
|
+
get() {
|
|
131
|
+
return value;
|
|
186
132
|
},
|
|
187
133
|
subscribe(listener) {
|
|
188
134
|
listeners.add(listener);
|
|
@@ -194,18 +140,16 @@ export function createPerkStatus(): PerkStatusHandle {
|
|
|
194
140
|
// --- the perk-owned footer (charter D2) -----------------------------------------------
|
|
195
141
|
|
|
196
142
|
/**
|
|
197
|
-
* The raw material for one composed footer line. Left group (charter order 1–
|
|
198
|
-
* `objective
|
|
143
|
+
* The raw material for one composed footer line. Left group (charter order 1–2): `identity`,
|
|
144
|
+
* `objective` — the segment renders verbatim (it carries its own 🎯 mark).
|
|
199
145
|
* Right group (charter order 4, 5, +context, 6): `branch`, `model`, `thinking`, `cache`,
|
|
200
146
|
* `context`, `guests` — right-aligned, non-segment system text dim-themed.
|
|
201
147
|
*/
|
|
202
148
|
export interface FooterParts {
|
|
203
149
|
/** e.g. `perk v0.0.1` — standing identity (D7), dim. */
|
|
204
150
|
identity: string;
|
|
205
|
-
/** The 🎯 objective segment, verbatim (`handle.get(
|
|
151
|
+
/** The 🎯 objective segment, verbatim (`handle.get()`). */
|
|
206
152
|
objective?: string;
|
|
207
|
-
/** The 📋 checkpoints segment, verbatim (`handle.get("checkpoints")`). */
|
|
208
|
-
checkpoints?: string;
|
|
209
153
|
/** Git branch (dim); omitted when not in a repo. */
|
|
210
154
|
branch?: string;
|
|
211
155
|
/** Model id (dim); omitted when no model. */
|
|
@@ -231,28 +175,45 @@ export interface UsageEntryLike {
|
|
|
231
175
|
role?: string;
|
|
232
176
|
usage?: { input: number; cacheRead: number; cacheWrite: number };
|
|
233
177
|
};
|
|
178
|
+
/** Entry-level usage on `branch_summary`/`compaction` entries (pi 0.81.0 usage accounting). */
|
|
179
|
+
usage?: { input: number; cacheRead: number; cacheWrite: number };
|
|
234
180
|
}
|
|
235
181
|
|
|
236
182
|
/**
|
|
237
183
|
* The prompt-cache-hit rate of the latest usage-bearing assistant message, as a percentage —
|
|
238
|
-
* an exact local mirror of pi's default-footer `CH` computation (pi's cache-stats helpers
|
|
239
|
-
* unexported; the `sanitizeGuestStatus` reimplementation precedent). Includes pi's display
|
|
240
|
-
* returns `null` unless the session shows cache activity
|
|
241
|
-
*
|
|
242
|
-
*
|
|
184
|
+
* an exact local mirror of pi 0.84.1's default-footer `CH` computation (pi's cache-stats helpers
|
|
185
|
+
* are unexported; the `sanitizeGuestStatus` reimplementation precedent). Includes pi's display
|
|
186
|
+
* gate: returns `null` unless the session shows cache activity — total cacheRead or cacheWrite
|
|
187
|
+
* > 0 summed over assistant messages, `toolResult` messages carrying `usage`, and
|
|
188
|
+
* `branch_summary`/`compaction` entries' entry-level `usage` — AND the latest usage-bearing
|
|
189
|
+
* assistant message has prompt tokens > 0 (a trailing zero-prompt-token assistant message resets
|
|
190
|
+
* the rate, exactly like pi's `undefined`). The CH VALUE always comes from the latest
|
|
191
|
+
* usage-bearing assistant message; the non-assistant entries only widen the activity gate.
|
|
243
192
|
*/
|
|
244
193
|
export function latestCacheHitRate(entries: readonly UsageEntryLike[]): number | null {
|
|
245
194
|
let totalCacheRead = 0;
|
|
246
195
|
let totalCacheWrite = 0;
|
|
247
196
|
let latest: number | null = null;
|
|
248
197
|
for (const entry of entries) {
|
|
249
|
-
if (entry.type
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
198
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
199
|
+
const usage = entry.message.usage;
|
|
200
|
+
if (usage === undefined) continue;
|
|
201
|
+
totalCacheRead += usage.cacheRead;
|
|
202
|
+
totalCacheWrite += usage.cacheWrite;
|
|
203
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
204
|
+
latest = promptTokens > 0 ? (usage.cacheRead / promptTokens) * 100 : null;
|
|
205
|
+
} else if (entry.type === "message" && entry.message?.role === "toolResult") {
|
|
206
|
+
const usage = entry.message.usage;
|
|
207
|
+
if (usage === undefined) continue;
|
|
208
|
+
totalCacheRead += usage.cacheRead;
|
|
209
|
+
totalCacheWrite += usage.cacheWrite;
|
|
210
|
+
} else if (
|
|
211
|
+
(entry.type === "branch_summary" || entry.type === "compaction") &&
|
|
212
|
+
entry.usage !== undefined
|
|
213
|
+
) {
|
|
214
|
+
totalCacheRead += entry.usage.cacheRead;
|
|
215
|
+
totalCacheWrite += entry.usage.cacheWrite;
|
|
216
|
+
}
|
|
256
217
|
}
|
|
257
218
|
if (totalCacheRead <= 0 && totalCacheWrite <= 0) return null;
|
|
258
219
|
return latest;
|
|
@@ -279,11 +240,11 @@ function formatContextSegment(
|
|
|
279
240
|
}
|
|
280
241
|
|
|
281
242
|
/**
|
|
282
|
-
* Compose THE one footer line (FOOTER_MAX_LINES = 1): left group = identity + objective
|
|
283
|
-
*
|
|
243
|
+
* Compose THE one footer line (FOOTER_MAX_LINES = 1): left group = identity + objective
|
|
244
|
+
* (two-space-joined, charter order); right group = branch + model + context + guests
|
|
284
245
|
* (two-space-joined), right-aligned with ≥2 spaces of padding. When the line exceeds `width`,
|
|
285
246
|
* whole segments drop in the extended D9 order — guests (rightmost-first) → thinking → model →
|
|
286
|
-
* branch → cache → context
|
|
247
|
+
* branch → cache → context; `identity` and `objective` are NEVER dropped — then
|
|
287
248
|
* `truncateToWidth` as the last resort (ANSI- and 2-cell-emoji-aware).
|
|
288
249
|
*/
|
|
289
250
|
export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: number): string {
|
|
@@ -294,12 +255,10 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
294
255
|
branch: true,
|
|
295
256
|
cache: true,
|
|
296
257
|
context: true,
|
|
297
|
-
checkpoints: true,
|
|
298
258
|
};
|
|
299
259
|
const compose = (): string => {
|
|
300
260
|
const left = [theme.fg("dim", parts.identity)];
|
|
301
261
|
if (parts.objective !== undefined) left.push(parts.objective);
|
|
302
|
-
if (keep.checkpoints && parts.checkpoints !== undefined) left.push(parts.checkpoints);
|
|
303
262
|
const right: string[] = [];
|
|
304
263
|
if (keep.branch && parts.branch !== undefined) right.push(theme.fg("dim", parts.branch));
|
|
305
264
|
if (keep.model && parts.model !== undefined) right.push(theme.fg("dim", parts.model));
|
|
@@ -309,9 +268,9 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
309
268
|
right.push(formatContextSegment(parts.context, theme));
|
|
310
269
|
}
|
|
311
270
|
for (const guest of keep.guests) right.push(theme.fg("dim", guest));
|
|
312
|
-
const leftText = left.join(
|
|
271
|
+
const leftText = left.join(" ");
|
|
313
272
|
if (right.length === 0) return leftText;
|
|
314
|
-
const rightText = right.join(
|
|
273
|
+
const rightText = right.join(" ");
|
|
315
274
|
const padding = Math.max(2, width - visibleWidth(leftText) - visibleWidth(rightText));
|
|
316
275
|
return `${leftText}${" ".repeat(padding)}${rightText}`;
|
|
317
276
|
};
|
|
@@ -323,7 +282,6 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
323
282
|
else if (keep.branch) keep.branch = false;
|
|
324
283
|
else if (keep.cache) keep.cache = false;
|
|
325
284
|
else if (keep.context) keep.context = false;
|
|
326
|
-
else if (keep.checkpoints) keep.checkpoints = false;
|
|
327
285
|
else break; // identity + objective only — nothing left to drop
|
|
328
286
|
line = compose();
|
|
329
287
|
}
|
|
@@ -364,10 +322,16 @@ export type PerkFooterFactory = (
|
|
|
364
322
|
/**
|
|
365
323
|
* The perk-owned footer factory (charter D2): replaces pi's default footer wholesale with one
|
|
366
324
|
* line in the intended split layout. `render` gathers everything live per call (D10 stateless
|
|
367
|
-
* render):
|
|
368
|
-
* `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the
|
|
325
|
+
* render): the objective value via the handle, branch/guests via `footerData` (excluding perk's
|
|
326
|
+
* own `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the value
|
|
369
327
|
* directly), model/cache/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
370
|
-
* every handle recompose and on branch change; `dispose` detaches both.
|
|
328
|
+
* every handle recompose and on branch change; `dispose` detaches both. Lifecycle (pi ≥ 0.84,
|
|
329
|
+
* verified at 0.84.1): `setExtensionFooter` disposes a replaced factory's component, and pi's
|
|
330
|
+
* `resetExtensionUI` restores the built-in footer (disposing this one) on /reload and before
|
|
331
|
+
* session replacement — so installing a fresh factory per headful `session_start` leaks nothing.
|
|
332
|
+
*
|
|
333
|
+
* Two DELIBERATE divergences from pi's default footer remain: no `(auto)` context suffix
|
|
334
|
+
* (auto-compact state is not extension-readable) and no cost/`(sub)` segment (charter scope).
|
|
371
335
|
*/
|
|
372
336
|
export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
373
337
|
return (tui, theme, footerData) => {
|
|
@@ -382,8 +346,7 @@ export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
|
382
346
|
const rate = deps.getCacheHitRate();
|
|
383
347
|
const parts: FooterParts = {
|
|
384
348
|
identity: deps.identity,
|
|
385
|
-
objective: deps.status.get(
|
|
386
|
-
checkpoints: deps.status.get("checkpoints"),
|
|
349
|
+
objective: deps.status.get(),
|
|
387
350
|
branch: footerData.getGitBranch() ?? undefined,
|
|
388
351
|
model: deps.getModelId() ?? undefined,
|
|
389
352
|
thinking: deps.getThinkingLevel() ?? undefined,
|
|
@@ -404,7 +367,10 @@ export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
|
404
367
|
};
|
|
405
368
|
}
|
|
406
369
|
|
|
407
|
-
/**
|
|
370
|
+
/**
|
|
371
|
+
* Install the perk-owned footer (sole-owner law, D2); no-op headless. Safe to call on every
|
|
372
|
+
* headful `session_start`: pi ≥ 0.84 disposes the replaced factory (see `perkFooter`).
|
|
373
|
+
*/
|
|
408
374
|
export function installPerkFooter(
|
|
409
375
|
target: { hasUI: boolean; ui: { setFooter(factory: PerkFooterFactory | undefined): void } },
|
|
410
376
|
deps: PerkFooterDeps,
|
|
@@ -413,107 +379,15 @@ export function installPerkFooter(
|
|
|
413
379
|
target.ui.setFooter(perkFooter(deps));
|
|
414
380
|
}
|
|
415
381
|
|
|
416
|
-
// --- format helpers
|
|
417
|
-
// Structural parameter types (not CheckpointState/CheckpointStep imports) keep surfaces.ts
|
|
418
|
-
// dependency-free and avoid an import cycle with the surface controllers.
|
|
419
|
-
|
|
420
|
-
export interface ProgressStep {
|
|
421
|
-
step: number;
|
|
422
|
-
text: string;
|
|
423
|
-
completed: boolean;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export interface ProgressState {
|
|
427
|
-
steps: ProgressStep[];
|
|
428
|
-
/** The in-progress step number, or `null`. */
|
|
429
|
-
current: number | null;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/** The `done/total` checkpoint progress summary (with `· ▸n` when a step is current). */
|
|
433
|
-
export function progressLine(state: ProgressState): string {
|
|
434
|
-
const done = state.steps.filter((s) => s.completed).length;
|
|
435
|
-
const base = `${done}/${state.steps.length}`;
|
|
436
|
-
return state.current != null ? `${base} · ▸${state.current}` : base;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** The `GLYPHS` kind for a step: done if completed, current if it IS the current step, else pending. */
|
|
440
|
-
export function stepGlyphKind(state: ProgressState, s: ProgressStep): GlyphKind {
|
|
441
|
-
if (s.completed) return "done";
|
|
442
|
-
if (s.step === state.current) return "current";
|
|
443
|
-
return "pending";
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/** A windowed progress item: a visible step, or an elision marker for the hidden steps. */
|
|
447
|
-
export type ProgressWindowItem =
|
|
448
|
-
| { kind: "step"; step: ProgressStep }
|
|
449
|
-
| { kind: "elision"; hidden: number; side: "earlier" | "later" };
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* The D1 sliding window: at most `cap` step items (elision markers extra). For `n ≤ cap` all
|
|
453
|
-
* steps show with no markers. Otherwise the window anchors on the current step (`current == null`
|
|
454
|
-
* ⟹ all complete ⟹ anchor at the end) sitting second when possible — one earlier step above,
|
|
455
|
-
* the rest below — with `… +N earlier` / `… +N later` markers for the hidden steps.
|
|
456
|
-
*/
|
|
457
|
-
export function windowProgress(state: ProgressState, cap: number): ProgressWindowItem[] {
|
|
458
|
-
const n = state.steps.length;
|
|
459
|
-
if (n <= cap) return state.steps.map((step) => ({ kind: "step", step }));
|
|
460
|
-
const anchorIdx =
|
|
461
|
-
state.current != null
|
|
462
|
-
? Math.max(
|
|
463
|
-
state.steps.findIndex((s) => s.step === state.current),
|
|
464
|
-
0,
|
|
465
|
-
)
|
|
466
|
-
: n - 1;
|
|
467
|
-
const start = Math.min(Math.max(anchorIdx - 1, 0), n - cap);
|
|
468
|
-
const items: ProgressWindowItem[] = [];
|
|
469
|
-
if (start > 0) items.push({ kind: "elision", hidden: start, side: "earlier" });
|
|
470
|
-
for (const step of state.steps.slice(start, start + cap)) items.push({ kind: "step", step });
|
|
471
|
-
const later = n - start - cap;
|
|
472
|
-
if (later > 0) items.push({ kind: "elision", hidden: later, side: "later" });
|
|
473
|
-
return items;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* The themed checkpoints-widget lines (charter D1/D3/D9/D10): the `windowProgress` window mapped
|
|
478
|
-
* to `✓/▸/○ <n>. <text>` lines colored per the §5 table (completed text muted, elision markers
|
|
479
|
-
* dim), every line width-truncated via pi-tui's `truncateToWidth` (ANSI- and wide-glyph-aware).
|
|
480
|
-
* Pure per call — call it inside a component's `render()` so theming stays live (D10).
|
|
481
|
-
*/
|
|
482
|
-
export function renderProgressLines(
|
|
483
|
-
state: ProgressState,
|
|
484
|
-
theme: ThemeLike,
|
|
485
|
-
width: number,
|
|
486
|
-
): string[] {
|
|
487
|
-
return windowProgress(state, CHECKPOINTS_WIDGET_MAX_LINES).map((item) => {
|
|
488
|
-
if (item.kind === "elision") {
|
|
489
|
-
return truncateToWidth(theme.fg("dim", `… +${item.hidden} ${item.side}`), width);
|
|
490
|
-
}
|
|
491
|
-
return renderStepLine(state, item.step, theme, width);
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* ONE themed per-step line (`✓/▸/○ <n>. <text>`, §5 colors, D9-truncated) — shared by the
|
|
497
|
-
* checkpoints widget (`renderProgressLines`, windowed) and the checkpoint transcript marker's
|
|
498
|
-
* expanded view (all steps, unwindowed), so the two surfaces render steps identically.
|
|
499
|
-
*/
|
|
500
|
-
function renderStepLine(
|
|
501
|
-
state: ProgressState,
|
|
502
|
-
step: ProgressStep,
|
|
503
|
-
theme: ThemeLike,
|
|
504
|
-
width: number,
|
|
505
|
-
): string {
|
|
506
|
-
const kind = stepGlyphKind(state, step);
|
|
507
|
-
const glyph = theme.fg(GLYPHS[kind].themeColor, GLYPHS[kind].glyph);
|
|
508
|
-
const text = `${step.step}. ${step.text}`;
|
|
509
|
-
const line = `${glyph} ${kind === "done" ? theme.fg("muted", text) : text}`;
|
|
510
|
-
return truncateToWidth(line, width);
|
|
511
|
-
}
|
|
382
|
+
// --- format helpers --------------------------------------------------------------------------
|
|
512
383
|
|
|
384
|
+
/** Pi 0.84.1's default-footer `formatTokens` tiers, mirrored exactly (`200000` → `200k`). */
|
|
513
385
|
function formatTokens(tokens: number): string {
|
|
514
386
|
if (tokens < 1000) return `${tokens}`;
|
|
515
|
-
|
|
516
|
-
return `${(tokens / 1000)
|
|
387
|
+
if (tokens < 10_000) return `${(tokens / 1000).toFixed(1)}k`;
|
|
388
|
+
if (tokens < 1_000_000) return `${Math.round(tokens / 1000)}k`;
|
|
389
|
+
if (tokens < 10_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
390
|
+
return `${Math.round(tokens / 1_000_000)}M`;
|
|
517
391
|
}
|
|
518
392
|
|
|
519
393
|
function formatElapsed(ms: number): string {
|
|
@@ -525,13 +399,13 @@ function formatElapsed(ms: number): string {
|
|
|
525
399
|
return `${hr}h${min % 60}m`;
|
|
526
400
|
}
|
|
527
401
|
|
|
528
|
-
/** A compact one-line budget summary (e.g. `
|
|
402
|
+
/** A compact one-line budget summary (e.g. `12k tok · 5m`, `1.2M tok · 2h5m` past 1M). */
|
|
529
403
|
export function formatBudgetLine(args: { tokens: number; elapsedMs: number }): string {
|
|
530
404
|
return `${formatTokens(args.tokens)} tok · ${formatElapsed(args.elapsedMs)}`;
|
|
531
405
|
}
|
|
532
406
|
|
|
533
407
|
// --- the transcript markers — display-only entry renderers ---------------------------------------
|
|
534
|
-
// The audit §2.3 verdict (docs/design/pi-adoption-audit.md): perk's
|
|
408
|
+
// The audit §2.3 verdict (docs/design/pi-adoption-audit.md): perk's display-only custom-entry
|
|
535
409
|
// families render as durable one-line transcript markers. Renderer BODIES live here (a transcript
|
|
536
410
|
// renderer IS a rich-UI surface the surfaces module owns); registration is wiring at the feature
|
|
537
411
|
// modules via the `registerTranscriptRenderer` seam below. Renderers are an interactive-TUI-only
|
|
@@ -585,15 +459,13 @@ export function registerTranscriptRenderer(
|
|
|
585
459
|
|
|
586
460
|
/**
|
|
587
461
|
* Charter budget: a COLLAPSED transcript marker is exactly one line. The expanded view is
|
|
588
|
-
* human-requested scrollback and renders its full detail unbounded (
|
|
589
|
-
* whole btw answer).
|
|
462
|
+
* human-requested scrollback and renders its full detail unbounded (the whole btw answer).
|
|
590
463
|
*/
|
|
591
464
|
export const TRANSCRIPT_MARKER_MAX_LINES = 1;
|
|
592
465
|
|
|
593
466
|
/**
|
|
594
467
|
* The collapsed-marker grammar: the `report()` transition grammar `perk: <scope> — <message>`,
|
|
595
|
-
* dim, D9-truncated. Emoji stay footer-only (D3)
|
|
596
|
-
* checkpoint step lines.
|
|
468
|
+
* dim, D9-truncated. Emoji stay footer-only (D3).
|
|
597
469
|
*/
|
|
598
470
|
function markerLine(scope: string, message: string, theme: ThemeLike, width: number): string {
|
|
599
471
|
return truncateToWidth(theme.fg("dim", `perk: ${scope} — ${message}`), width);
|
|
@@ -605,48 +477,6 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
605
477
|
return value as Record<string, unknown>;
|
|
606
478
|
}
|
|
607
479
|
|
|
608
|
-
/** Decode `{ steps }` from a `perk:checkpoint` entry: a non-empty array of valid steps, or null. */
|
|
609
|
-
function decodeCheckpointSteps(data: unknown): ProgressStep[] | null {
|
|
610
|
-
const record = asRecord(data);
|
|
611
|
-
if (record === null) return null;
|
|
612
|
-
const steps = record.steps;
|
|
613
|
-
if (!Array.isArray(steps) || steps.length === 0) return null;
|
|
614
|
-
const decoded: ProgressStep[] = [];
|
|
615
|
-
for (const raw of steps) {
|
|
616
|
-
const step = asRecord(raw);
|
|
617
|
-
if (step === null) return null;
|
|
618
|
-
if (
|
|
619
|
-
typeof step.step !== "number" ||
|
|
620
|
-
typeof step.text !== "string" ||
|
|
621
|
-
typeof step.completed !== "boolean"
|
|
622
|
-
) {
|
|
623
|
-
return null;
|
|
624
|
-
}
|
|
625
|
-
decoded.push({ step: step.step, text: step.text, completed: step.completed });
|
|
626
|
-
}
|
|
627
|
-
return decoded;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* `perk:checkpoint` marker. Collapsed: `perk: checkpoints — <done/total>`. Expanded: that line +
|
|
632
|
-
* one §5 glyph line per step — ALL steps, unwindowed (scrollback is human-requested, so the
|
|
633
|
-
* `CHECKPOINTS_WIDGET_MAX_LINES` standing budget does not apply). `current` is derived state that
|
|
634
|
-
* lives in checkpoints.ts; a historical marker renders with `current: null` (bare `done/total`,
|
|
635
|
-
* no `▸` step) — which keeps surfaces.ts free of checkpoint imports.
|
|
636
|
-
*/
|
|
637
|
-
export const checkpointEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
638
|
-
const steps = decodeCheckpointSteps(entry.data);
|
|
639
|
-
if (steps === null) return undefined;
|
|
640
|
-
const state: ProgressState = { steps, current: null };
|
|
641
|
-
return {
|
|
642
|
-
render(width) {
|
|
643
|
-
const collapsed = markerLine("checkpoints", progressLine(state), theme, width);
|
|
644
|
-
if (!options.expanded) return [collapsed];
|
|
645
|
-
return [collapsed, ...steps.map((step) => renderStepLine(state, step, theme, width))];
|
|
646
|
-
},
|
|
647
|
-
};
|
|
648
|
-
};
|
|
649
|
-
|
|
650
480
|
/**
|
|
651
481
|
* The first matching workflow-state field's marker message — a deliberately BOUNDED vocabulary
|
|
652
482
|
* (the four headline fields + a SET `objective_node_claim`), extensible later. Bookkeeping deltas
|
|
@@ -769,17 +599,3 @@ export const btwThreadResetEntryRenderer: TranscriptRenderer = (entry, options,
|
|
|
769
599
|
},
|
|
770
600
|
};
|
|
771
601
|
};
|
|
772
|
-
|
|
773
|
-
/**
|
|
774
|
-
* The coarse prose-plan widget line (relocated verbatim from checkpoints.ts's inline factory —
|
|
775
|
-
* its only pi-tui usage): one dim, D9-truncated line naming the active plan with no `## Steps`
|
|
776
|
-
* checklist.
|
|
777
|
-
*/
|
|
778
|
-
export function renderCoarsePlanLines(planId: string, theme: ThemeLike, width: number): string[] {
|
|
779
|
-
return [
|
|
780
|
-
truncateToWidth(
|
|
781
|
-
theme.fg("dim", `Plan #${planId}: prose plan — no \`## Steps\` checklist`),
|
|
782
|
-
width,
|
|
783
|
-
),
|
|
784
|
-
];
|
|
785
|
-
}
|