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 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 type RecallTool = "auto" | "rg" | "ag" | "ugrep" | "git-grep" | "grep" | "select-string" | "findstr" | "none";
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 type WardenMode = "steer" | "confirm" | "advise";
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 type ConscienceSkillMode = "off" | "recommend" | "load";
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 typeof value === "string" && RECALL_TOOLS.includes(value);
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 === "steer" || value === "confirm" || value === "advise";
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 === "belowEditor" || raw.placement === "aboveEditor" ? raw.placement : base.placement,
533
- barMode: raw.barMode === "live" || raw.barMode === "stack" ? raw.barMode : base.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 = typeof skillsRaw?.mode === "string" && ["off", "recommend", "load"].includes(skillsRaw.mode) ? skillsRaw.mode : base.skills.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 editing;
79
- private editBuffer;
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 { applyUserOverrides, defaultConfig, readUserConfig, writeUserConfig } from "./config.js";
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", "context", "runaway", "notify", "subagent"]);
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
- editing = false;
206
- editBuffer = "";
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
- this.entries = flattenConfig(config);
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, scroll, editing, editBuffer, edits } = this;
258
+ const { theme, entries, cursor, draft, edits } = this;
220
259
  const out = [];
221
- const modeLabel = editing ? "editing" : "browsing";
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
- ? `↑↓ navigate · enter toggle/edit · esc ${editing ? "cancel" : "back"} · s save · ${modeLabel}`
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 (isSelected && editing && entry.type !== "boolean") {
240
- valueStr = editBuffer + "\u2588";
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 maxScroll = Math.max(0, all.length - this.viewport);
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
- const visible = all.slice(this.scroll, this.scroll + this.viewport);
270
- if (all.length > this.viewport) {
271
- const last = visible.length - 1;
272
- const below = all.length - this.scroll - this.viewport;
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[last] = truncateToWidth(theme_fg(this.theme, "dim", label), inner, "");
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, editing } = this;
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 (editing) {
290
- this.editing = false;
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 (editing) {
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
- this.edits.set(entry.path, this.parseValue(this.editBuffer, entry.type));
302
- (entries[this.cursor]).value = this.parseValue(this.editBuffer, entry.type);
303
- this.editing = false;
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
- this.editBuffer = this.editBuffer.slice(0, -1);
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
- this.editBuffer += data;
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.type === "boolean") {
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.editing = true;
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
- const raw = readUserConfig(this.dirs);
377
- const obj = (typeof raw === "object" && raw !== null && !Array.isArray(raw)) ? raw : {};
378
- for (const [path, value] of this.edits) {
379
- const keys = path.split(".");
380
- let current = obj;
381
- for (let i = 0; i < keys.length - 1; i++) {
382
- const k = keys[i];
383
- if (typeof current[k] !== "object" || current[k] === null)
384
- current[k] = {};
385
- current = current[k];
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
- current[keys.at(-1)] = value;
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 type WidgetPlacement = "aboveEditor" | "belowEditor";
9
- export type WidgetBarMode = "stack" | "live";
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.4.0",
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",