@mgiles/perk 1.0.1 → 2.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 +67 -57
- package/extension/adapters/planAdapterPlannotator.ts +27 -41
- package/extension/adapters/planAdapterTombell.ts +15 -28
- package/extension/adapters/todoAdapterJuicesharp.ts +10 -13
- package/extension/checkpoints/checkpoints.ts +21 -13
- package/extension/doors/address.ts +6 -5
- package/extension/doors/askUser.ts +12 -8
- package/extension/doors/ciExecutor.ts +23 -15
- package/extension/doors/hunkHandoff.ts +202 -0
- package/extension/doors/land.ts +33 -10
- package/extension/doors/learn.ts +240 -34
- package/extension/doors/learnFactory.ts +144 -0
- package/extension/doors/lifecycleGates.ts +2 -1
- package/extension/doors/plannotatorHandoff.ts +509 -0
- package/extension/doors/prReview.ts +22 -37
- package/extension/doors/prReviewBrowser.ts +341 -0
- package/extension/doors/prReviewTerminal.ts +267 -0
- package/extension/doors/ready.ts +2 -1
- package/extension/doors/selfcheck.ts +240 -6
- package/extension/doors/submit.ts +37 -19
- package/extension/doors/submitPrReview.ts +408 -0
- package/extension/factories/implementHere.ts +116 -0
- package/extension/factories/objective.ts +17 -6
- package/extension/factories/objectiveAuthor.ts +15 -32
- package/extension/factories/objectiveDraft.ts +1 -1
- package/extension/factories/objectivePlan.ts +16 -34
- package/extension/factories/objectiveSave.ts +7 -17
- package/extension/factories/planMode.ts +24 -38
- package/extension/factories/planReview.ts +291 -177
- package/extension/factories/planSave.ts +23 -8
- package/extension/index.ts +117 -20
- package/extension/substrate/bindingDelivery.ts +32 -10
- package/extension/substrate/bindings.ts +4 -2
- package/extension/substrate/cache.ts +36 -9
- package/extension/substrate/clipboard.ts +81 -0
- package/extension/substrate/command.ts +25 -0
- package/extension/substrate/config.ts +98 -76
- package/extension/substrate/consoleCapture.ts +90 -0
- package/extension/substrate/git.ts +77 -0
- package/extension/substrate/miniJinja.ts +480 -0
- package/extension/substrate/paths.ts +38 -0
- package/extension/substrate/prompts.ts +15 -24
- package/extension/substrate/providers.ts +62 -8
- package/extension/substrate/sessionData.ts +1 -1
- package/extension/substrate/sessionPointers.ts +184 -0
- package/extension/substrate/structuredOutput.ts +3 -1
- package/extension/substrate/terminalLaunch.ts +178 -0
- package/extension/substrate/toolGating.ts +332 -73
- package/extension/substrate/toolParams.ts +7 -0
- package/extension/substrate/workflowState.ts +54 -2
- package/extension/surfaces/footerProvider.ts +8 -4
- package/extension/surfaces/surfaces.ts +338 -13
- package/extension/vendor/btw/btw.ts +10 -0
- package/extension/worker/readOnlySession.ts +19 -6
- package/extension/worker/worker.ts +187 -31
- package/extension/workerMain.ts +14 -13
- package/package.json +3 -7
- package/prompts/README.md +56 -5
- package/prompts/_fixtures/cases.yaml +52 -131
- package/prompts/_fixtures/golden/cond_elif-a.txt +3 -0
- package/prompts/_fixtures/golden/cond_elif-b.txt +3 -0
- package/prompts/_fixtures/golden/cond_elif-c.txt +3 -0
- package/prompts/_fixtures/golden/cond_if-false.txt +3 -0
- package/prompts/_fixtures/golden/cond_if-true.txt +3 -0
- package/prompts/_fixtures/golden/cond_ops-1.txt +3 -0
- package/prompts/_fixtures/golden/cond_ops-2.txt +3 -0
- package/prompts/_fixtures/golden/no_trailing_nl.txt +2 -0
- package/prompts/_fixtures/golden/trailing_nl.txt +2 -0
- package/prompts/_fixtures/golden/trim_block.txt +3 -0
- package/prompts/_fixtures/golden/trim_inline.txt +1 -0
- package/prompts/_fixtures/live.yaml +396 -0
- package/prompts/_fixtures/templates/cond_elif.md +9 -0
- package/prompts/_fixtures/templates/cond_if.md +7 -0
- package/prompts/_fixtures/templates/cond_ops.md +3 -0
- package/prompts/_fixtures/templates/no_trailing_nl.md +2 -0
- package/prompts/_fixtures/templates/trailing_nl.md +2 -0
- package/prompts/_fixtures/templates/trim_block.md +5 -0
- package/prompts/_fixtures/templates/trim_inline.md +1 -0
- package/prompts/contexts/adapters/juicesharp-todo.md +7 -0
- package/prompts/contexts/adapters/plannotator-objective.md +7 -0
- package/prompts/contexts/adapters/plannotator-plan.md +6 -0
- package/prompts/contexts/adapters/tombell-plan.md +17 -0
- package/prompts/contexts/objective-authoring.md +20 -0
- package/prompts/contexts/plan-authoring.md +24 -0
- package/prompts/contexts/read-only.md +10 -0
- package/prompts/stages/conflict-resolution.md +4 -0
- package/prompts/stages/learn-code.md +8 -0
- package/prompts/stages/learn-docs.md +7 -6
- package/prompts/stages/learn-orchestrate.md +6 -0
- package/prompts/stages/learn.md +1 -1
- package/prompts/stages/objective-author/adopt.md +12 -0
- package/prompts/stages/objective-author/file.md +9 -0
- package/prompts/stages/objective-author/seed.md +9 -0
- package/prompts/stages/objective-plan/guidance.md +1 -1
- package/prompts/stages/objective-plan/seed.md +3 -2
- package/prompts/stages/objective-reconcile.md +7 -0
- package/prompts/stages/objective-replan.md +14 -0
- package/prompts/stages/objective-save.md +9 -0
- package/prompts/stages/plan-from/adopt.md +10 -0
- package/prompts/stages/plan-from/file.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +11 -0
- package/prompts/stages/pr-review-browser/foreign.md +11 -0
- package/prompts/stages/pr-review-terminal/active.md +12 -0
- package/prompts/stages/pr-review-terminal/foreign.md +13 -0
- package/prompts/stages/pr-review-terminal/local.md +4 -0
- package/prompts/stages/pr-review.md +6 -0
- package/prompts/stages/replan.md +13 -0
- package/prompts/stages/skills/create-from.md +15 -0
- package/prompts/stages/skills/create.md +9 -0
- package/prompts/stages/skills/refine.md +9 -0
- package/shared/README.md +23 -13
- package/shared/bindings.yaml +22 -2
- package/shared/contracts-history.md +191 -0
- package/shared/contracts.md +2258 -1638
- package/shared/providers.yaml +8 -1
- package/shared/registry.yaml +10 -11
- package/shared/schemas/contracts/bindings.schema.json +38 -0
- package/shared/schemas/contracts/providers.schema.json +89 -0
- package/shared/schemas/contracts/registry.schema.json +98 -0
- package/shared/schemas/inputs/handoff-arg.schema.json +6 -0
- package/shared/schemas/inputs/resolve-threads-batch.schema.json +37 -0
- package/shared/schemas/inputs/review-post-batch.schema.json +84 -0
- package/shared/schemas/inputs/review-submit-batch.schema.json +66 -0
- package/shared/schemas/inputs/structured-roadmap-node.schema.json +102 -0
- package/shared/schemas/outputs/doctor-report.schema.json +236 -0
- package/shared/schemas/outputs/init-report.schema.json +419 -0
- package/shared/schemas/outputs/learn-capture.schema.json +90 -0
- package/shared/schemas/outputs/learn-skip.schema.json +59 -0
- package/shared/schemas/outputs/plan-save.schema.json +209 -0
- package/shared/schemas/outputs/pr-feedback.schema.json +334 -0
- package/shared/schemas/outputs/pr-land.schema.json +187 -0
- package/shared/schemas/outputs/pr-ready.schema.json +75 -0
- package/shared/schemas/outputs/pr-review-checkout.schema.json +69 -0
- package/shared/schemas/outputs/pr-review-cleanup.schema.json +54 -0
- package/shared/schemas/outputs/pr-review-context.schema.json +86 -0
- package/shared/schemas/outputs/pr-review-submit.schema.json +64 -0
- package/shared/schemas/outputs/pr-submit.schema.json +147 -0
- package/extension/doors/learnDocs.ts +0 -99
- package/prompts/_fixtures/golden/address-action-model.txt +0 -10
- package/prompts/_fixtures/golden/address-action.txt +0 -10
- package/prompts/_fixtures/golden/address-preview-model.txt +0 -6
- package/prompts/_fixtures/golden/address-preview.txt +0 -6
- package/prompts/_fixtures/golden/implement-github.txt +0 -8
- package/prompts/_fixtures/golden/learn-docs.txt +0 -8
- package/prompts/_fixtures/golden/learn-github.txt +0 -11
- package/prompts/_fixtures/golden/learn-linear.txt +0 -11
- package/prompts/_fixtures/golden/learn-no-ref.txt +0 -8
- package/prompts/_fixtures/golden/learn-other.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-guidance-linear.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-guidance.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-seed-linear.txt +0 -20
- package/prompts/_fixtures/golden/objective-plan-seed.txt +0 -15
- package/prompts/_fixtures/golden/objective-read-linear-nourl.txt +0 -1
- package/prompts/_fixtures/golden/objective-read-linear.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-github.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-linear.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-other.txt +0 -1
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
18
18
|
|
|
19
|
+
// `Key` is keybinding vocabulary (`pi.registerShortcut(Key.ctrlAlt("p"), …)`), not rich UI —
|
|
20
|
+
// re-exported so pi-tui imports stay structurally confined to the surfaces module (the
|
|
21
|
+
// surfacesGuard pi-tui import rule) without allowlisting the shortcut-registering modules.
|
|
22
|
+
export { Key } from "@earendil-works/pi-tui";
|
|
19
23
|
// Re-exports: the notify seam stays in report.ts; surfaces.ts is the one import for UI vocabulary.
|
|
20
24
|
export { type ReportTarget, report, type Severity } from "./report.ts";
|
|
21
25
|
|
|
@@ -30,7 +34,7 @@ export const WIDGET_SLOT_CHECKPOINTS = "perk-checkpoints";
|
|
|
30
34
|
export const MARK_CHECKPOINTS = "📋";
|
|
31
35
|
export const MARK_OBJECTIVE = "🎯";
|
|
32
36
|
|
|
33
|
-
// --- glyph vocabulary (charter §5 / D3) — data only; themed rendering
|
|
37
|
+
// --- glyph vocabulary (charter §5 / D3) — data only; themed rendering lives in the widget/footer builders below ---
|
|
34
38
|
export type GlyphKind = "done" | "current" | "pending" | "warning" | "failure";
|
|
35
39
|
export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> = {
|
|
36
40
|
done: { glyph: "✓", themeColor: "success" },
|
|
@@ -40,7 +44,7 @@ export const GLYPHS: Record<GlyphKind, { glyph: string; themeColor: string }> =
|
|
|
40
44
|
failure: { glyph: "✗", themeColor: "error" },
|
|
41
45
|
};
|
|
42
46
|
|
|
43
|
-
// --- height bounds (charter §4 / D1/D8) —
|
|
47
|
+
// --- height bounds (charter §4 / D1/D8) — enforced by the report() notify budget and the footer/widget builders ---
|
|
44
48
|
export const NOTIFY_MAX_LINES = 1;
|
|
45
49
|
export const FOOTER_MAX_LINES = 1;
|
|
46
50
|
export const CHECKPOINTS_WIDGET_MAX_LINES = 4;
|
|
@@ -192,8 +196,8 @@ export function createPerkStatus(): PerkStatusHandle {
|
|
|
192
196
|
/**
|
|
193
197
|
* The raw material for one composed footer line. Left group (charter order 1–3): `identity`,
|
|
194
198
|
* `objective`, `checkpoints` — the segments render verbatim (they carry their own 🎯/📋 marks).
|
|
195
|
-
* Right group (charter order 4, 5, +context, 6): `branch`, `model`, `
|
|
196
|
-
* right-aligned, non-segment system text dim-themed.
|
|
199
|
+
* Right group (charter order 4, 5, +context, 6): `branch`, `model`, `thinking`, `cache`,
|
|
200
|
+
* `context`, `guests` — right-aligned, non-segment system text dim-themed.
|
|
197
201
|
*/
|
|
198
202
|
export interface FooterParts {
|
|
199
203
|
/** e.g. `perk v0.0.1` — standing identity (D7), dim. */
|
|
@@ -206,12 +210,54 @@ export interface FooterParts {
|
|
|
206
210
|
branch?: string;
|
|
207
211
|
/** Model id (dim); omitted when no model. */
|
|
208
212
|
model?: string;
|
|
213
|
+
/** The session thinking level (dim; e.g. `high`/`off`); omitted when there is no model. */
|
|
214
|
+
thinking?: string;
|
|
215
|
+
/** The prompt-cache-hit segment, e.g. `CH42.3%` (dim; right group); omitted until cache activity. */
|
|
216
|
+
cache?: string;
|
|
209
217
|
/** Context usage — rendered `<pct>%/<window>` (dim; warning >70, error >90; `?` when null). */
|
|
210
218
|
context?: { percent: number | null; contextWindow: number };
|
|
211
219
|
/** Guest extension statuses (dim), pre-sorted by slot key; sanitized here. */
|
|
212
220
|
guests: string[];
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
/**
|
|
224
|
+
* A structural slice of pi's `SessionEntry` — only what `latestCacheHitRate` reads. Keeps
|
|
225
|
+
* surfaces.ts dependency-light (no pi imports; `SessionEntry[]` is assignable) — the same
|
|
226
|
+
* structural-mirror pattern as `FooterDataLike`/`ThemeLike`.
|
|
227
|
+
*/
|
|
228
|
+
export interface UsageEntryLike {
|
|
229
|
+
type: string;
|
|
230
|
+
message?: {
|
|
231
|
+
role?: string;
|
|
232
|
+
usage?: { input: number; cacheRead: number; cacheWrite: number };
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 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 are
|
|
239
|
+
* unexported; the `sanitizeGuestStatus` reimplementation precedent). Includes pi's display gate:
|
|
240
|
+
* returns `null` unless the session shows cache activity (total cacheRead or cacheWrite > 0) AND
|
|
241
|
+
* the latest usage-bearing assistant message has prompt tokens > 0 (a trailing zero-prompt-token
|
|
242
|
+
* assistant message resets the rate, exactly like pi's `undefined`).
|
|
243
|
+
*/
|
|
244
|
+
export function latestCacheHitRate(entries: readonly UsageEntryLike[]): number | null {
|
|
245
|
+
let totalCacheRead = 0;
|
|
246
|
+
let totalCacheWrite = 0;
|
|
247
|
+
let latest: number | null = null;
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (entry.type !== "message" || entry.message?.role !== "assistant") continue;
|
|
250
|
+
const usage = entry.message.usage;
|
|
251
|
+
if (usage === undefined) continue;
|
|
252
|
+
totalCacheRead += usage.cacheRead;
|
|
253
|
+
totalCacheWrite += usage.cacheWrite;
|
|
254
|
+
const promptTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
255
|
+
latest = promptTokens > 0 ? (usage.cacheRead / promptTokens) * 100 : null;
|
|
256
|
+
}
|
|
257
|
+
if (totalCacheRead <= 0 && totalCacheWrite <= 0) return null;
|
|
258
|
+
return latest;
|
|
259
|
+
}
|
|
260
|
+
|
|
215
261
|
/** Pi's `sanitizeStatusText` behavior, reimplemented locally (pi does not export it). */
|
|
216
262
|
function sanitizeGuestStatus(text: string): string {
|
|
217
263
|
return text
|
|
@@ -236,15 +282,17 @@ function formatContextSegment(
|
|
|
236
282
|
* Compose THE one footer line (FOOTER_MAX_LINES = 1): left group = identity + objective +
|
|
237
283
|
* checkpoints (two-space-joined, charter order); right group = branch + model + context + guests
|
|
238
284
|
* (two-space-joined), right-aligned with ≥2 spaces of padding. When the line exceeds `width`,
|
|
239
|
-
* whole segments drop in the extended D9 order — guests (rightmost-first) →
|
|
240
|
-
* context → checkpoints; `identity` and `objective` are NEVER dropped — then
|
|
241
|
-
* as the last resort (ANSI- and 2-cell-emoji-aware).
|
|
285
|
+
* whole segments drop in the extended D9 order — guests (rightmost-first) → thinking → model →
|
|
286
|
+
* branch → cache → context → checkpoints; `identity` and `objective` are NEVER dropped — then
|
|
287
|
+
* `truncateToWidth` as the last resort (ANSI- and 2-cell-emoji-aware).
|
|
242
288
|
*/
|
|
243
289
|
export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: number): string {
|
|
244
290
|
const keep = {
|
|
245
291
|
guests: parts.guests.map((g) => sanitizeGuestStatus(g)),
|
|
246
292
|
model: true,
|
|
293
|
+
thinking: true,
|
|
247
294
|
branch: true,
|
|
295
|
+
cache: true,
|
|
248
296
|
context: true,
|
|
249
297
|
checkpoints: true,
|
|
250
298
|
};
|
|
@@ -255,6 +303,8 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
255
303
|
const right: string[] = [];
|
|
256
304
|
if (keep.branch && parts.branch !== undefined) right.push(theme.fg("dim", parts.branch));
|
|
257
305
|
if (keep.model && parts.model !== undefined) right.push(theme.fg("dim", parts.model));
|
|
306
|
+
if (keep.thinking && parts.thinking !== undefined) right.push(theme.fg("dim", parts.thinking));
|
|
307
|
+
if (keep.cache && parts.cache !== undefined) right.push(theme.fg("dim", parts.cache));
|
|
258
308
|
if (keep.context && parts.context !== undefined) {
|
|
259
309
|
right.push(formatContextSegment(parts.context, theme));
|
|
260
310
|
}
|
|
@@ -268,8 +318,10 @@ export function composeFooterLine(parts: FooterParts, theme: ThemeLike, width: n
|
|
|
268
318
|
let line = compose();
|
|
269
319
|
while (visibleWidth(line) > width) {
|
|
270
320
|
if (keep.guests.length > 0) keep.guests.pop();
|
|
321
|
+
else if (keep.thinking) keep.thinking = false;
|
|
271
322
|
else if (keep.model) keep.model = false;
|
|
272
323
|
else if (keep.branch) keep.branch = false;
|
|
324
|
+
else if (keep.cache) keep.cache = false;
|
|
273
325
|
else if (keep.context) keep.context = false;
|
|
274
326
|
else if (keep.checkpoints) keep.checkpoints = false;
|
|
275
327
|
else break; // identity + objective only — nothing left to drop
|
|
@@ -293,6 +345,8 @@ export interface PerkFooterDeps {
|
|
|
293
345
|
identity: string;
|
|
294
346
|
status: PerkStatusHandle;
|
|
295
347
|
getModelId(): string | null;
|
|
348
|
+
getThinkingLevel(): string | null;
|
|
349
|
+
getCacheHitRate(): number | null;
|
|
296
350
|
getContext(): { percent: number | null; contextWindow: number } | null;
|
|
297
351
|
}
|
|
298
352
|
|
|
@@ -312,7 +366,7 @@ export type PerkFooterFactory = (
|
|
|
312
366
|
* line in the intended split layout. `render` gathers everything live per call (D10 stateless
|
|
313
367
|
* render): segments via the handle, branch/guests via `footerData` (excluding perk's own
|
|
314
368
|
* `STATUS_SLOT_PERK` — the slot keeps publishing for RPC, but the footer renders the segments
|
|
315
|
-
* directly), model/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
369
|
+
* directly), model/cache/context via the deps closures. Reactivity (the D2 contract): repaints on
|
|
316
370
|
* every handle recompose and on branch change; `dispose` detaches both.
|
|
317
371
|
*/
|
|
318
372
|
export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
@@ -325,12 +379,15 @@ export function perkFooter(deps: PerkFooterDeps): PerkFooterFactory {
|
|
|
325
379
|
.filter(([key]) => key !== STATUS_SLOT_PERK)
|
|
326
380
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
327
381
|
.map(([, text]) => text);
|
|
382
|
+
const rate = deps.getCacheHitRate();
|
|
328
383
|
const parts: FooterParts = {
|
|
329
384
|
identity: deps.identity,
|
|
330
385
|
objective: deps.status.get("objective"),
|
|
331
386
|
checkpoints: deps.status.get("checkpoints"),
|
|
332
387
|
branch: footerData.getGitBranch() ?? undefined,
|
|
333
388
|
model: deps.getModelId() ?? undefined,
|
|
389
|
+
thinking: deps.getThinkingLevel() ?? undefined,
|
|
390
|
+
cache: rate === null ? undefined : `CH${rate.toFixed(1)}%`,
|
|
334
391
|
context: deps.getContext() ?? undefined,
|
|
335
392
|
guests,
|
|
336
393
|
};
|
|
@@ -431,14 +488,28 @@ export function renderProgressLines(
|
|
|
431
488
|
if (item.kind === "elision") {
|
|
432
489
|
return truncateToWidth(theme.fg("dim", `… +${item.hidden} ${item.side}`), width);
|
|
433
490
|
}
|
|
434
|
-
|
|
435
|
-
const glyph = theme.fg(GLYPHS[kind].themeColor, GLYPHS[kind].glyph);
|
|
436
|
-
const text = `${item.step.step}. ${item.step.text}`;
|
|
437
|
-
const line = `${glyph} ${kind === "done" ? theme.fg("muted", text) : text}`;
|
|
438
|
-
return truncateToWidth(line, width);
|
|
491
|
+
return renderStepLine(state, item.step, theme, width);
|
|
439
492
|
});
|
|
440
493
|
}
|
|
441
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
|
+
}
|
|
512
|
+
|
|
442
513
|
function formatTokens(tokens: number): string {
|
|
443
514
|
if (tokens < 1000) return `${tokens}`;
|
|
444
515
|
// Whole-k values render bare (`200k`, matching pi's footer); fractional keep one decimal.
|
|
@@ -458,3 +529,257 @@ function formatElapsed(ms: number): string {
|
|
|
458
529
|
export function formatBudgetLine(args: { tokens: number; elapsedMs: number }): string {
|
|
459
530
|
return `${formatTokens(args.tokens)} tok · ${formatElapsed(args.elapsedMs)}`;
|
|
460
531
|
}
|
|
532
|
+
|
|
533
|
+
// --- the transcript markers — display-only entry renderers ---------------------------------------
|
|
534
|
+
// The audit §2.3 verdict (docs/design/pi-adoption-audit.md): perk's four display-only custom-entry
|
|
535
|
+
// families render as durable one-line transcript markers. Renderer BODIES live here (a transcript
|
|
536
|
+
// renderer IS a rich-UI surface the surfaces module owns); registration is wiring at the feature
|
|
537
|
+
// modules via the `registerTranscriptRenderer` seam below. Renderers are an interactive-TUI-only
|
|
538
|
+
// concern (never invoked in json/RPC mode), so registration is inert-safe everywhere.
|
|
539
|
+
|
|
540
|
+
/** Structural slice of pi's `CustomEntry` — the only field the marker renderers read. */
|
|
541
|
+
export interface TranscriptEntryLike {
|
|
542
|
+
data?: unknown;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Structural mirror of pi's `EntryRenderOptions`. */
|
|
546
|
+
export interface EntryRenderOptionsLike {
|
|
547
|
+
expanded: boolean;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* A transcript entry renderer, assignable to pi's `EntryRenderer<unknown>`: the params are
|
|
552
|
+
* structural supertypes of pi's (`CustomEntry`/`EntryRenderOptions`/`Theme`), and the returned
|
|
553
|
+
* object satisfies pi-tui's structural `Component` (`render(width): string[]`; `handleInput` is
|
|
554
|
+
* optional). `undefined` = render nothing (malformed/missing `data` stays invisible — exactly the
|
|
555
|
+
* pre-renderer behavior).
|
|
556
|
+
*/
|
|
557
|
+
export type TranscriptRenderer = (
|
|
558
|
+
entry: TranscriptEntryLike,
|
|
559
|
+
options: EntryRenderOptionsLike,
|
|
560
|
+
theme: ThemeLike,
|
|
561
|
+
) => { render(width: number): string[] } | undefined;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* The minimal host surface the registration seam needs. The member is OPTIONAL and
|
|
565
|
+
* method-syntax (bivariant — the `PerkFooterFactory` recipe): pi ≥ 0.80.4's `ExtensionAPI`
|
|
566
|
+
* satisfies it; pre-0.80.4 hosts simply don't have the method.
|
|
567
|
+
*/
|
|
568
|
+
export interface TranscriptRendererHost {
|
|
569
|
+
registerEntryRenderer?(customType: string, renderer: TranscriptRenderer): void;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* The one sanctioned `registerEntryRenderer` call site (guard-confined) carrying the one typeof
|
|
574
|
+
* feature-detect: on a pre-0.80.4 host the method is absent (calling it would `TypeError`), so
|
|
575
|
+
* registration is a silent no-op and the entries stay invisible — exactly today's behavior.
|
|
576
|
+
*/
|
|
577
|
+
export function registerTranscriptRenderer(
|
|
578
|
+
host: TranscriptRendererHost,
|
|
579
|
+
customType: string,
|
|
580
|
+
renderer: TranscriptRenderer,
|
|
581
|
+
): void {
|
|
582
|
+
if (typeof host.registerEntryRenderer !== "function") return; // pre-0.80.4 host: inert
|
|
583
|
+
host.registerEntryRenderer(customType, renderer);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Charter budget: a COLLAPSED transcript marker is exactly one line. The expanded view is
|
|
588
|
+
* human-requested scrollback and renders its full detail unbounded (all checkpoint steps, the
|
|
589
|
+
* whole btw answer).
|
|
590
|
+
*/
|
|
591
|
+
export const TRANSCRIPT_MARKER_MAX_LINES = 1;
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The collapsed-marker grammar: the `report()` transition grammar `perk: <scope> — <message>`,
|
|
595
|
+
* dim, D9-truncated. Emoji stay footer-only (D3); themed §5 glyphs appear only in expanded
|
|
596
|
+
* checkpoint step lines.
|
|
597
|
+
*/
|
|
598
|
+
function markerLine(scope: string, message: string, theme: ThemeLike, width: number): string {
|
|
599
|
+
return truncateToWidth(theme.fg("dim", `perk: ${scope} — ${message}`), width);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** The three-clause object-shape guard: a plain (non-null, non-array) object or null. */
|
|
603
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
604
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
605
|
+
return value as Record<string, unknown>;
|
|
606
|
+
}
|
|
607
|
+
|
|
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
|
+
/**
|
|
651
|
+
* The first matching workflow-state field's marker message — a deliberately BOUNDED vocabulary
|
|
652
|
+
* (the four headline fields + a SET `objective_node_claim`), extensible later. Bookkeeping deltas
|
|
653
|
+
* (`session_artifacts`, `last_review*`, `conflict_resolution_attempts`, cleared node claims) stay
|
|
654
|
+
* invisible by returning null.
|
|
655
|
+
*/
|
|
656
|
+
function workflowStateMessage(data: Record<string, unknown>): string | null {
|
|
657
|
+
if (typeof data.run_id === "string") {
|
|
658
|
+
if (typeof data.predecessor === "string") {
|
|
659
|
+
return `run ${data.run_id} · child of ${data.predecessor}`;
|
|
660
|
+
}
|
|
661
|
+
let message = `run ${data.run_id} claimed`;
|
|
662
|
+
if (typeof data.stage === "string") message += ` · stage ${data.stage}`;
|
|
663
|
+
if (typeof data.mode === "string") message += ` · ${data.mode}`;
|
|
664
|
+
return message;
|
|
665
|
+
}
|
|
666
|
+
if (typeof data.mode === "string") return `${data.mode} mode`;
|
|
667
|
+
// Key-presence check (not value truthiness): an explicit null means "cleared" here.
|
|
668
|
+
if (Object.hasOwn(data, "active_objective")) {
|
|
669
|
+
if (typeof data.active_objective === "string") {
|
|
670
|
+
return `objective ${data.active_objective} activated`;
|
|
671
|
+
}
|
|
672
|
+
if (data.active_objective === null) return "objective cleared";
|
|
673
|
+
}
|
|
674
|
+
const planRef = asRecord(data.active_plan_ref);
|
|
675
|
+
if (planRef !== null && typeof planRef.pr_id === "string") {
|
|
676
|
+
return `plan ${planRef.pr_id} linked`;
|
|
677
|
+
}
|
|
678
|
+
const claim = asRecord(data.objective_node_claim);
|
|
679
|
+
if (claim !== null && typeof claim.objective === "string" && typeof claim.node === "string") {
|
|
680
|
+
// A cleared claim (null) stays invisible — only the SET claim is a marker-worthy moment.
|
|
681
|
+
return `node ${claim.node} claimed for objective ${claim.objective}`;
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* `perk:workflow-state` marker: the first matching headline field renders (precedence:
|
|
688
|
+
* run claim/fork → mode flip → objective set/clear → plan link → node claim); bookkeeping-only
|
|
689
|
+
* deltas stay invisible. Expanded: the collapsed line + the raw delta as one dim JSON line
|
|
690
|
+
* (/learn-grade debuggability).
|
|
691
|
+
*/
|
|
692
|
+
export const workflowStateEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
693
|
+
const data = asRecord(entry.data);
|
|
694
|
+
if (data === null) return undefined;
|
|
695
|
+
const message = workflowStateMessage(data);
|
|
696
|
+
if (message === null) return undefined;
|
|
697
|
+
return {
|
|
698
|
+
render(width) {
|
|
699
|
+
const collapsed = markerLine("workflow", message, theme, width);
|
|
700
|
+
if (!options.expanded) return [collapsed];
|
|
701
|
+
return [collapsed, truncateToWidth(theme.fg("dim", JSON.stringify(data)), width)];
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* `perk:objective-budget` marker (`{ objective_id, activated_at }` activation entries).
|
|
708
|
+
* Collapsed: `perk: objective — <id> budget tracking started`. Expanded: + a dim activation
|
|
709
|
+
* timestamp line.
|
|
710
|
+
*/
|
|
711
|
+
export const objectiveBudgetEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
712
|
+
const data = asRecord(entry.data);
|
|
713
|
+
if (data === null) return undefined;
|
|
714
|
+
const objectiveId = data.objective_id;
|
|
715
|
+
const activatedAt = data.activated_at;
|
|
716
|
+
if (typeof objectiveId !== "string" || typeof activatedAt !== "string") return undefined;
|
|
717
|
+
return {
|
|
718
|
+
render(width) {
|
|
719
|
+
const collapsed = markerLine(
|
|
720
|
+
"objective",
|
|
721
|
+
`${objectiveId} budget tracking started`,
|
|
722
|
+
theme,
|
|
723
|
+
width,
|
|
724
|
+
);
|
|
725
|
+
if (!options.expanded) return [collapsed];
|
|
726
|
+
return [collapsed, truncateToWidth(theme.fg("dim", `activated at ${activatedAt}`), width)];
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* `btw-thread-entry` marker. Collapsed: `perk: btw — <first line of question>` (dim). Expanded:
|
|
733
|
+
* the question line accented + the answer split on newlines, each line dim + width-truncated —
|
|
734
|
+
* no wrapping (a bounded choice: the marker is a durable pointer; the `/btw` overlay remains the
|
|
735
|
+
* full reader).
|
|
736
|
+
*/
|
|
737
|
+
export const btwThreadEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
738
|
+
const data = asRecord(entry.data);
|
|
739
|
+
if (data === null) return undefined;
|
|
740
|
+
const question = data.question;
|
|
741
|
+
const answer = data.answer;
|
|
742
|
+
if (typeof question !== "string" || typeof answer !== "string") return undefined;
|
|
743
|
+
const headline = question.split("\n", 1)[0] ?? "";
|
|
744
|
+
return {
|
|
745
|
+
render(width) {
|
|
746
|
+
if (!options.expanded) return [markerLine("btw", headline, theme, width)];
|
|
747
|
+
return [
|
|
748
|
+
truncateToWidth(theme.fg("accent", `perk: btw — ${headline}`), width),
|
|
749
|
+
...answer.split("\n").map((line) => truncateToWidth(theme.fg("dim", line), width)),
|
|
750
|
+
];
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
/** `btw-thread-reset` marker: `perk: btw — thread reset` (+ a dim ISO timestamp line expanded). */
|
|
756
|
+
export const btwThreadResetEntryRenderer: TranscriptRenderer = (entry, options, theme) => {
|
|
757
|
+
const data = asRecord(entry.data);
|
|
758
|
+
if (data === null) return undefined;
|
|
759
|
+
const timestamp = data.timestamp;
|
|
760
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) return undefined;
|
|
761
|
+
return {
|
|
762
|
+
render(width) {
|
|
763
|
+
const collapsed = markerLine("btw", "thread reset", theme, width);
|
|
764
|
+
if (!options.expanded) return [collapsed];
|
|
765
|
+
return [
|
|
766
|
+
collapsed,
|
|
767
|
+
truncateToWidth(theme.fg("dim", new Date(timestamp).toISOString()), width),
|
|
768
|
+
];
|
|
769
|
+
},
|
|
770
|
+
};
|
|
771
|
+
};
|
|
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
|
+
}
|
|
@@ -57,6 +57,11 @@ type OverlayHandleLike = {
|
|
|
57
57
|
|
|
58
58
|
import type { ToolGating } from "../../substrate/toolGating.ts";
|
|
59
59
|
import { report } from "../../surfaces/report.ts";
|
|
60
|
+
import {
|
|
61
|
+
btwThreadEntryRenderer,
|
|
62
|
+
btwThreadResetEntryRenderer,
|
|
63
|
+
registerTranscriptRenderer,
|
|
64
|
+
} from "../../surfaces/surfaces.ts";
|
|
60
65
|
import {
|
|
61
66
|
extractEventAssistantText,
|
|
62
67
|
extractText,
|
|
@@ -304,6 +309,11 @@ class BtwOverlay extends Container implements Focusable {
|
|
|
304
309
|
}
|
|
305
310
|
|
|
306
311
|
export function registerBtw(pi: ExtensionAPI, gating: ToolGating): void {
|
|
312
|
+
// Transcript markers for the btw thread entries (audit §2.3): renderer bodies in surfaces.ts,
|
|
313
|
+
// registration = wiring, feature-detect inside the seam (pre-0.80.4 hosts stay inert).
|
|
314
|
+
registerTranscriptRenderer(pi, BTW_ENTRY_TYPE, btwThreadEntryRenderer);
|
|
315
|
+
registerTranscriptRenderer(pi, BTW_RESET_TYPE, btwThreadResetEntryRenderer);
|
|
316
|
+
|
|
307
317
|
let thread: BtwDetails[] = [];
|
|
308
318
|
let pendingQuestion: string | null = null;
|
|
309
319
|
let pendingAnswer = "";
|
|
@@ -80,25 +80,38 @@ export interface CapResult {
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* UTF-8-byte-safe truncation (subagent's byte-trim loop). Under cap ⇒ unchanged, truncated:false.
|
|
83
|
-
* When truncated,
|
|
84
|
-
*
|
|
83
|
+
* When truncated, a notice points at the scratch file holding the full result; the notice sits at
|
|
84
|
+
* the cut edge (appended in head mode, prepended in tail mode) so a top-down reader immediately
|
|
85
|
+
* knows which side is missing.
|
|
86
|
+
*
|
|
87
|
+
* `keep` mirrors the SDK's truncateHead/truncateTail guidance: "head" (default) for
|
|
88
|
+
* model-authored summaries/handoffs where the beginning matters; "tail" for command/CI logs where
|
|
89
|
+
* failure summaries live at the end. Deliberately perk's own byte-only util (not the SDK's
|
|
90
|
+
* line-count-aware `truncateTail`): `CapResult`'s byte fields and the scratch-pointing notice are
|
|
91
|
+
* load-bearing in `ChildStructured`/`CiCheckResult`. Pure.
|
|
85
92
|
*/
|
|
86
93
|
export function capForModel(
|
|
87
94
|
text: string,
|
|
88
95
|
cap: number = DEFAULT_MODEL_VISIBLE_CAP,
|
|
89
96
|
scratchPath: string | null = null,
|
|
97
|
+
keep: "head" | "tail" = "head",
|
|
90
98
|
): CapResult {
|
|
91
99
|
const bytesTotal = Buffer.byteLength(text, "utf8");
|
|
92
100
|
if (bytesTotal <= cap) {
|
|
93
101
|
return { shown: text, bytesTotal, bytesShown: bytesTotal, truncated: false };
|
|
94
102
|
}
|
|
95
|
-
let trimmed = text.slice(0, cap);
|
|
96
|
-
while (Buffer.byteLength(trimmed, "utf8") > cap)
|
|
103
|
+
let trimmed = keep === "head" ? text.slice(0, cap) : text.slice(-cap);
|
|
104
|
+
while (Buffer.byteLength(trimmed, "utf8") > cap) {
|
|
105
|
+
trimmed = keep === "head" ? trimmed.slice(0, -1) : trimmed.slice(1);
|
|
106
|
+
}
|
|
97
107
|
const bytesShown = Buffer.byteLength(trimmed, "utf8");
|
|
98
108
|
const omitted = bytesTotal - bytesShown;
|
|
99
109
|
const where = scratchPath ? ` Full output preserved at ${scratchPath}.` : "";
|
|
100
|
-
const
|
|
101
|
-
|
|
110
|
+
const shown =
|
|
111
|
+
keep === "head"
|
|
112
|
+
? `${trimmed}\n\n[Output truncated: ${omitted} bytes omitted.${where}]`
|
|
113
|
+
: `[Output truncated: ${omitted} bytes omitted.${where}]\n\n${trimmed}`;
|
|
114
|
+
return { shown, bytesTotal, bytesShown, truncated: true };
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
/**
|