pi-warden 1.4.0 → 1.5.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/CHANGELOG.md +6 -0
- package/dist/config.d.ts +6 -3
- package/dist/config.js +12 -7
- package/dist/panel.d.ts +4 -2
- package/dist/panel.js +161 -48
- package/dist/widget.d.ts +4 -2
- package/dist/widget.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ How to keep this current: add the entry in the same pull request as the change,
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 1.5.0
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The config panel has selectors for mode, judgment backend, widget placement and bar mode, conscience skill mode, and recall tool. Left/right wraps through supported values; Enter saves the chosen field and Esc cancels. Invalid values and custom backends remain intact until a choice is made, and other staged edits keep their existing `s` save behavior. ([Configuration](docs/configuration.md#user-config))
|
|
14
|
+
|
|
9
15
|
## 1.4.0
|
|
10
16
|
|
|
11
17
|
### Added
|
package/dist/config.d.ts
CHANGED
|
@@ -321,9 +321,11 @@ export interface SubagentConfig {
|
|
|
321
321
|
/** At most one wake per this window, so several children finishing together cost one interruption. */
|
|
322
322
|
cooldownMs: number;
|
|
323
323
|
}
|
|
324
|
-
export
|
|
324
|
+
export declare const RECALL_TOOLS: readonly ["auto", "rg", "ag", "ugrep", "git-grep", "grep", "select-string", "findstr", "none"];
|
|
325
|
+
export type RecallTool = typeof RECALL_TOOLS[number];
|
|
325
326
|
export declare function isRecallTool(value: unknown): value is RecallTool;
|
|
326
|
-
export
|
|
327
|
+
export declare const WARDEN_MODES: readonly ["steer", "confirm", "advise"];
|
|
328
|
+
export type WardenMode = typeof WARDEN_MODES[number];
|
|
327
329
|
export interface WasteConfig {
|
|
328
330
|
/** Master switch for the call-waste notes and the session tip. */
|
|
329
331
|
enabled: boolean;
|
|
@@ -369,7 +371,8 @@ export interface LearningConfig {
|
|
|
369
371
|
/** Days to keep records of calls that were not held; they repeat their task text across a turn and are the bulk of the file. 0 disables this pruning. */
|
|
370
372
|
allowedRetentionDays: number;
|
|
371
373
|
}
|
|
372
|
-
export
|
|
374
|
+
export declare const CONSCIENCE_SKILL_MODES: readonly ["off", "recommend", "load"];
|
|
375
|
+
export type ConscienceSkillMode = typeof CONSCIENCE_SKILL_MODES[number];
|
|
373
376
|
export interface ConscienceSkillConfig {
|
|
374
377
|
/** off: no automatic skill selection; recommend: name a skill and ask the agent to load it; load: supply instructions directly. */
|
|
375
378
|
mode: ConscienceSkillMode;
|
package/dist/config.js
CHANGED
|
@@ -4,7 +4,7 @@ import { writeFileAtomicSync } from "./atomic.js";
|
|
|
4
4
|
import { resolveJudgmentBackend } from "./backend.js";
|
|
5
5
|
import { defaultHostDirs } from "./host-dirs.js";
|
|
6
6
|
import { COMMAND_TOOLS } from "./tools.js";
|
|
7
|
-
import { defaultWidgetConfig } from "./widget.js";
|
|
7
|
+
import { WIDGET_BAR_MODES, WIDGET_PLACEMENTS, defaultWidgetConfig } from "./widget.js";
|
|
8
8
|
// Tests of UI code are proven by running them, not by looking at them.
|
|
9
9
|
export const DEFAULT_UI_FILES = [
|
|
10
10
|
"**/*.{css,scss,sass,less,html,htm,vue,svelte,jsx,tsx,astro,dart}", "**/web/**/*.js", "**/public/**/*.js",
|
|
@@ -21,10 +21,15 @@ export function defaultVisualTools() {
|
|
|
21
21
|
images: ["png", "jpg", "jpeg", "webp"],
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
const RECALL_TOOLS = ["auto", "rg", "ag", "ugrep", "git-grep", "grep", "select-string", "findstr", "none"];
|
|
24
|
+
export const RECALL_TOOLS = ["auto", "rg", "ag", "ugrep", "git-grep", "grep", "select-string", "findstr", "none"];
|
|
25
|
+
function isOption(value, options) {
|
|
26
|
+
return typeof value === "string" && options.includes(value);
|
|
27
|
+
}
|
|
25
28
|
export function isRecallTool(value) {
|
|
26
|
-
return
|
|
29
|
+
return isOption(value, RECALL_TOOLS);
|
|
27
30
|
}
|
|
31
|
+
export const WARDEN_MODES = ["steer", "confirm", "advise"];
|
|
32
|
+
export const CONSCIENCE_SKILL_MODES = ["off", "recommend", "load"];
|
|
28
33
|
export const PACKAGE_NAME = "pi-warden";
|
|
29
34
|
/** Bumped when WardenConfig gains a section; extension.ts checks it so a half-updated module graph is reported, not crashed on. */
|
|
30
35
|
export const CONFIG_SCHEMA = 13;
|
|
@@ -147,7 +152,7 @@ function positiveInteger(value, fallback) {
|
|
|
147
152
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
148
153
|
}
|
|
149
154
|
export function isMode(value) {
|
|
150
|
-
return value
|
|
155
|
+
return isOption(value, WARDEN_MODES);
|
|
151
156
|
}
|
|
152
157
|
/** One config warning for a rule value no kind accepts; the rule still applies, at the level named. */
|
|
153
158
|
function unknownRuleValue(kind, id, key, value, valid, applied) {
|
|
@@ -529,8 +534,8 @@ function applyWidget(base, raw) {
|
|
|
529
534
|
const template = (value, fallback) => (typeof value === "string" && value.trim() ? value : fallback);
|
|
530
535
|
return {
|
|
531
536
|
enabled: boolean(raw.enabled, base.enabled),
|
|
532
|
-
placement: raw.placement
|
|
533
|
-
barMode: raw.barMode
|
|
537
|
+
placement: isOption(raw.placement, WIDGET_PLACEMENTS) ? raw.placement : base.placement,
|
|
538
|
+
barMode: isOption(raw.barMode, WIDGET_BAR_MODES) ? raw.barMode : base.barMode,
|
|
534
539
|
shortcut: typeof raw.shortcut === "string" ? raw.shortcut.trim() : base.shortcut,
|
|
535
540
|
panelWidth: typeof raw.panelWidth === "number" && Number.isSafeInteger(raw.panelWidth) && raw.panelWidth >= 20 ? raw.panelWidth
|
|
536
541
|
: typeof raw.panelWidth === "string" && /^[1-9]\d?%$/.test(raw.panelWidth.trim()) ? raw.panelWidth.trim() : base.panelWidth,
|
|
@@ -713,7 +718,7 @@ function applyConscience(base, raw) {
|
|
|
713
718
|
return base;
|
|
714
719
|
const skillsRaw = isObject(raw.skills) ? raw.skills : undefined;
|
|
715
720
|
const toolsRaw = isObject(raw.tools) ? raw.tools : undefined;
|
|
716
|
-
const mode =
|
|
721
|
+
const mode = isOption(skillsRaw?.mode, CONSCIENCE_SKILL_MODES) ? skillsRaw.mode : base.skills.mode;
|
|
717
722
|
return {
|
|
718
723
|
enabled: boolean(raw.enabled, base.enabled),
|
|
719
724
|
skills: {
|
package/dist/panel.d.ts
CHANGED
|
@@ -75,8 +75,9 @@ export declare class ConfigPanel implements Component {
|
|
|
75
75
|
private cursor;
|
|
76
76
|
private scroll;
|
|
77
77
|
private viewport;
|
|
78
|
-
private
|
|
79
|
-
private
|
|
78
|
+
private keepCursorVisible;
|
|
79
|
+
private draft;
|
|
80
|
+
private notice;
|
|
80
81
|
private readonly edits;
|
|
81
82
|
private readonly entries;
|
|
82
83
|
constructor(config: WardenConfig, theme: ThemeLike, actions: PanelActions, requestRender: () => void, dirs?: HostDirs);
|
|
@@ -88,6 +89,7 @@ export declare class ConfigPanel implements Component {
|
|
|
88
89
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
|
89
90
|
private parseValue;
|
|
90
91
|
save(): void;
|
|
92
|
+
private saveChanges;
|
|
91
93
|
}
|
|
92
94
|
export declare function openConfigPanel(ui: PanelUi, config: WardenConfig, options?: {
|
|
93
95
|
width?: string | number;
|
package/dist/panel.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
-
import {
|
|
2
|
+
import { DECISIONS_BACKENDS } from "pi-typesafe";
|
|
3
|
+
import { resolveJudgmentBackend } from "./backend.js";
|
|
4
|
+
import { CONSCIENCE_SKILL_MODES, RECALL_TOOLS, WARDEN_MODES, applyUserOverrides, defaultConfig, getNestedValue, readUserConfig, setNestedValue, writeUserConfig } from "./config.js";
|
|
3
5
|
import { defaultHostDirs } from "./host-dirs.js";
|
|
4
|
-
import { LEVEL_COLOR, parseVerdictLine, renderSegment } from "./widget.js";
|
|
6
|
+
import { LEVEL_COLOR, WIDGET_BAR_MODES, WIDGET_PLACEMENTS, parseVerdictLine, renderSegment } from "./widget.js";
|
|
5
7
|
/**
|
|
6
8
|
* Sidebar listing the trace newest-first, live-updating while open. It opens without taking keyboard input: the editor
|
|
7
9
|
* keeps working while it is visible. A click inside it (fullscreen mode) focuses it; then ↑/↓/j/k scroll a line,
|
|
@@ -175,13 +177,41 @@ export function openTracePanel(ui, trace, options = {}) {
|
|
|
175
177
|
});
|
|
176
178
|
return { closed, close: () => close(), built: () => built };
|
|
177
179
|
}
|
|
180
|
+
const SELECTOR_OPTIONS = {
|
|
181
|
+
mode: WARDEN_MODES,
|
|
182
|
+
typesafeBackend: Object.keys(DECISIONS_BACKENDS),
|
|
183
|
+
"widget.placement": WIDGET_PLACEMENTS,
|
|
184
|
+
"widget.barMode": WIDGET_BAR_MODES,
|
|
185
|
+
"conscience.skills.mode": CONSCIENCE_SKILL_MODES,
|
|
186
|
+
"context.recallTool": RECALL_TOOLS,
|
|
187
|
+
};
|
|
188
|
+
function isCustomBackend(value) {
|
|
189
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
190
|
+
&& resolveJudgmentBackend(value).backendRefusal === undefined;
|
|
191
|
+
}
|
|
192
|
+
function selectorValue(entry, value) {
|
|
193
|
+
if (typeof value === "string" && entry.options.includes(value))
|
|
194
|
+
return value;
|
|
195
|
+
if (entry.path === "typesafeBackend" && isCustomBackend(value))
|
|
196
|
+
return "custom";
|
|
197
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
|
|
198
|
+
return raw + " (invalid)";
|
|
199
|
+
}
|
|
178
200
|
function flattenConfig(obj, prefix = "") {
|
|
179
201
|
const entries = [];
|
|
180
|
-
const skip = new Set(["action", "stuck", "done", "slop", "security", "rules", "
|
|
202
|
+
const skip = new Set(["action", "stuck", "done", "slop", "security", "rules", "runaway", "notify", "subagent", "backendRefusal"]);
|
|
181
203
|
for (const [key, value] of Object.entries(obj)) {
|
|
182
204
|
if (skip.has(key) && !prefix)
|
|
183
205
|
continue;
|
|
184
206
|
const path = prefix ? `${prefix}.${key}` : key;
|
|
207
|
+
const level = prefix ? prefix.split(".").length : 0;
|
|
208
|
+
const options = SELECTOR_OPTIONS[path];
|
|
209
|
+
if (options) {
|
|
210
|
+
entries.push({ path, label: key, level, value, type: "select", options, editable: true });
|
|
211
|
+
if (path === "typesafeBackend" && isCustomBackend(value))
|
|
212
|
+
entries.push(...flattenConfig(value, path));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
185
215
|
if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
186
216
|
entries.push({ path, label: key, level: prefix ? prefix.split(".").length : 0, value, type: typeof value, editable: true });
|
|
187
217
|
}
|
|
@@ -202,8 +232,9 @@ export class ConfigPanel {
|
|
|
202
232
|
cursor = 0;
|
|
203
233
|
scroll = 0;
|
|
204
234
|
viewport = 20;
|
|
205
|
-
|
|
206
|
-
|
|
235
|
+
keepCursorVisible = true;
|
|
236
|
+
draft;
|
|
237
|
+
notice = "";
|
|
207
238
|
edits = new Map();
|
|
208
239
|
entries;
|
|
209
240
|
constructor(config, theme, actions, requestRender, dirs = defaultHostDirs()) {
|
|
@@ -212,15 +243,26 @@ export class ConfigPanel {
|
|
|
212
243
|
this.actions = actions;
|
|
213
244
|
this.requestRender = requestRender;
|
|
214
245
|
this.dirs = dirs;
|
|
215
|
-
|
|
246
|
+
const raw = readUserConfig(dirs);
|
|
247
|
+
const user = applyUserOverrides(defaultConfig(), raw);
|
|
248
|
+
let visible = { ...config, context: { recallTool: user.context.recallTool } };
|
|
249
|
+
// Only selector rows use the user layer; never persist a trusted project's recall override here.
|
|
250
|
+
for (const path of Object.keys(SELECTOR_OPTIONS)) {
|
|
251
|
+
const value = getNestedValue(raw, path);
|
|
252
|
+
visible = setNestedValue(visible, path, value === undefined ? getNestedValue(user, path) : value);
|
|
253
|
+
}
|
|
254
|
+
this.entries = flattenConfig(visible);
|
|
216
255
|
}
|
|
217
256
|
invalidate() { }
|
|
218
257
|
lines(width) {
|
|
219
|
-
const { theme, entries, cursor,
|
|
258
|
+
const { theme, entries, cursor, draft, edits } = this;
|
|
220
259
|
const out = [];
|
|
221
|
-
const modeLabel =
|
|
260
|
+
const modeLabel = draft?.type === "select" ? "selecting" : draft ? "editing" : "browsing";
|
|
261
|
+
const editingKeys = draft?.type === "select" ? "←→ choose · enter save · esc cancel"
|
|
262
|
+
: draft ? "enter stage · esc cancel · s saves after editing"
|
|
263
|
+
: "↑↓ navigate · enter toggle/edit · esc back · s save";
|
|
222
264
|
const keys = this.focused
|
|
223
|
-
?
|
|
265
|
+
? editingKeys + " · " + modeLabel
|
|
224
266
|
: `click for keys · /warden config closes`;
|
|
225
267
|
out.push(theme.bold(theme.fg("accent", "pi-warden config")) + theme.fg("muted", ` · ${entries.length} keys`));
|
|
226
268
|
out.push(theme.fg("muted", keys));
|
|
@@ -236,8 +278,12 @@ export class ConfigPanel {
|
|
|
236
278
|
continue;
|
|
237
279
|
}
|
|
238
280
|
let valueStr;
|
|
239
|
-
if (
|
|
240
|
-
|
|
281
|
+
if (entry.type === "select") {
|
|
282
|
+
const value = isSelected && draft?.type === "select" ? draft.value : entry.value;
|
|
283
|
+
valueStr = "‹ " + selectorValue(entry, value) + " ›";
|
|
284
|
+
}
|
|
285
|
+
else if (isSelected && draft?.type === "text") {
|
|
286
|
+
valueStr = draft.buffer + "\u2588";
|
|
241
287
|
}
|
|
242
288
|
else if (entry.type === "boolean") {
|
|
243
289
|
valueStr = entry.value ? "●" : "○";
|
|
@@ -263,31 +309,48 @@ export class ConfigPanel {
|
|
|
263
309
|
const border = theme_fg(this.theme, "muted", "│ ");
|
|
264
310
|
const inner = Math.max(10, width - 2);
|
|
265
311
|
const all = this.lines(inner);
|
|
266
|
-
const
|
|
312
|
+
const fixed = all.splice(0, 3);
|
|
313
|
+
if (this.notice) {
|
|
314
|
+
const notice = wrapTextWithAnsi(this.theme.fg("warning", this.notice), inner);
|
|
315
|
+
// Keep the notice outside the scrolling list, with room for at least one selected row.
|
|
316
|
+
fixed.push(...notice.slice(0, Math.max(1, this.viewport - fixed.length - 2)));
|
|
317
|
+
}
|
|
318
|
+
const available = Math.max(1, this.viewport - fixed.length);
|
|
319
|
+
const showFooter = all.length > available && available > 1;
|
|
320
|
+
const listRows = available - (showFooter ? 1 : 0);
|
|
321
|
+
const maxScroll = Math.max(0, all.length - listRows);
|
|
267
322
|
if (this.scroll > maxScroll)
|
|
268
323
|
this.scroll = maxScroll;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
324
|
+
if (this.keepCursorVisible) {
|
|
325
|
+
if (this.cursor < this.scroll)
|
|
326
|
+
this.scroll = this.cursor;
|
|
327
|
+
else if (this.cursor >= this.scroll + listRows)
|
|
328
|
+
this.scroll = this.cursor - listRows + 1;
|
|
329
|
+
this.keepCursorVisible = false;
|
|
330
|
+
}
|
|
331
|
+
const visible = [...fixed, ...all.slice(this.scroll, this.scroll + listRows)];
|
|
332
|
+
if (showFooter) {
|
|
333
|
+
const below = all.length - this.scroll - listRows;
|
|
273
334
|
const above = this.scroll;
|
|
274
335
|
const label = below > 0 ? `… ${below} more below${above ? ` · ${above} above` : ""}` : `… end${above ? ` · ${above} above` : ""}`;
|
|
275
|
-
visible
|
|
336
|
+
visible.push(truncateToWidth(theme_fg(this.theme, "dim", label), inner, ""));
|
|
276
337
|
}
|
|
277
338
|
while (visible.length < this.viewport)
|
|
278
339
|
visible.push("");
|
|
279
340
|
return visible.map(line => border + line);
|
|
280
341
|
}
|
|
281
|
-
setViewport(rows) { this.viewport = Math.max(5, rows); }
|
|
342
|
+
setViewport(rows) { this.viewport = Math.max(5, rows); this.keepCursorVisible = true; }
|
|
282
343
|
handleInput(data) {
|
|
283
|
-
const { entries,
|
|
344
|
+
const { entries, draft } = this;
|
|
284
345
|
if (data === "q" || matchesKey(data, Key.ctrl("c"))) {
|
|
285
346
|
this.actions.close();
|
|
286
347
|
return;
|
|
287
348
|
}
|
|
349
|
+
this.keepCursorVisible = true;
|
|
288
350
|
if (matchesKey(data, Key.escape)) {
|
|
289
|
-
if (
|
|
290
|
-
this.
|
|
351
|
+
if (draft) {
|
|
352
|
+
this.draft = undefined;
|
|
353
|
+
this.notice = "";
|
|
291
354
|
this.requestRender();
|
|
292
355
|
return;
|
|
293
356
|
}
|
|
@@ -295,22 +358,58 @@ export class ConfigPanel {
|
|
|
295
358
|
this.requestRender();
|
|
296
359
|
return;
|
|
297
360
|
}
|
|
298
|
-
if (
|
|
361
|
+
if (draft) {
|
|
299
362
|
const entry = entries[this.cursor];
|
|
363
|
+
if (draft.type === "select" && entry.type === "select") {
|
|
364
|
+
if (matchesKey(data, Key.left) || matchesKey(data, Key.right)) {
|
|
365
|
+
const step = matchesKey(data, Key.right) ? 1 : -1;
|
|
366
|
+
const index = typeof draft.value === "string" ? entry.options.indexOf(draft.value) : -1;
|
|
367
|
+
const next = index < 0 ? (step > 0 ? 0 : entry.options.length - 1)
|
|
368
|
+
: (index + step + entry.options.length) % entry.options.length;
|
|
369
|
+
draft.value = entry.options[next];
|
|
370
|
+
draft.cycled = true;
|
|
371
|
+
this.notice = "";
|
|
372
|
+
}
|
|
373
|
+
else if (data === "enter" || matchesKey(data, Key.return)) {
|
|
374
|
+
if (!draft.cycled)
|
|
375
|
+
this.draft = undefined;
|
|
376
|
+
else if (entry.path === "typesafeBackend" && [...this.edits.keys()].some(path => path.startsWith("typesafeBackend."))) {
|
|
377
|
+
this.notice = "Backend fields have pending edits. Press Esc, then s to save them before choosing a named backend.";
|
|
378
|
+
}
|
|
379
|
+
else if (this.saveChanges(new Map([[entry.path, draft.value]]))) {
|
|
380
|
+
entry.value = draft.value;
|
|
381
|
+
this.draft = undefined;
|
|
382
|
+
if (entry.path === "typesafeBackend") {
|
|
383
|
+
// Replace only this subtree; rebuilding all rows would discard unrelated staged values.
|
|
384
|
+
let end = this.cursor + 1;
|
|
385
|
+
while (entries[end]?.path.startsWith("typesafeBackend."))
|
|
386
|
+
end++;
|
|
387
|
+
entries.splice(this.cursor + 1, end - this.cursor - 1);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else
|
|
392
|
+
return;
|
|
393
|
+
this.requestRender();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (draft.type !== "text")
|
|
397
|
+
return;
|
|
300
398
|
if (matchesKey(data, Key.return)) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
399
|
+
const value = this.parseValue(draft.buffer, entry.type);
|
|
400
|
+
this.edits.set(entry.path, value);
|
|
401
|
+
entry.value = value;
|
|
402
|
+
this.draft = undefined;
|
|
304
403
|
this.requestRender();
|
|
305
404
|
return;
|
|
306
405
|
}
|
|
307
406
|
if (matchesKey(data, Key.backspace)) {
|
|
308
|
-
|
|
407
|
+
draft.buffer = draft.buffer.slice(0, -1);
|
|
309
408
|
this.requestRender();
|
|
310
409
|
return;
|
|
311
410
|
}
|
|
312
411
|
if (data.length === 1 && data >= " ") {
|
|
313
|
-
|
|
412
|
+
draft.buffer += data;
|
|
314
413
|
this.requestRender();
|
|
315
414
|
return;
|
|
316
415
|
}
|
|
@@ -330,13 +429,18 @@ export class ConfigPanel {
|
|
|
330
429
|
this.cursor = entries.length - 1;
|
|
331
430
|
else if (data === "enter" || matchesKey(data, Key.return)) {
|
|
332
431
|
const entry = entries[this.cursor];
|
|
333
|
-
if (entry.
|
|
432
|
+
if (!entry.editable)
|
|
433
|
+
return;
|
|
434
|
+
this.notice = "";
|
|
435
|
+
if (entry.type === "select") {
|
|
436
|
+
this.draft = { type: "select", value: entry.value, cycled: false };
|
|
437
|
+
}
|
|
438
|
+
else if (entry.type === "boolean") {
|
|
334
439
|
this.edits.set(entry.path, !entry.value);
|
|
335
440
|
entry.value = !entry.value;
|
|
336
441
|
}
|
|
337
442
|
else {
|
|
338
|
-
this.
|
|
339
|
-
this.editBuffer = String(entry.value);
|
|
443
|
+
this.draft = { type: "text", buffer: String(entry.value) };
|
|
340
444
|
}
|
|
341
445
|
}
|
|
342
446
|
else if (data === "s") {
|
|
@@ -345,16 +449,12 @@ export class ConfigPanel {
|
|
|
345
449
|
}
|
|
346
450
|
else
|
|
347
451
|
return;
|
|
348
|
-
// Keep cursor visible
|
|
349
|
-
if (this.cursor < this.scroll)
|
|
350
|
-
this.scroll = this.cursor;
|
|
351
|
-
else if (this.cursor >= this.scroll + this.viewport)
|
|
352
|
-
this.scroll = this.cursor - this.viewport + 1;
|
|
353
452
|
this.requestRender();
|
|
354
453
|
}
|
|
355
454
|
handleMouse(event) {
|
|
356
455
|
if (event.type === "wheel") {
|
|
357
456
|
this.scroll = Math.max(0, this.scroll + (event.wheelDelta ?? 0) * 3);
|
|
457
|
+
this.keepCursorVisible = false;
|
|
358
458
|
return { handled: true, render: true };
|
|
359
459
|
}
|
|
360
460
|
if (event.type === "press" && event.button === "left")
|
|
@@ -373,21 +473,34 @@ export class ConfigPanel {
|
|
|
373
473
|
save() {
|
|
374
474
|
if (this.edits.size === 0)
|
|
375
475
|
return;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
476
|
+
this.keepCursorVisible = true;
|
|
477
|
+
if (this.saveChanges(this.edits))
|
|
478
|
+
this.edits.clear();
|
|
479
|
+
this.requestRender();
|
|
480
|
+
}
|
|
481
|
+
saveChanges(changes) {
|
|
482
|
+
try {
|
|
483
|
+
const obj = readUserConfig(this.dirs);
|
|
484
|
+
for (const [path, value] of changes) {
|
|
485
|
+
const keys = path.split(".");
|
|
486
|
+
let current = obj;
|
|
487
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
488
|
+
const key = keys[i];
|
|
489
|
+
if (typeof current[key] !== "object" || current[key] === null || Array.isArray(current[key]))
|
|
490
|
+
current[key] = {};
|
|
491
|
+
current = current[key];
|
|
492
|
+
}
|
|
493
|
+
current[keys.at(-1)] = value;
|
|
386
494
|
}
|
|
387
|
-
|
|
495
|
+
writeUserConfig(obj, this.dirs);
|
|
496
|
+
this.notice = "";
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
const retry = this.draft?.type === "select" ? "Enter" : "s";
|
|
501
|
+
this.notice = `Save failed. Retry ${retry}. ` + (error instanceof Error ? error.message : String(error));
|
|
502
|
+
return false;
|
|
388
503
|
}
|
|
389
|
-
writeUserConfig(obj, this.dirs);
|
|
390
|
-
this.edits.clear();
|
|
391
504
|
}
|
|
392
505
|
}
|
|
393
506
|
export function openConfigPanel(ui, config, options = {}) {
|
package/dist/widget.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ import type { ProseVerdict } from "./prose.js";
|
|
|
5
5
|
import type { RulesVerdict } from "./rules.js";
|
|
6
6
|
import type { RunawayVerdict } from "./runaway.js";
|
|
7
7
|
import type { StuckVerdict } from "./stuck.js";
|
|
8
|
-
export
|
|
9
|
-
export type
|
|
8
|
+
export declare const WIDGET_PLACEMENTS: readonly ["aboveEditor", "belowEditor"];
|
|
9
|
+
export type WidgetPlacement = typeof WIDGET_PLACEMENTS[number];
|
|
10
|
+
export declare const WIDGET_BAR_MODES: readonly ["stack", "live"];
|
|
11
|
+
export type WidgetBarMode = typeof WIDGET_BAR_MODES[number];
|
|
10
12
|
/** Palette shared by the status line and the trace sidebar: one place decides what a verdict looks like. */
|
|
11
13
|
export interface ThemeLike {
|
|
12
14
|
fg(color: string, text: string): string;
|
package/dist/widget.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { freshChecks } from "./done.js";
|
|
2
|
+
export const WIDGET_PLACEMENTS = ["aboveEditor", "belowEditor"];
|
|
3
|
+
export const WIDGET_BAR_MODES = ["stack", "live"];
|
|
2
4
|
export const LEVEL_COLOR = { allow: "success", ok: "success", warn: "warning", unverified: "warning", nudged: "warning", confirm: "error", deny: "error", stuck: "error", "false claim": "error", stopped: "error", "stopped, recovering": "error", violation: "warning", skipped: "muted", wake: "warning", silent: "muted", "appended silently": "muted", "possible credentials": "warning", error: "error" };
|
|
3
5
|
/** Verdicts that need no eye: the guard ran and found nothing. The rest keep a line of their own. */
|
|
4
6
|
export const QUIET_COLORS = new Set(["success", "muted"]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-warden",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Makes the Pi agent follow your project's rules. Jev judges every write against your pi-warden.md and quotes the broken rule back to the agent, names slop, breaks stuck loops, calls out unverified done claims, compresses large tool output, and holds the rare destructive command. Built on pi-typesafe.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|