pi-goal-list-loop-audit 0.29.15 → 0.29.17
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/extensions/loops/goal.ts +115 -14
- package/extensions/model-picker.ts +208 -0
- package/package.json +1 -1
package/extensions/loops/goal.ts
CHANGED
|
@@ -152,6 +152,11 @@ import {
|
|
|
152
152
|
SettingsMenuComponent,
|
|
153
153
|
type SettingsRow,
|
|
154
154
|
} from "../settings-menu.js";
|
|
155
|
+
import {
|
|
156
|
+
buildModelPickItems,
|
|
157
|
+
ModelPickerComponent,
|
|
158
|
+
type ModelPickItem,
|
|
159
|
+
} from "../model-picker.js";
|
|
155
160
|
import {
|
|
156
161
|
applyMeasurement,
|
|
157
162
|
applyMetriclessTick,
|
|
@@ -424,6 +429,15 @@ const countedLoopTokenMessages = new Set<string>();
|
|
|
424
429
|
|
|
425
430
|
// Heartbeat self-watchdog state: liveness is the loop's own job.
|
|
426
431
|
let lastActivityAt = Date.now();
|
|
432
|
+
// v0.29.16: zombie-run watchdog clock — updated ONLY by genuine pi stream
|
|
433
|
+
// events (message_update / tool_call / agent_start / turn_start /
|
|
434
|
+
// agent_end), never by heartbeat-internal bookkeeping. A session pi
|
|
435
|
+
// reports as BUSY with zero stream events for N min is a hung provider
|
|
436
|
+
// stream (pi has no read timeout): continuations queue into the void and
|
|
437
|
+
// the busy flag conceals the wedge from every other watchdog (field:
|
|
438
|
+
// hellhunter + hegemon 2026-07-30, MiniMax streams died silently).
|
|
439
|
+
let lastStreamActivityAt = Date.now();
|
|
440
|
+
let lastZombieAlertAt = 0;
|
|
427
441
|
let lastWedgeAlertAt = 0;
|
|
428
442
|
let heartbeatNudges = 0;
|
|
429
443
|
// v0.28.4 (P3): skip nudge accounting for the first agent_end turns after a
|
|
@@ -445,6 +459,9 @@ let carryoverResolved = true;
|
|
|
445
459
|
let completionAuditInFlight = false;
|
|
446
460
|
let heartbeatTimer: NodeJS.Timeout | null = null;
|
|
447
461
|
|
|
462
|
+
const ZOMBIE_RUN_SILENT_MS = 20 * 60_000;
|
|
463
|
+
const ZOMBIE_RUN_ALERT_THROTTLE_MS = 10 * 60_000;
|
|
464
|
+
|
|
448
465
|
function noteActivity(real = false): void {
|
|
449
466
|
lastActivityAt = Date.now();
|
|
450
467
|
if (real) consecutiveStalls = 0;
|
|
@@ -653,6 +670,20 @@ function heartbeatTick(): void {
|
|
|
653
670
|
// escalation would PAUSE the goal — silently cancelling the
|
|
654
671
|
// interruptedAt → hold-on-restart promise the footer shows.
|
|
655
672
|
if (probeExtensionApiStale()) { goStaleTerminal(ctx, "heartbeat probe"); return; }
|
|
673
|
+
// v0.29.16: zombie-run watchdog. pi reports BUSY (a run is "active") but
|
|
674
|
+
// zero stream events for 20 min = the provider stream hung silently —
|
|
675
|
+
// queued continuations can't land, and every other watchdog stays quiet
|
|
676
|
+
// because busy≠wedged. Detection + loud guidance only: aborting a turn
|
|
677
|
+
// is the user's call (consent line); Esc frees the queue and the
|
|
678
|
+
// heartbeat refires the goal/loop by itself.
|
|
679
|
+
const streamSilentMs = Date.now() - lastStreamActivityAt;
|
|
680
|
+
if (isSupervising() && !idle && streamSilentMs >= ZOMBIE_RUN_SILENT_MS && Date.now() - lastZombieAlertAt >= ZOMBIE_RUN_ALERT_THROTTLE_MS) {
|
|
681
|
+
lastZombieAlertAt = Date.now();
|
|
682
|
+
appendLedger(ctx.cwd, "zombie_run_suspected", { streamSilentMs, pending });
|
|
683
|
+
ctx.ui.notify(`glla: the session has been BUSY with zero stream activity for ${Math.round(streamSilentMs / 60000)} min — the provider stream is hung (pi never times it out; queued continuations can't land). Press Esc to abort the zombie turn — the goal/loop refires itself.`, "warning");
|
|
684
|
+
notifyExternal(ctx, `glla: zombie run suspected (${Math.round(streamSilentMs / 60000)} min busy-silent) — press Esc to abort.`);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
656
687
|
// v0.29.1: stranded-audit recovery. A goal left in "auditing" with NO
|
|
657
688
|
// in-flight audit means the auditor's result never landed (wedged queue
|
|
658
689
|
// ate the tool result; compaction/restart mid-audit). Field-observed in
|
|
@@ -3917,19 +3948,37 @@ function getSessionThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high
|
|
|
3917
3948
|
function resolveAuditorModel(ctx: ExtensionContext, ref?: string): { model: any; error?: string; via?: string } {
|
|
3918
3949
|
if (ref && ref.trim()) {
|
|
3919
3950
|
const trimmed = ref.trim();
|
|
3951
|
+
// v0.29.17: an unavailable configured model (unknown id, or a provider
|
|
3952
|
+
// with no configured auth) falls back LOUDLY to the session model —
|
|
3953
|
+
// user request: "fall back to the session if unavailable". The v0.9.12
|
|
3954
|
+
// no-SILENT-substitution law stands: the fallback notifies + ledgers.
|
|
3955
|
+
// (Quota-exhausted keys stay on the quota-retry path — the model IS
|
|
3956
|
+
// available there; the key's window is the failure, not the model.)
|
|
3957
|
+
const fail = (reason: string) => {
|
|
3958
|
+
const sessionModel = ctx.model as any;
|
|
3959
|
+
if (sessionModel) {
|
|
3960
|
+
appendLedger(ctx.cwd, "auditor_model_fallback", { configured: trimmed, reason });
|
|
3961
|
+
ctx.ui.notify(`Auditor model "${trimmed}" is unavailable (${reason}) — falling back to the session model. Fix via /glla → Auditor model.`, "warning");
|
|
3962
|
+
return { model: sessionModel, via: "session-fallback" };
|
|
3963
|
+
}
|
|
3964
|
+
return { model: undefined, error: `${reason}: ${trimmed}` };
|
|
3965
|
+
};
|
|
3920
3966
|
const slash = trimmed.indexOf("/");
|
|
3921
3967
|
if (slash > 0) {
|
|
3922
3968
|
const provider = trimmed.slice(0, slash);
|
|
3923
3969
|
const id = trimmed.slice(slash + 1);
|
|
3924
3970
|
const model = ctx.modelRegistry.find(provider, id);
|
|
3925
|
-
|
|
3971
|
+
if (!model) return fail("model not found");
|
|
3972
|
+
if (!ctx.modelRegistry.hasConfiguredAuth(model)) return fail(`no configured auth for ${provider}`);
|
|
3973
|
+
return { model, via: "setting" };
|
|
3926
3974
|
}
|
|
3927
3975
|
const matches = ctx.modelRegistry.getAvailable().filter((m: any) => m.id === trimmed || m.name === trimmed);
|
|
3928
|
-
|
|
3976
|
+
if (matches[0]) return { model: matches[0], via: "setting" };
|
|
3977
|
+
return fail("no available model matching");
|
|
3929
3978
|
}
|
|
3930
3979
|
const sessionModel = ctx.model as any;
|
|
3931
3980
|
if (sessionModel) return { model: sessionModel, via: "session" };
|
|
3932
|
-
return { model: undefined, error: "no session model and no auditorModel configured — set one with /glla model
|
|
3981
|
+
return { model: undefined, error: "no session model and no auditorModel configured — set one with /glla → Auditor model" };
|
|
3933
3982
|
}
|
|
3934
3983
|
|
|
3935
3984
|
// (v0.9.12) The auto-fallback apparatus was REMOVED: no tier ranking, no
|
|
@@ -4006,6 +4055,43 @@ async function promptSettingsMenu(
|
|
|
4006
4055
|
*/
|
|
4007
4056
|
// v0.28.7 (T4): exported for the behavioral settings-editor tests
|
|
4008
4057
|
// (tests/settings-editors.test.ts drives each editor class end-to-end).
|
|
4058
|
+
/**
|
|
4059
|
+
* v0.29.17: /model-style fuzzy picker for model-valued settings. Builds the
|
|
4060
|
+
* item list from the registry (configured-auth providers only — a pick
|
|
4061
|
+
* from this list can never be a dead provider) and hosts the picker via
|
|
4062
|
+
* ctx.ui.custom. Falls back to the typed input when the runtime has no
|
|
4063
|
+
* custom shard (headless) — typing stays the emergency hatch there.
|
|
4064
|
+
* Returns { kind: "session" } to clear the override, { kind: "ref" } with
|
|
4065
|
+
* provider/id, or undefined for cancel.
|
|
4066
|
+
*/
|
|
4067
|
+
async function promptModelRef(
|
|
4068
|
+
ctx: ExtensionContext,
|
|
4069
|
+
title: string,
|
|
4070
|
+
emptyLabel: string,
|
|
4071
|
+
): Promise<{ kind: "session" } | { kind: "ref"; ref: string } | undefined> {
|
|
4072
|
+
if (typeof (ctx.ui as { custom?: unknown }).custom !== "function" || !ctx.modelRegistry) {
|
|
4073
|
+
const v = await ctx.ui.input(title, "provider/model-id — empty keeps the default");
|
|
4074
|
+
if (v === undefined) return undefined;
|
|
4075
|
+
return v.trim() ? { kind: "ref", ref: v.trim() } : { kind: "session" };
|
|
4076
|
+
}
|
|
4077
|
+
const sessionModel = ctx.model as any;
|
|
4078
|
+
const sessionLabel = sessionModel ? `${sessionModel.provider}/${sessionModel.id}` : "pi session model";
|
|
4079
|
+
const models = ctx.modelRegistry
|
|
4080
|
+
.getAvailable()
|
|
4081
|
+
.filter((m: any) => ctx.modelRegistry.hasConfiguredAuth(m));
|
|
4082
|
+
const items = buildModelPickItems(models, sessionLabel);
|
|
4083
|
+
const pick = await ctx.ui.custom<ModelPickItem | undefined>((tui, theme, keybindings, done) => {
|
|
4084
|
+
return new ModelPickerComponent({ title, items }, () => tui.requestRender(), theme, keybindings, done);
|
|
4085
|
+
});
|
|
4086
|
+
if (!pick) return undefined;
|
|
4087
|
+
if (pick.kind === "session") return { kind: "session" };
|
|
4088
|
+
if (pick.kind === "model" && pick.ref) return { kind: "ref", ref: pick.ref };
|
|
4089
|
+
// manual escape hatch — typed provider/model, validated like before
|
|
4090
|
+
const v = await ctx.ui.input(title, emptyLabel);
|
|
4091
|
+
if (v === undefined) return undefined;
|
|
4092
|
+
return v.trim() ? { kind: "ref", ref: v.trim() } : { kind: "session" };
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4009
4095
|
export async function handleSettingChoice(id: string, ctx: ExtensionContext): Promise<void> {
|
|
4010
4096
|
switch (id) {
|
|
4011
4097
|
case "autoResume": {
|
|
@@ -4045,8 +4131,10 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
|
|
|
4045
4131
|
return;
|
|
4046
4132
|
}
|
|
4047
4133
|
case "auditorModel": {
|
|
4048
|
-
const
|
|
4049
|
-
if (
|
|
4134
|
+
const pick = await promptModelRef(ctx, "Auditor model override", "provider/model-id — empty keeps the pi session model");
|
|
4135
|
+
if (pick === undefined) return;
|
|
4136
|
+
saveSettings("global", ctx.cwd, { auditorModel: pick.kind === "session" ? undefined : pick.ref });
|
|
4137
|
+
if (pick.kind === "session") ctx.ui.notify("Auditor model override cleared — the auditor follows the pi session model.", "info");
|
|
4050
4138
|
return;
|
|
4051
4139
|
}
|
|
4052
4140
|
case "auditorThinkingLevel": {
|
|
@@ -4151,15 +4239,14 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
|
|
|
4151
4239
|
case "subagentModelOverrides.Plan":
|
|
4152
4240
|
case "subagentModelOverrides.general-purpose": {
|
|
4153
4241
|
const agentType = id.slice("subagentModelOverrides.".length);
|
|
4154
|
-
const
|
|
4155
|
-
if (
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
}
|
|
4242
|
+
const pick = await promptModelRef(ctx, `Model pin for ${agentType} subagents`, "provider/model-id e.g. minimax/MiniMax-M3 — always wins over strategy; empty = follow strategy");
|
|
4243
|
+
if (pick === undefined) return;
|
|
4244
|
+
const current = loadSettings(ctx.cwd).subagentModelOverrides ?? {};
|
|
4245
|
+
const next = { ...current };
|
|
4246
|
+
if (pick.kind === "ref") next[agentType] = pick.ref;
|
|
4247
|
+
else delete next[agentType];
|
|
4248
|
+
saveSettings("global", ctx.cwd, { subagentModelOverrides: Object.keys(next).length > 0 ? next : undefined });
|
|
4249
|
+
ctx.ui.notify(`${agentType} model pin saved — applies to NEW pi sessions.`, "info");
|
|
4163
4250
|
return;
|
|
4164
4251
|
}
|
|
4165
4252
|
case "subagentResolved":
|
|
@@ -5467,6 +5554,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5467
5554
|
// continuation loop.
|
|
5468
5555
|
if (isForeignCtx(ctx)) return;
|
|
5469
5556
|
noteActivity(true);
|
|
5557
|
+
lastStreamActivityAt = Date.now();
|
|
5470
5558
|
// v0.27.2: folded-in length-continue (standalone pi-length-continue is
|
|
5471
5559
|
// deprecated). A response cut by the per-response output cap is NOT a
|
|
5472
5560
|
// completed turn (no telemetry), NOT a stall (no no-tool nudge), and
|
|
@@ -5734,9 +5822,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
5734
5822
|
pi.on("tool_call", () => {
|
|
5735
5823
|
toolCallsThisTurn++;
|
|
5736
5824
|
noteActivity(true);
|
|
5825
|
+
lastStreamActivityAt = Date.now();
|
|
5737
5826
|
// v0.24.0: count loop-iteration tool calls (narration-only detection).
|
|
5738
5827
|
if (isLoopActive()) {
|
|
5739
5828
|
state.loop!.toolsThisTurn = (state.loop!.toolsThisTurn ?? 0) + 1;
|
|
5740
5829
|
}
|
|
5741
5830
|
});
|
|
5831
|
+
|
|
5832
|
+
// v0.29.16: stream liveness for the zombie-run watchdog — deltas, run
|
|
5833
|
+
// starts, and turn starts all prove the provider stream is alive.
|
|
5834
|
+
pi.on("message_update", () => {
|
|
5835
|
+
lastStreamActivityAt = Date.now();
|
|
5836
|
+
});
|
|
5837
|
+
pi.on("agent_start", () => {
|
|
5838
|
+
lastStreamActivityAt = Date.now();
|
|
5839
|
+
});
|
|
5840
|
+
pi.on("turn_start", () => {
|
|
5841
|
+
lastStreamActivityAt = Date.now();
|
|
5842
|
+
});
|
|
5742
5843
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// pi-goal-list-loop-audit — v0.29.17
|
|
2
|
+
// extensions/model-picker.ts
|
|
3
|
+
//
|
|
4
|
+
// A /model-style fuzzy picker for model-valued settings (Auditor model,
|
|
5
|
+
// subagent model pins). Why this exists:
|
|
6
|
+
// • ctx.ui.select renders EVERY option unsorted with no search — a full
|
|
7
|
+
// model registry is hundreds of rows; unusable (field: the auditor
|
|
8
|
+
// model was left on a quota-dead openrouter key partly because fixing
|
|
9
|
+
// it meant hand-typing provider/model into a bare input).
|
|
10
|
+
// • pi's own /model dialog (ModelSelectorComponent) needs ModelRuntime
|
|
11
|
+
// and SettingsManager internals that extensions never receive.
|
|
12
|
+
// So we rebuild the same interaction shape — a search line with a
|
|
13
|
+
// fuzzy-filtered list — from pi-tui primitives (fuzzyFilter) over
|
|
14
|
+
// ctx.modelRegistry, hosted via ctx.ui.custom, exactly like the v0.28.0
|
|
15
|
+
// settings table. Unit-testable via synthetic handleInput calls.
|
|
16
|
+
//
|
|
17
|
+
// Item order: "session model" (clear override) first, then every
|
|
18
|
+
// configured-auth model sorted by provider/id, then "type manually…" last.
|
|
19
|
+
|
|
20
|
+
import { fuzzyFilter, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
21
|
+
import type { SettingsMenuTheme, KeybindingsManagerLike } from "./settings-menu.ts";
|
|
22
|
+
|
|
23
|
+
export type ModelPickKind = "session" | "model" | "manual";
|
|
24
|
+
|
|
25
|
+
export interface ModelPickItem {
|
|
26
|
+
kind: ModelPickKind;
|
|
27
|
+
/** provider/model-id for kind === "model". */
|
|
28
|
+
ref?: string;
|
|
29
|
+
/** Row label shown in the list. */
|
|
30
|
+
label: string;
|
|
31
|
+
/** Text the fuzzy filter matches against. */
|
|
32
|
+
searchText: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RegistryModelLike {
|
|
36
|
+
provider: string;
|
|
37
|
+
id: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Build the picker's static item list from registry models (already
|
|
42
|
+
* filtered to configured-auth providers by the caller). Session row first,
|
|
43
|
+
* manual-entry row last; models sorted by provider then id. */
|
|
44
|
+
export function buildModelPickItems(models: RegistryModelLike[], sessionLabel: string): ModelPickItem[] {
|
|
45
|
+
const sorted = [...models].sort((a, b) =>
|
|
46
|
+
a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider),
|
|
47
|
+
);
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
kind: "session",
|
|
51
|
+
label: `session model (${sessionLabel}) — clear the override`,
|
|
52
|
+
searchText: "session model default clear override follow",
|
|
53
|
+
},
|
|
54
|
+
...sorted.map((m) => {
|
|
55
|
+
const ref = `${m.provider}/${m.id}`;
|
|
56
|
+
return {
|
|
57
|
+
kind: "model" as const,
|
|
58
|
+
ref,
|
|
59
|
+
label: m.name && m.name !== m.id ? `${ref} — ${m.name}` : ref,
|
|
60
|
+
searchText: `${ref} ${m.name ?? ""}`,
|
|
61
|
+
};
|
|
62
|
+
}),
|
|
63
|
+
{
|
|
64
|
+
kind: "manual",
|
|
65
|
+
label: "type provider/model manually…",
|
|
66
|
+
searchText: "manual type custom provider model id",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ModelPickerFactoryDeps {
|
|
72
|
+
title: string;
|
|
73
|
+
items: ModelPickItem[];
|
|
74
|
+
/** Cap on visible list rows (window scrolls with the selection). */
|
|
75
|
+
maxVisibleRows?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class ModelPickerComponent {
|
|
79
|
+
private readonly title: string;
|
|
80
|
+
private readonly items: ModelPickItem[];
|
|
81
|
+
private readonly maxRows: number;
|
|
82
|
+
private readonly requestRender: () => void;
|
|
83
|
+
private readonly theme: SettingsMenuTheme;
|
|
84
|
+
private readonly keybindings: KeybindingsManagerLike;
|
|
85
|
+
private readonly done: (item: ModelPickItem | undefined) => void;
|
|
86
|
+
|
|
87
|
+
private query = "";
|
|
88
|
+
private selectedIdx = 0;
|
|
89
|
+
|
|
90
|
+
constructor(
|
|
91
|
+
deps: ModelPickerFactoryDeps,
|
|
92
|
+
requestRender: () => void,
|
|
93
|
+
theme: SettingsMenuTheme,
|
|
94
|
+
keybindings: KeybindingsManagerLike,
|
|
95
|
+
done: (item: ModelPickItem | undefined) => void,
|
|
96
|
+
) {
|
|
97
|
+
this.title = deps.title;
|
|
98
|
+
this.items = deps.items;
|
|
99
|
+
this.maxRows = deps.maxVisibleRows ?? 12;
|
|
100
|
+
this.requestRender = requestRender;
|
|
101
|
+
this.theme = theme;
|
|
102
|
+
this.keybindings = keybindings;
|
|
103
|
+
this.done = done;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Current search query. Exposed for tests. */
|
|
107
|
+
getQuery(): string {
|
|
108
|
+
return this.query;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Index into the filtered list. Exposed for tests. */
|
|
112
|
+
getSelectedIdx(): number {
|
|
113
|
+
return this.selectedIdx;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Filtered items for the current query. Exposed for tests. */
|
|
117
|
+
filteredItems(): ModelPickItem[] {
|
|
118
|
+
if (!this.query.trim()) return this.items;
|
|
119
|
+
return fuzzyFilter(this.items, this.query.trim(), (it) => it.searchText);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private refresh(): void {
|
|
123
|
+
this.requestRender();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private move(delta: number): void {
|
|
127
|
+
const n = this.filteredItems().length;
|
|
128
|
+
if (n === 0) return;
|
|
129
|
+
this.selectedIdx = ((this.selectedIdx + delta) % n + n) % n;
|
|
130
|
+
this.refresh();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
render(width: number): string[] {
|
|
134
|
+
const w = Math.max(20, width - 2);
|
|
135
|
+
const lines: string[] = [];
|
|
136
|
+
lines.push(this.theme.fg("accent", this.theme.bold(this.title)));
|
|
137
|
+
lines.push("");
|
|
138
|
+
const searchLine = `search: ${this.query}`;
|
|
139
|
+
lines.push(this.theme.fg("muted", truncateToWidth(searchLine, w, "…") + "▏"));
|
|
140
|
+
lines.push("");
|
|
141
|
+
const filtered = this.filteredItems();
|
|
142
|
+
if (filtered.length === 0) {
|
|
143
|
+
lines.push(this.theme.fg("warning", " no matches — keep typing, or Esc to cancel"));
|
|
144
|
+
} else {
|
|
145
|
+
const sel = Math.min(this.selectedIdx, filtered.length - 1);
|
|
146
|
+
const half = Math.floor(this.maxRows / 2);
|
|
147
|
+
const start = Math.max(0, Math.min(sel - half, filtered.length - this.maxRows));
|
|
148
|
+
const window = filtered.slice(start, start + this.maxRows);
|
|
149
|
+
if (start > 0) lines.push(this.theme.fg("dim", ` ↑ ${start} more`));
|
|
150
|
+
for (let i = 0; i < window.length; i++) {
|
|
151
|
+
const idx = start + i;
|
|
152
|
+
const it = window[i]!;
|
|
153
|
+
const row = truncateToWidth(it.label, w - 2, "…");
|
|
154
|
+
lines.push(idx === sel ? this.theme.fg("accent", `→ ${row}`) : ` ${row}`);
|
|
155
|
+
}
|
|
156
|
+
const remaining = filtered.length - (start + window.length);
|
|
157
|
+
if (remaining > 0) lines.push(this.theme.fg("dim", ` ↓ ${remaining} more`));
|
|
158
|
+
}
|
|
159
|
+
lines.push("");
|
|
160
|
+
lines.push(this.theme.fg("dim", "type to filter · ↑/↓ move · enter select · esc cancel"));
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
handleInput(data: string): void {
|
|
165
|
+
if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
166
|
+
const it = this.filteredItems()[this.selectedIdx];
|
|
167
|
+
this.done(it);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (this.keybindings.matches(data, "tui.select.cancel") || data === "\x1b") {
|
|
171
|
+
this.done(undefined);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
175
|
+
this.move(-1);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (this.keybindings.matches(data, "tui.select.down")) {
|
|
179
|
+
this.move(+1);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (data === "\x7f" || data === "\b") {
|
|
183
|
+
if (this.query.length > 0) {
|
|
184
|
+
this.query = this.query.slice(0, -1);
|
|
185
|
+
this.selectedIdx = 0;
|
|
186
|
+
this.refresh();
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Printable input (single keystrokes and pasted runs alike). Ignore
|
|
191
|
+
// escape/CSI sequences — they start with \x1b and were handled above.
|
|
192
|
+
if (!data.startsWith("\x1b")) {
|
|
193
|
+
const printable = [...data].filter((ch) => ch >= " ").join("");
|
|
194
|
+
if (printable.length > 0) {
|
|
195
|
+
this.query += printable;
|
|
196
|
+
this.selectedIdx = 0;
|
|
197
|
+
this.refresh();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
invalidate(): void {
|
|
203
|
+
// Stateless beyond the query/selection — nothing to clear.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Re-export for callers that only need the width helper's type signature.
|
|
208
|
+
export { visibleWidth };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.17",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|