@mgiles/perk 2.3.0 → 3.1.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 +135 -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/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +412 -0
- package/extension/doors/dreamWaveTools.ts +475 -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 +537 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +475 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +180 -63
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +57 -30
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +397 -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 +153 -30
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +90 -2
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +304 -46
- 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 +169 -57
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +166 -23
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +41 -12
- package/extension/substrate/git.ts +95 -2
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +16 -7
- package/extension/substrate/sessionData.ts +8 -6
- package/extension/substrate/sessionPointers.ts +4 -5
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +179 -23
- package/extension/substrate/workflowState.ts +50 -2
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +213 -275
- package/extension/vendor/btw/btw.ts +94 -17
- package/extension/waves/adversarialReviewWave.ts +181 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +205 -0
- package/extension/waves/dreamReducerWave.ts +700 -0
- package/extension/waves/dreamReport.ts +1494 -0
- package/extension/waves/dreamWave.ts +927 -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/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +383 -72
- package/extension/waves/prReviewWave.ts +175 -25
- package/extension/waves/reportWave.ts +503 -107
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +4 -5
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +18 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +115 -65
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +8 -3
- package/prompts/contexts/adapters/plannotator-plan.md +8 -3
- 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/conflict-resolution.md +1 -1
- 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-dream.md +10 -0
- 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 +6 -6
- 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-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +15 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +5373 -281
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +42 -18
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +487 -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-land.schema.json +3 -3
- 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
|
@@ -1,40 +1,41 @@
|
|
|
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";
|
|
16
|
+
import type { ReportDetailSink } from "./report.ts";
|
|
18
17
|
|
|
19
18
|
// `Key` is keybinding vocabulary (`pi.registerShortcut(Key.ctrlAlt("p"), …)`), not rich UI —
|
|
20
19
|
// re-exported so pi-tui imports stay structurally confined to the surfaces module (the
|
|
21
20
|
// surfacesGuard pi-tui import rule) without allowlisting the shortcut-registering modules.
|
|
22
21
|
export { Key } from "@earendil-works/pi-tui";
|
|
23
22
|
// Re-exports: the notify seam stays in report.ts; surfaces.ts is the one import for UI vocabulary.
|
|
24
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
attachReportDetailSink,
|
|
25
|
+
type ReportDetailSink,
|
|
26
|
+
type ReportTarget,
|
|
27
|
+
report,
|
|
28
|
+
type Severity,
|
|
29
|
+
} from "./report.ts";
|
|
25
30
|
|
|
26
31
|
// --- standing-surface slot keys (charter §2) ---
|
|
27
|
-
// The ONE
|
|
32
|
+
// The ONE perk status slot (D2): perk's single status value renders under this key.
|
|
28
33
|
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
34
|
|
|
33
35
|
// --- footer identity marks (charter §5 / D3: emoji are footer-only identity, 2 cells wide) ---
|
|
34
|
-
export const MARK_CHECKPOINTS = "📋";
|
|
35
36
|
export const MARK_OBJECTIVE = "🎯";
|
|
36
37
|
|
|
37
|
-
// --- glyph vocabulary (charter §5 / D3) — data
|
|
38
|
+
// --- glyph vocabulary (charter §5 / D3) — charter-law data, pinned by tests ---
|
|
38
39
|
export type GlyphKind = "done" | "current" | "pending" | "warning" | "failure";
|
|
39
40
|
export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> = {
|
|
40
41
|
done: { glyph: "✓", themeColor: "success" },
|
|
@@ -44,10 +45,9 @@ export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> =
|
|
|
44
45
|
failure: { glyph: "✗", themeColor: "error" },
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
// --- height bounds (charter §4 / D1/D8) — enforced by the report() notify budget and the footer
|
|
48
|
+
// --- height bounds (charter §4 / D1/D8) — enforced by the report() notify budget and the footer builder ---
|
|
48
49
|
export const NOTIFY_MAX_LINES = 1;
|
|
49
50
|
export const FOOTER_MAX_LINES = 1;
|
|
50
|
-
export const CHECKPOINTS_WIDGET_MAX_LINES = 4;
|
|
51
51
|
|
|
52
52
|
// --- the standing-surface setter -----------------------------------------------------------------
|
|
53
53
|
|
|
@@ -61,48 +61,18 @@ export interface ThemeLike {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
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
|
|
64
|
+
* The minimal headless-aware surface the status handle needs — the `perk` status slot is the one
|
|
65
|
+
* standing surface it serves. `ExtensionContext` satisfies it; tests fake it (the same
|
|
66
|
+
* minimal-structural-interface recipe as report.ts's `ReportTarget` — see
|
|
77
67
|
* `docs/learned/pi/extension-seams.md`).
|
|
78
68
|
*/
|
|
79
69
|
export interface StandingTarget {
|
|
80
70
|
hasUI: boolean;
|
|
81
71
|
ui: {
|
|
82
72
|
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
73
|
};
|
|
89
74
|
}
|
|
90
75
|
|
|
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
76
|
// --- the working-message seam (vendored `whimsical`; charter §6 permitted text-only) ------------
|
|
107
77
|
|
|
108
78
|
/**
|
|
@@ -128,61 +98,44 @@ export function setWorkingMessage(target: WorkingMessageTarget, message?: string
|
|
|
128
98
|
target.ui.setWorkingMessage(message);
|
|
129
99
|
}
|
|
130
100
|
|
|
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 = " ";
|
|
101
|
+
// --- the perk status (charter D2) --------------------------------------------
|
|
141
102
|
|
|
142
103
|
/**
|
|
143
|
-
* The
|
|
144
|
-
* handle
|
|
145
|
-
*
|
|
146
|
-
*
|
|
104
|
+
* The single-value status handle: the objective publisher sets the one value through `set`, and
|
|
105
|
+
* the handle republishes the single `perk` status slot. The footer reads the value back via `get`
|
|
106
|
+
* and repaints via `subscribe` — the slot's `setStatus` dual-publish is deliberate (RPC clients
|
|
107
|
+
* see the slot; setFooter is an RPC no-op).
|
|
147
108
|
*/
|
|
148
109
|
export interface PerkStatusHandle {
|
|
149
|
-
/** Set (or clear, with undefined) one
|
|
150
|
-
set(target: StandingTarget,
|
|
151
|
-
/** The current text
|
|
152
|
-
get(
|
|
110
|
+
/** Set (or clear, with undefined) the one value; publishes the slot. No-op headless. */
|
|
111
|
+
set(target: StandingTarget, text: string | undefined): void;
|
|
112
|
+
/** The current text (undefined when unset). */
|
|
113
|
+
get(): string | undefined;
|
|
153
114
|
/**
|
|
154
|
-
* Subscribe to
|
|
115
|
+
* Subscribe to publishes: the listener fires after every headful `set` (headless `set`
|
|
155
116
|
* calls are full no-ops, so nothing fires). Returns an unsubscribe.
|
|
156
117
|
*/
|
|
157
118
|
subscribe(listener: () => void): () => void;
|
|
158
119
|
}
|
|
159
120
|
|
|
160
121
|
/**
|
|
161
|
-
* Create the
|
|
162
|
-
* passed to the
|
|
163
|
-
* the
|
|
164
|
-
*
|
|
165
|
-
* slot. No width handling: pi's footer truncates the joined status line itself.
|
|
122
|
+
* Create the single-value `perk` status handle (one per extension instance — created in index.ts
|
|
123
|
+
* and passed to the objective publisher; no hidden module state). Headless calls are full no-ops
|
|
124
|
+
* (never record the text, so headless-era text can't resurrect in a later headful render).
|
|
125
|
+
* `undefined` clears the slot. No width handling: pi's footer truncates the status line itself.
|
|
166
126
|
*/
|
|
167
127
|
export function createPerkStatus(): PerkStatusHandle {
|
|
168
|
-
|
|
128
|
+
let value: string | undefined;
|
|
169
129
|
const listeners = new Set<() => void>();
|
|
170
130
|
return {
|
|
171
|
-
set(target,
|
|
131
|
+
set(target, text) {
|
|
172
132
|
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);
|
|
133
|
+
value = text;
|
|
134
|
+
target.ui.setStatus(STATUS_SLOT_PERK, text);
|
|
182
135
|
for (const listener of listeners) listener();
|
|
183
136
|
},
|
|
184
|
-
get(
|
|
185
|
-
return
|
|
137
|
+
get() {
|
|
138
|
+
return value;
|
|
186
139
|
},
|
|
187
140
|
subscribe(listener) {
|
|
188
141
|
listeners.add(listener);
|
|
@@ -194,18 +147,16 @@ export function createPerkStatus(): PerkStatusHandle {
|
|
|
194
147
|
// --- the perk-owned footer (charter D2) -----------------------------------------------
|
|
195
148
|
|
|
196
149
|
/**
|
|
197
|
-
* The raw material for one composed footer line. Left group (charter order 1–
|
|
198
|
-
* `objective
|
|
150
|
+
* The raw material for one composed footer line. Left group (charter order 1–2): `identity`,
|
|
151
|
+
* `objective` — the segment renders verbatim (it carries its own 🎯 mark).
|
|
199
152
|
* Right group (charter order 4, 5, +context, 6): `branch`, `model`, `thinking`, `cache`,
|
|
200
153
|
* `context`, `guests` — right-aligned, non-segment system text dim-themed.
|
|
201
154
|
*/
|
|
202
155
|
export interface FooterParts {
|
|
203
156
|
/** e.g. `perk v0.0.1` — standing identity (D7), dim. */
|
|
204
157
|
identity: string;
|
|
205
|
-
/** The 🎯 objective segment, verbatim (`handle.get(
|
|
158
|
+
/** The 🎯 objective segment, verbatim (`handle.get()`). */
|
|
206
159
|
objective?: string;
|
|
207
|
-
/** The 📋 checkpoints segment, verbatim (`handle.get("checkpoints")`). */
|
|
208
|
-
checkpoints?: string;
|
|
209
160
|
/** Git branch (dim); omitted when not in a repo. */
|
|
210
161
|
branch?: string;
|
|
211
162
|
/** Model id (dim); omitted when no model. */
|
|
@@ -231,28 +182,45 @@ export interface UsageEntryLike {
|
|
|
231
182
|
role?: string;
|
|
232
183
|
usage?: { input: number; cacheRead: number; cacheWrite: number };
|
|
233
184
|
};
|
|
185
|
+
/** Entry-level usage on `branch_summary`/`compaction` entries (pi 0.81.0 usage accounting). */
|
|
186
|
+
usage?: { input: number; cacheRead: number; cacheWrite: number };
|
|
234
187
|
}
|
|
235
188
|
|
|
236
189
|
/**
|
|
237
190
|
* 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
|
-
*
|
|
191
|
+
* an exact local mirror of pi 0.84.1's default-footer `CH` computation (pi's cache-stats helpers
|
|
192
|
+
* are unexported; the `sanitizeGuestStatus` reimplementation precedent). Includes pi's display
|
|
193
|
+
* gate: returns `null` unless the session shows cache activity — total cacheRead or cacheWrite
|
|
194
|
+
* > 0 summed over assistant messages, `toolResult` messages carrying `usage`, and
|
|
195
|
+
* `branch_summary`/`compaction` entries' entry-level `usage` — AND the latest usage-bearing
|
|
196
|
+
* assistant message has prompt tokens > 0 (a trailing zero-prompt-token assistant message resets
|
|
197
|
+
* the rate, exactly like pi's `undefined`). The CH VALUE always comes from the latest
|
|
198
|
+
* usage-bearing assistant message; the non-assistant entries only widen the activity gate.
|
|
243
199
|
*/
|
|
244
200
|
export function latestCacheHitRate(entries: readonly UsageEntryLike[]): number | null {
|
|
245
201
|
let totalCacheRead = 0;
|
|
246
202
|
let totalCacheWrite = 0;
|
|
247
203
|
let latest: number | null = null;
|
|
248
204
|
for (const entry of entries) {
|
|
249
|
-
if (entry.type
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
205
|
+
if (entry.type === "message" && entry.message?.role === "assistant") {
|
|
206
|
+
const usage = entry.message.usage;
|
|
207
|
+
if (usage === undefined) continue;
|
|
208
|
+
totalCacheRead += usage.cacheRead;
|
|
209
|
+
totalCacheWrite += usage.cacheWrite;
|
|
210
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
211
|
+
latest = promptTokens > 0 ? (usage.cacheRead / promptTokens) * 100 : null;
|
|
212
|
+
} else if (entry.type === "message" && entry.message?.role === "toolResult") {
|
|
213
|
+
const usage = entry.message.usage;
|
|
214
|
+
if (usage === undefined) continue;
|
|
215
|
+
totalCacheRead += usage.cacheRead;
|
|
216
|
+
totalCacheWrite += usage.cacheWrite;
|
|
217
|
+
} else if (
|
|
218
|
+
(entry.type === "branch_summary" || entry.type === "compaction") &&
|
|
219
|
+
entry.usage !== undefined
|
|
220
|
+
) {
|
|
221
|
+
totalCacheRead += entry.usage.cacheRead;
|
|
222
|
+
totalCacheWrite += entry.usage.cacheWrite;
|
|
223
|
+
}
|
|
256
224
|
}
|
|
257
225
|
if (totalCacheRead <= 0 && totalCacheWrite <= 0) return null;
|
|
258
226
|
return latest;
|
|
@@ -279,11 +247,11 @@ function formatContextSegment(
|
|
|
279
247
|
}
|
|
280
248
|
|
|
281
249
|
/**
|
|
282
|
-
* Compose THE one footer line (FOOTER_MAX_LINES = 1): left group = identity + objective
|
|
283
|
-
*
|
|
250
|
+
* Compose THE one footer line (FOOTER_MAX_LINES = 1): left group = identity + objective
|
|
251
|
+
* (two-space-joined, charter order); right group = branch + model + context + guests
|
|
284
252
|
* (two-space-joined), right-aligned with ≥2 spaces of padding. When the line exceeds `width`,
|
|
285
253
|
* whole segments drop in the extended D9 order — guests (rightmost-first) → thinking → model →
|
|
286
|
-
* branch → cache → context
|
|
254
|
+
* branch → cache → context; `identity` and `objective` are NEVER dropped — then
|
|
287
255
|
* `truncateToWidth` as the last resort (ANSI- and 2-cell-emoji-aware).
|
|
288
256
|
*/
|
|
289
257
|
export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: number): string {
|
|
@@ -294,12 +262,10 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
294
262
|
branch: true,
|
|
295
263
|
cache: true,
|
|
296
264
|
context: true,
|
|
297
|
-
checkpoints: true,
|
|
298
265
|
};
|
|
299
266
|
const compose = (): string => {
|
|
300
267
|
const left = [theme.fg("dim", parts.identity)];
|
|
301
268
|
if (parts.objective !== undefined) left.push(parts.objective);
|
|
302
|
-
if (keep.checkpoints && parts.checkpoints !== undefined) left.push(parts.checkpoints);
|
|
303
269
|
const right: string[] = [];
|
|
304
270
|
if (keep.branch && parts.branch !== undefined) right.push(theme.fg("dim", parts.branch));
|
|
305
271
|
if (keep.model && parts.model !== undefined) right.push(theme.fg("dim", parts.model));
|
|
@@ -309,9 +275,9 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
309
275
|
right.push(formatContextSegment(parts.context, theme));
|
|
310
276
|
}
|
|
311
277
|
for (const guest of keep.guests) right.push(theme.fg("dim", guest));
|
|
312
|
-
const leftText = left.join(
|
|
278
|
+
const leftText = left.join(" ");
|
|
313
279
|
if (right.length === 0) return leftText;
|
|
314
|
-
const rightText = right.join(
|
|
280
|
+
const rightText = right.join(" ");
|
|
315
281
|
const padding = Math.max(2, width - visibleWidth(leftText) - visibleWidth(rightText));
|
|
316
282
|
return `${leftText}${" ".repeat(padding)}${rightText}`;
|
|
317
283
|
};
|
|
@@ -323,7 +289,6 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
323
289
|
else if (keep.branch) keep.branch = false;
|
|
324
290
|
else if (keep.cache) keep.cache = false;
|
|
325
291
|
else if (keep.context) keep.context = false;
|
|
326
|
-
else if (keep.checkpoints) keep.checkpoints = false;
|
|
327
292
|
else break; // identity + objective only — nothing left to drop
|
|
328
293
|
line = compose();
|
|
329
294
|
}
|
|
@@ -364,10 +329,16 @@ export type PerkFooterFactory = (
|
|
|
364
329
|
/**
|
|
365
330
|
* The perk-owned footer factory (charter D2): replaces pi's default footer wholesale with one
|
|
366
331
|
* 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
|
|
332
|
+
* render): the objective value via the handle, branch/guests via `footerData` (excluding perk's
|
|
333
|
+
* own `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the value
|
|
369
334
|
* 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.
|
|
335
|
+
* every handle recompose and on branch change; `dispose` detaches both. Lifecycle (pi ≥ 0.84,
|
|
336
|
+
* verified at 0.84.1): `setExtensionFooter` disposes a replaced factory's component, and pi's
|
|
337
|
+
* `resetExtensionUI` restores the built-in footer (disposing this one) on /reload and before
|
|
338
|
+
* session replacement — so installing a fresh factory per headful `session_start` leaks nothing.
|
|
339
|
+
*
|
|
340
|
+
* Two DELIBERATE divergences from pi's default footer remain: no `(auto)` context suffix
|
|
341
|
+
* (auto-compact state is not extension-readable) and no cost/`(sub)` segment (charter scope).
|
|
371
342
|
*/
|
|
372
343
|
export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
373
344
|
return (tui, theme, footerData) => {
|
|
@@ -382,8 +353,7 @@ export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
|
382
353
|
const rate = deps.getCacheHitRate();
|
|
383
354
|
const parts: FooterParts = {
|
|
384
355
|
identity: deps.identity,
|
|
385
|
-
objective: deps.status.get(
|
|
386
|
-
checkpoints: deps.status.get("checkpoints"),
|
|
356
|
+
objective: deps.status.get(),
|
|
387
357
|
branch: footerData.getGitBranch() ?? undefined,
|
|
388
358
|
model: deps.getModelId() ?? undefined,
|
|
389
359
|
thinking: deps.getThinkingLevel() ?? undefined,
|
|
@@ -404,7 +374,10 @@ export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
|
404
374
|
};
|
|
405
375
|
}
|
|
406
376
|
|
|
407
|
-
/**
|
|
377
|
+
/**
|
|
378
|
+
* Install the perk-owned footer (sole-owner law, D2); no-op headless. Safe to call on every
|
|
379
|
+
* headful `session_start`: pi ≥ 0.84 disposes the replaced factory (see `perkFooter`).
|
|
380
|
+
*/
|
|
408
381
|
export function installPerkFooter(
|
|
409
382
|
target: { hasUI: boolean; ui: { setFooter(factory: PerkFooterFactory | undefined): void } },
|
|
410
383
|
deps: PerkFooterDeps,
|
|
@@ -413,107 +386,15 @@ export function installPerkFooter(
|
|
|
413
386
|
target.ui.setFooter(perkFooter(deps));
|
|
414
387
|
}
|
|
415
388
|
|
|
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
|
-
}
|
|
389
|
+
// --- format helpers --------------------------------------------------------------------------
|
|
512
390
|
|
|
391
|
+
/** Pi 0.84.1's default-footer `formatTokens` tiers, mirrored exactly (`200000` → `200k`). */
|
|
513
392
|
function formatTokens(tokens: number): string {
|
|
514
393
|
if (tokens < 1000) return `${tokens}`;
|
|
515
|
-
|
|
516
|
-
return `${(tokens / 1000)
|
|
394
|
+
if (tokens < 10_000) return `${(tokens / 1000).toFixed(1)}k`;
|
|
395
|
+
if (tokens < 1_000_000) return `${Math.round(tokens / 1000)}k`;
|
|
396
|
+
if (tokens < 10_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
397
|
+
return `${Math.round(tokens / 1_000_000)}M`;
|
|
517
398
|
}
|
|
518
399
|
|
|
519
400
|
function formatElapsed(ms: number): string {
|
|
@@ -525,17 +406,31 @@ function formatElapsed(ms: number): string {
|
|
|
525
406
|
return `${hr}h${min % 60}m`;
|
|
526
407
|
}
|
|
527
408
|
|
|
528
|
-
/** A compact one-line budget summary (e.g. `
|
|
409
|
+
/** A compact one-line budget summary (e.g. `12k tok · 5m`, `1.2M tok · 2h5m` past 1M). */
|
|
529
410
|
export function formatBudgetLine(args: { tokens: number; elapsedMs: number }): string {
|
|
530
411
|
return `${formatTokens(args.tokens)} tok · ${formatElapsed(args.elapsedMs)}`;
|
|
531
412
|
}
|
|
532
413
|
|
|
533
|
-
// ---
|
|
534
|
-
// The audit §2.3 verdict (docs/design/pi-adoption-audit.md): perk's
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
414
|
+
// --- display-only transcript entry renderers -----------------------------------------------------
|
|
415
|
+
// The audit §2.3 verdict (docs/design/pi-adoption-audit.md): perk's custom-entry families are
|
|
416
|
+
// display-only. Most render as durable one-line transition markers; report detail is the generic
|
|
417
|
+
// full-diagnostic family and always renders every logical row. Renderer BODIES live here (a
|
|
418
|
+
// transcript renderer IS a rich-UI surface the surfaces module owns); registration is wiring at the
|
|
419
|
+
// feature modules via the `registerTranscriptRenderer` seam below. Renderers are an
|
|
420
|
+
// interactive-TUI-only concern (never invoked in json/RPC mode), so registration is inert-safe
|
|
421
|
+
// everywhere.
|
|
422
|
+
|
|
423
|
+
/** The generic display-only transcript entry carrying complete multiline warm-command reports. */
|
|
424
|
+
export const REPORT_DETAIL_TYPE = "perk:report-detail";
|
|
425
|
+
|
|
426
|
+
export interface ReportDetailEntryHost {
|
|
427
|
+
appendEntry(customType: string, data?: unknown): void;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Create the sink attached to each warm-command context by `registerPerkCommand`. */
|
|
431
|
+
export function createReportDetailSink(host: ReportDetailEntryHost): ReportDetailSink {
|
|
432
|
+
return (text, severity) => host.appendEntry(REPORT_DETAIL_TYPE, { text, severity });
|
|
433
|
+
}
|
|
539
434
|
|
|
540
435
|
/** Structural slice of pi's `CustomEntry` — the only field the marker renderers read. */
|
|
541
436
|
export interface TranscriptEntryLike {
|
|
@@ -585,15 +480,13 @@ export function registerTranscriptRenderer(
|
|
|
585
480
|
|
|
586
481
|
/**
|
|
587
482
|
* 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).
|
|
483
|
+
* human-requested scrollback and renders its full detail unbounded (the whole btw answer).
|
|
590
484
|
*/
|
|
591
485
|
export const TRANSCRIPT_MARKER_MAX_LINES = 1;
|
|
592
486
|
|
|
593
487
|
/**
|
|
594
488
|
* The collapsed-marker grammar: the `report()` transition grammar `perk: <scope> — <message>`,
|
|
595
|
-
* dim, D9-truncated. Emoji stay footer-only (D3)
|
|
596
|
-
* checkpoint step lines.
|
|
489
|
+
* dim, D9-truncated. Emoji stay footer-only (D3).
|
|
597
490
|
*/
|
|
598
491
|
function markerLine(scope: string, message: string, theme: ThemeLike, width: number): string {
|
|
599
492
|
return truncateToWidth(theme.fg("dim", `perk: ${scope} — ${message}`), width);
|
|
@@ -605,44 +498,103 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
605
498
|
return value as Record<string, unknown>;
|
|
606
499
|
}
|
|
607
500
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const record = asRecord(data);
|
|
501
|
+
function asPlainRecord(value: unknown): Record<string, unknown> | null {
|
|
502
|
+
const record = asRecord(value);
|
|
611
503
|
if (record === null) return null;
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
)
|
|
623
|
-
|
|
504
|
+
const prototype = Object.getPrototypeOf(record);
|
|
505
|
+
if (prototype !== Object.prototype && prototype !== null) return null;
|
|
506
|
+
return record;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function skipControlString(text: string, start: number, osc: boolean): number {
|
|
510
|
+
let index = start;
|
|
511
|
+
while (index < text.length) {
|
|
512
|
+
const code = text.charCodeAt(index);
|
|
513
|
+
if ((osc && code === 0x07) || code === 0x9c) return index + 1;
|
|
514
|
+
if (code === 0x1b && text.charCodeAt(index + 1) === 0x5c) return index + 2;
|
|
515
|
+
index += 1;
|
|
516
|
+
}
|
|
517
|
+
return index;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function skipControlSequence(text: string, start: number): number {
|
|
521
|
+
let index = start;
|
|
522
|
+
while (index < text.length) {
|
|
523
|
+
const code = text.charCodeAt(index);
|
|
524
|
+
index += 1;
|
|
525
|
+
if (code >= 0x40 && code <= 0x7e) break;
|
|
526
|
+
}
|
|
527
|
+
return index;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Strip terminal controls from the display projection; the persisted report text stays exact. */
|
|
531
|
+
function stripTerminalControls(text: string): string {
|
|
532
|
+
let clean = "";
|
|
533
|
+
let index = 0;
|
|
534
|
+
while (index < text.length) {
|
|
535
|
+
const code = text.charCodeAt(index);
|
|
536
|
+
if (code === 0x1b) {
|
|
537
|
+
const next = text.charCodeAt(index + 1);
|
|
538
|
+
if (next === 0x5b) {
|
|
539
|
+
index = skipControlSequence(text, index + 2);
|
|
540
|
+
} else if (next === 0x5d) {
|
|
541
|
+
index = skipControlString(text, index + 2, true);
|
|
542
|
+
} else if (next === 0x50 || next === 0x58 || next === 0x5e || next === 0x5f) {
|
|
543
|
+
index = skipControlString(text, index + 2, false);
|
|
544
|
+
} else {
|
|
545
|
+
index += 1;
|
|
546
|
+
while (index < text.length) {
|
|
547
|
+
const part = text.charCodeAt(index);
|
|
548
|
+
if (part < 0x20 || part > 0x2f) break;
|
|
549
|
+
index += 1;
|
|
550
|
+
}
|
|
551
|
+
const final = text.charCodeAt(index);
|
|
552
|
+
if (final >= 0x30 && final <= 0x7e) index += 1;
|
|
553
|
+
}
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (code === 0x9b) {
|
|
557
|
+
index = skipControlSequence(text, index + 1);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (code === 0x9d) {
|
|
561
|
+
index = skipControlString(text, index + 1, true);
|
|
562
|
+
continue;
|
|
624
563
|
}
|
|
625
|
-
|
|
564
|
+
if (code === 0x90 || code === 0x98 || code === 0x9e || code === 0x9f) {
|
|
565
|
+
index = skipControlString(text, index + 1, false);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
|
|
569
|
+
index += 1;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
clean += text[index];
|
|
573
|
+
index += 1;
|
|
626
574
|
}
|
|
627
|
-
return
|
|
575
|
+
return clean;
|
|
628
576
|
}
|
|
629
577
|
|
|
630
578
|
/**
|
|
631
|
-
* `perk:
|
|
632
|
-
*
|
|
633
|
-
*
|
|
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.
|
|
579
|
+
* `perk:report-detail` full diagnostic renderer. Unlike collapsed transition markers, report detail
|
|
580
|
+
* always renders every logical row; the expanded flag does not alter it. The first row carries the
|
|
581
|
+
* live severity color and continuation rows are dim. Blank rows remain blank.
|
|
636
582
|
*/
|
|
637
|
-
export const
|
|
638
|
-
const
|
|
639
|
-
if (
|
|
640
|
-
const
|
|
583
|
+
export const reportDetailEntryRenderer: TranscriptRenderer = (entry, _options, theme) => {
|
|
584
|
+
const data = asPlainRecord(entry.data);
|
|
585
|
+
if (data === null) return undefined;
|
|
586
|
+
const text = data.text;
|
|
587
|
+
const severity = data.severity;
|
|
588
|
+
if (typeof text !== "string" || text.trim().length === 0) return undefined;
|
|
589
|
+
if (severity !== "info" && severity !== "warning" && severity !== "error") return undefined;
|
|
641
590
|
return {
|
|
642
591
|
render(width) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
592
|
+
return text.split(/\r\n|\n|\r/).map((line, index) => {
|
|
593
|
+
const safeLine = stripTerminalControls(line);
|
|
594
|
+
if (safeLine.length === 0) return "";
|
|
595
|
+
const color = index === 0 && severity !== "info" ? severity : "dim";
|
|
596
|
+
return truncateToWidth(theme.fg(color, safeLine), width);
|
|
597
|
+
});
|
|
646
598
|
},
|
|
647
599
|
};
|
|
648
600
|
};
|
|
@@ -769,17 +721,3 @@ export const btwThreadResetEntryRenderer: TranscriptRenderer = (entry, options,
|
|
|
769
721
|
},
|
|
770
722
|
};
|
|
771
723
|
};
|
|
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
|
-
}
|