pi-jev-auto-mode 0.4.1 → 0.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
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2026-09-24
4
+
5
+ - **Decision records are compact by default.** An approval is drawn as one line — status, tool,
6
+ latency, and the command — and a block as two, keeping the rationale. Each record used to take
7
+ five padded blocks, so a long session was mostly gate records. `/jev-auto-mode display full`
8
+ (or `"display": "full"`) restores the previous view, and the expanded view shows every detail
9
+ in either mode.
10
+
3
11
  ## 0.4.1 - 2026-09-18
4
12
 
5
13
  - **An unclear answer passes by default.** `uncertain` defaulted to `deny`, which meant a
package/README.md CHANGED
@@ -113,8 +113,14 @@ Packages are discovered in the [package gallery](https://pi.dev/packages) throug
113
113
  /jev-auto-mode uncertain show what the middle band resolves to
114
114
  /jev-auto-mode uncertain deny|ask|allow
115
115
  /jev-auto-mode scope all|matched how far the semantic layer reaches
116
+ /jev-auto-mode display full|compact how decision records are drawn
116
117
  ```
117
118
 
119
+ Each decision leaves a record in the transcript. `display: "compact"` (the default) folds an
120
+ approval into one line and a block into two (the rationale stays, because a block is something
121
+ to act on). `display: "full"` shows the heading, the engine line, the command, the reasons, and
122
+ the rationale. The expanded view shows every detail in either mode.
123
+
118
124
  ```
119
125
  pi --jev-auto-mode start with auto mode enabled
120
126
  ```
@@ -195,6 +201,7 @@ Policy notes: `$PI_CODING_AGENT_DIR/jev-auto-mode-policy.md`.
195
201
  "maxStateCharacters": 120000,
196
202
  "uncertain": "deny",
197
203
  "gateScope": "all",
204
+ "display": "compact",
198
205
  "thresholds": {}
199
206
  }
200
207
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-auto-mode",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Jev (TypeSafe System One) backed auto mode for the Pi coding agent: semantically auto-approves bash, write, and edit tool calls and fails closed when a decision cannot be made.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/extension.ts CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  import {
56
56
  DEFAULT_SETTINGS,
57
57
  JevAutoModeStore,
58
+ isDisplayMode,
58
59
  isGateScope,
59
60
  isUncertainAction,
60
61
  parseThreshold,
@@ -64,6 +65,7 @@ import {
64
65
  import {
65
66
  buildConfirmationDialog,
66
67
  describeSettings,
68
+ DISPLAY_EXPLANATION,
67
69
  GATE_SCOPE_EXPLANATION,
68
70
  UNCERTAIN_EXPLANATION,
69
71
  formatRuleTable,
@@ -571,7 +573,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
571
573
  default: false,
572
574
  });
573
575
 
574
- registerDecisionEntryRenderer(pi);
576
+ registerDecisionEntryRenderer(pi, () => state.settings.display);
575
577
 
576
578
  pi.registerCommand(AUTO_MODE_COMMAND, {
577
579
  description: "Show or change the Jev auto mode settings",
@@ -579,7 +581,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
579
581
  const value = String(argumentPrefix ?? "");
580
582
  const tokens = value.split(/\s+/).filter(Boolean);
581
583
  if (tokens.length === 0) {
582
- return ["status", "on", "off", "policy", "threshold", "scope", "uncertain", "login", "logout"].map((item) => ({
584
+ return ["status", "on", "off", "policy", "threshold", "scope", "uncertain", "display", "login", "logout"].map((item) => ({
583
585
  value: item,
584
586
  label: item,
585
587
  }));
@@ -756,6 +758,23 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
756
758
  return;
757
759
  }
758
760
 
761
+ if (value.startsWith("display")) {
762
+ const argument = value.slice("display".length).trim();
763
+ if (argument === "") {
764
+ ctx.ui.notify(`display: ${state.settings.display}\n\n${DISPLAY_EXPLANATION}`, "info");
765
+ return;
766
+ }
767
+ if (!isDisplayMode(argument)) {
768
+ ctx.ui.notify(`Expected one of full, compact.\n\n${DISPLAY_EXPLANATION}`, "error");
769
+ return;
770
+ }
771
+
772
+ state.settings = { ...state.settings, display: argument };
773
+ await save(gateContext);
774
+ ctx.ui.notify(`decision records now display as: ${argument}`, "info");
775
+ return;
776
+ }
777
+
759
778
  if (value.startsWith("uncertain")) {
760
779
  const argument = value.slice("uncertain".length).trim();
761
780
  if (argument === "") {
package/src/records.ts CHANGED
@@ -6,10 +6,11 @@
6
6
  * recorded rationale should not become ammunition for the next tool call.
7
7
  */
8
8
 
9
- import { Box, Text } from "@earendil-works/pi-tui";
9
+ import { Box, Text, truncateToWidth, type Component } from "@earendil-works/pi-tui";
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
  import type { ConditionReport, DecisionSource } from "./decide.ts";
12
12
  import { formatThreshold } from "./jev/decide.ts";
13
+ import type { DisplayMode } from "./settings.ts";
13
14
 
14
15
  export const DECISION_ENTRY_TYPE = "jev-auto-mode-decision";
15
16
 
@@ -85,12 +86,77 @@ export function formatConditionLines(record: DecisionRecord): string[] {
85
86
  });
86
87
  }
87
88
 
88
- export function registerDecisionEntryRenderer(pi: Pick<ExtensionAPI, "registerEntryRenderer">): void {
89
+ /** Collapse whitespace so a multi-line command reads as one line. */
90
+ function oneLine(text: string): string {
91
+ return text.replace(/\s+/g, " ").trim();
92
+ }
93
+
94
+ export interface CompactDecisionLines {
95
+ /** `🛡 jev allowed · bash · 299ms · <command>`: everything an approval needs. */
96
+ readonly headline: string;
97
+ /**
98
+ * The rationale, only for a call that did not run. An approval does not need its
99
+ * reasoning on screen; a block does, because the user has to act on it.
100
+ */
101
+ readonly detail?: string;
102
+ }
103
+
104
+ /**
105
+ * The compact view: one line for an approval, two for a block.
106
+ *
107
+ * Width is applied at render time. This only decides what the lines say.
108
+ */
109
+ export function formatCompactLines(record: DecisionRecord): CompactDecisionLines {
110
+ const approved = record.status === "allowed" || record.status === "confirmed";
111
+ const parts = [`${approved ? "🛡" : "⛔"} jev ${STATUS_LABEL[record.status]}`, record.tool];
112
+ if (!approved) parts.push(`via ${record.source}`);
113
+ if (typeof record.latencyMs === "number") parts.push(`${Math.round(record.latencyMs)}ms`);
114
+ const summary = oneLine(record.summary);
115
+ if (summary.length > 0) parts.push(summary);
116
+ const headline = parts.join(" · ");
117
+ if (approved) return { headline };
118
+ return { headline, detail: oneLine(record.rationale) };
119
+ }
120
+
121
+ /** A single line clipped to the viewport instead of wrapped. */
122
+ class ClippedLine implements Component {
123
+ private readonly text: string;
124
+
125
+ constructor(text: string) {
126
+ this.text = text;
127
+ }
128
+
129
+ render(width: number): string[] {
130
+ return [truncateToWidth(this.text, Math.max(1, width))];
131
+ }
132
+
133
+ invalidate(): void {}
134
+ }
135
+
136
+ export type DisplayModeSource = () => DisplayMode;
137
+
138
+ export function registerDecisionEntryRenderer(
139
+ pi: Pick<ExtensionAPI, "registerEntryRenderer">,
140
+ displayMode: DisplayModeSource = () => "compact",
141
+ ): void {
89
142
  pi.registerEntryRenderer<DecisionRecord>(DECISION_ENTRY_TYPE, (entry, options, theme) => {
90
143
  const record = entry.data;
91
144
  if (!record) return undefined;
92
145
 
93
146
  const approved = record.status === "allowed" || record.status === "confirmed";
147
+
148
+ // The expanded view is the place to read a decision in full, so it ignores the
149
+ // compact setting.
150
+ if (displayMode() === "compact" && !options.expanded) {
151
+ const lines = formatCompactLines(record);
152
+ const box = new Box(1, 0, (text) => theme.bg("customMessageBg", text));
153
+ box.addChild(new ClippedLine(theme.fg(approved ? "muted" : "error", lines.headline)));
154
+ if (lines.detail !== undefined) {
155
+ box.addChild(new ClippedLine(theme.fg("dim", `rationale: ${lines.detail}`)));
156
+ }
157
+ return box;
158
+ }
159
+
94
160
  const icon = approved ? "🛡" : "⛔";
95
161
  const heading = `${icon} ${theme.bold("jev auto mode")} ${theme.fg(
96
162
  approved ? "success" : "error",
package/src/settings.ts CHANGED
@@ -45,6 +45,23 @@ export interface JevAutoModeSettings {
45
45
  * visible in `/jev-auto-mode threshold` instead of silently resetting the rule.
46
46
  */
47
47
  readonly thresholds: Readonly<Record<string, number>>;
48
+ /**
49
+ * How decision records are drawn in the transcript.
50
+ *
51
+ * `compact` (the default) folds an approval into one line and a block into two, so
52
+ * a long session is not dominated by gate records. `full` shows the heading, the
53
+ * engine line, the command, the reasons, and the rationale. The expanded view (the tool
54
+ * expansion key) always shows every detail, whichever mode is set.
55
+ */
56
+ readonly display: DisplayMode;
57
+ }
58
+
59
+ export type DisplayMode = "full" | "compact";
60
+
61
+ export const DISPLAY_MODES: readonly DisplayMode[] = ["full", "compact"];
62
+
63
+ export function isDisplayMode(value: unknown): value is DisplayMode {
64
+ return typeof value === "string" && DISPLAY_MODES.includes(value as DisplayMode);
48
65
  }
49
66
 
50
67
  export type SettingsScope = "global" | "project";
@@ -90,6 +107,7 @@ export const DEFAULT_SETTINGS: JevAutoModeSettings = {
90
107
  uncertain: "allow",
91
108
  gateScope: "all",
92
109
  thresholds: {},
110
+ display: "compact",
93
111
  };
94
112
 
95
113
  const MAX_PATTERN_ENTRIES = 200;
@@ -193,6 +211,10 @@ export function parseSettingsPatch(value: unknown): SettingsPatch {
193
211
  patch.gateScope = record.gateScope;
194
212
  }
195
213
 
214
+ if (record.display !== undefined && isDisplayMode(record.display)) {
215
+ patch.display = record.display;
216
+ }
217
+
196
218
  const safeCommands = record.safeCommands === undefined ? undefined : readStringArray(record.safeCommands);
197
219
  if (safeCommands !== undefined) patch.safeCommands = safeCommands;
198
220
 
package/src/ui.ts CHANGED
@@ -44,6 +44,7 @@ export function describeSettings(settings: JevAutoModeSettings, scope: SettingsS
44
44
  `max state characters: ${settings.maxStateCharacters}`,
45
45
  `uncertain band: ${settings.uncertain}`,
46
46
  `gate scope: ${settings.gateScope}`,
47
+ `display: ${settings.display}`,
47
48
  ].join("\n");
48
49
  }
49
50
 
@@ -149,6 +150,15 @@ export const USAGE_TEXT = [
149
150
  " /jev-auto-mode uncertain show what the middle band resolves to",
150
151
  " /jev-auto-mode uncertain deny|ask|allow",
151
152
  " /jev-auto-mode scope all|matched which calls reach Jev",
153
+ " /jev-auto-mode display full|compact how decision records are drawn",
154
+ ].join("\n");
155
+
156
+ export const DISPLAY_EXPLANATION = [
157
+ "How decision records are drawn in the transcript.",
158
+ " compact - one line for an approval, two for a block (with the rationale) (default).",
159
+ " full - heading, engine line, command, reasons, and rationale.",
160
+ "The expanded view always shows every detail. New records use the mode at once;",
161
+ "records already on screen switch when Pi redraws them (e.g. toggling expansion).",
152
162
  ].join("\n");
153
163
 
154
164
  export const GATE_SCOPE_EXPLANATION = [