pi-jev-auto-mode 0.4.0 → 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,31 @@
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
+
11
+ ## 0.4.1 - 2026-09-18
12
+
13
+ - **An unclear answer passes by default.** `uncertain` defaulted to `deny`, which meant a
14
+ judgment the model was unsure about stopped the call. That is the interruption an auto mode
15
+ exists to remove; clear rejections still block. `uncertain deny` and `uncertain ask` remain
16
+ available for anyone who wants the stricter behaviour.
17
+ - **Without a key the gate says so and stops**, instead of inventing a verdict and blocking with
18
+ an unexplained reason. The message names the fix: `/jev-auto-mode login`, or
19
+ `/jev-auto-mode off`. The footer reads `🛡 jev no key` in that state, and the session start
20
+ warns once.
21
+ - **A chain of read-only commands is read-only.** `cd src && ls -la && git log -3` was judged as
22
+ a whole because the allowlist rejected any command containing shell control syntax, so agents
23
+ paid a judgment round trip for their most common line. Each segment is now checked on its own,
24
+ and `cd` is allowed. A chain containing anything else (`curl … | sh`) is still judged.
25
+ - **The docs match the no-key behaviour.** The README and a comment still described a fallback
26
+ that confirms in a UI, which the gate no longer does: without a key it stops the calls it cannot
27
+ judge and says it is not connected to Jev.
28
+
3
29
 
4
30
  ## 0.4.0
5
31
 
package/README.md CHANGED
@@ -113,15 +113,22 @@ 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
  ```
121
127
 
122
128
  The semantic layer needs a [TypeSafe](https://typesafe.ai/) API key. Jev is early access, so an
123
- account may be waitlisted; **the gate still works without one**, running in ask-only mode
124
- (confirm in a UI, block without one) rather than silently allowing everything.
129
+ account may be waitlisted; **the gate still works without one**. Its own rules keep running —
130
+ read-only and user-declared safe commands pass, hard-deny shapes are blocked — but a call
131
+ nothing vouches for is blocked with "Not connected to Jev" instead of being judged.
125
132
 
126
133
  `/jev-auto-mode login` asks for the key, verifies it against the API (`GET /v1/models`), and
127
134
  stores it as an owner-only file at
@@ -134,9 +141,9 @@ A key is only stored after the API accepts it: a typo that got saved would turn
134
141
  that silently blocks every escalated call. If the API cannot be reached the key is not stored
135
142
  either, and the command says so rather than claiming success.
136
143
 
137
- Without a key the gate does not disable itself: it falls back to the ask-only engine, which
138
- confirms in a UI and blocks when there is none. The footer shows `🛡 jev (<scope>)` while the
139
- semantic layer is active and `🛡 jev ask-only (<scope>)` when it is not.
144
+ Without a key the gate does not disable itself and does not allow everything: it stops the calls
145
+ it cannot judge and says it is not connected to Jev. The footer shows `🛡 jev (<scope>)` while the
146
+ semantic layer is active and `🛡 jev no key (<scope>)` when it is not.
140
147
 
141
148
  ## Tuning
142
149
 
@@ -194,6 +201,7 @@ Policy notes: `$PI_CODING_AGENT_DIR/jev-auto-mode-policy.md`.
194
201
  "maxStateCharacters": 120000,
195
202
  "uncertain": "deny",
196
203
  "gateScope": "all",
204
+ "display": "compact",
197
205
  "thresholds": {}
198
206
  }
199
207
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-auto-mode",
3
- "version": "0.4.0",
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/decide.ts CHANGED
@@ -75,9 +75,8 @@ export interface DecisionEngine {
75
75
  /**
76
76
  * Milestone-1 engine: no semantic judgment at all.
77
77
  *
78
- * Every candidate is reported as `uncertain`, which means "ask the user when a UI
79
- * exists, block otherwise". That keeps the deterministic layer shippable and
80
- * verifiable on its own, and it fails in the safe direction.
78
+ * The gate checks for this engine before judging: with no key there is nothing to
79
+ * judge with, so it says so and blocks the call instead of inventing a verdict.
81
80
  */
82
81
  export function createManualEngine(): DecisionEngine {
83
82
  return {
package/src/extension.ts CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  dangerousReasons,
42
42
  evaluateUserCommandRules,
43
43
  hardDenyReasons,
44
- isReadOnlyCommand,
44
+ isReadOnlyCommandChain,
45
45
  isUserDeclaredSafeCommand,
46
46
  PROTECTED_DIRECTORY_SEGMENTS,
47
47
  unique,
@@ -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,
@@ -80,6 +82,13 @@ export const AUTO_MODE_COMMAND = "jev-auto-mode";
80
82
  /** The escalation reason for a call no pattern describes, under `gateScope: "all"`. */
81
83
  export const NOT_KNOWN_SAFE_REASON = "not on the known-safe list";
82
84
 
85
+ /** The engine used when no semantic layer is available. */
86
+ export const MANUAL_ENGINE_ID = "manual";
87
+
88
+ /** Shown (and used as the block reason) when the gate has no Jev connection. */
89
+ export const NO_ENGINE_MESSAGE =
90
+ "Not connected to Jev (no TypeSafe API key is set). Run `/jev-auto-mode login` to set a key, or `/jev-auto-mode off` to stop auto mode.";
91
+
83
92
  /** Structural context: what this extension needs from Pi, and nothing more. */
84
93
  export interface GateUi {
85
94
  notify(message: string, type?: "info" | "warning" | "error"): void;
@@ -248,7 +257,7 @@ export async function evaluateToolCall(
248
257
  const matchedReasons = dangerousReasons(command, ctx.cwd);
249
258
  if (matchedReasons.length > 0) {
250
259
  reasons = matchedReasons;
251
- } else if (isReadOnlyCommand(command)) {
260
+ } else if (isReadOnlyCommandChain(command)) {
252
261
  return undefined;
253
262
  } else if (state.settings.gateScope === "matched") {
254
263
  return undefined;
@@ -265,6 +274,20 @@ export async function evaluateToolCall(
265
274
  reasons = protectedReasons;
266
275
  }
267
276
 
277
+ // Without a key there is nothing to judge with. Say so and stop, rather than
278
+ // letting a call through unjudged or blocking it with an unexplained verdict.
279
+ if (deps.engine.id === MANUAL_ENGINE_ID) {
280
+ const rationale = NO_ENGINE_MESSAGE;
281
+ writeRecord(deps, {
282
+ call,
283
+ reasons,
284
+ status: "blocked",
285
+ source: "unavailable",
286
+ rationale,
287
+ });
288
+ return { block: true, reason: rationale };
289
+ }
290
+
268
291
  const input: CandidateInput = {
269
292
  call,
270
293
  reasons,
@@ -460,8 +483,9 @@ export interface RegisterOptions {
460
483
  /**
461
484
  * Build the semantic engine for the current settings.
462
485
  *
463
- * Without a key the gate keeps working with the ask-only engine rather than
464
- * dropping to "allow": the degradation stays visible and stays closed.
486
+ * With no key the gate still runs its own rules, but a call nothing vouches for is
487
+ * blocked with "Not connected to Jev" rather than judged. A missing key must not
488
+ * turn into "allow everything".
465
489
  */
466
490
  export function createEngine(
467
491
  settings: JevAutoModeSettings,
@@ -534,6 +558,9 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
534
558
  await rebuildEngine();
535
559
  loaded = true;
536
560
  updateStatus(ctx, { enabled: state.settings.enabled, engineId: deps.engine.id, scope: state.scope });
561
+ if (state.settings.enabled && deps.engine.id === MANUAL_ENGINE_ID) {
562
+ ctx.ui.notify(NO_ENGINE_MESSAGE, "warning");
563
+ }
537
564
  };
538
565
 
539
566
  const save = async (ctx: GateContext): Promise<void> => {
@@ -546,7 +573,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
546
573
  default: false,
547
574
  });
548
575
 
549
- registerDecisionEntryRenderer(pi);
576
+ registerDecisionEntryRenderer(pi, () => state.settings.display);
550
577
 
551
578
  pi.registerCommand(AUTO_MODE_COMMAND, {
552
579
  description: "Show or change the Jev auto mode settings",
@@ -554,7 +581,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
554
581
  const value = String(argumentPrefix ?? "");
555
582
  const tokens = value.split(/\s+/).filter(Boolean);
556
583
  if (tokens.length === 0) {
557
- 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) => ({
558
585
  value: item,
559
586
  label: item,
560
587
  }));
@@ -685,7 +712,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
685
712
  "Remove the stored TypeSafe API key?",
686
713
  availability.source === "env"
687
714
  ? "It is not in use anyway: TYPESAFE_API_KEY takes precedence."
688
- : "The semantic layer will fall back to ask-only until a key is available again.",
715
+ : "Without a key the gate blocks every call it cannot vouch for, and says why.",
689
716
  );
690
717
  if (!confirmed) return;
691
718
 
@@ -699,7 +726,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
699
726
  ctx.ui.notify(
700
727
  availability.available
701
728
  ? `Stored key removed. Still using ${describeKeySource(availability.source)}.`
702
- : "Stored key removed. The semantic layer is now ask-only.",
729
+ : "Stored key removed. The gate will block calls it cannot vouch for until a key is set again.",
703
730
  "info",
704
731
  );
705
732
  return;
@@ -731,6 +758,23 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
731
758
  return;
732
759
  }
733
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
+
734
778
  if (value.startsWith("uncertain")) {
735
779
  const argument = value.slice("uncertain".length).trim();
736
780
  if (argument === "") {
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Is the semantic layer usable right now, and where did the key come from?
3
3
  *
4
- * A missing key must not silently degrade into "allow everything": the engine
5
- * falls back to the ask-only engine, which confirms in a UI and blocks without
6
- * one. The reason and the key's origin are surfaced in `/jev-auto-mode status` so
7
- * the degradation and the credential path are visible rather than mysterious.
4
+ * A missing key must not silently degrade into "allow everything". The gate keeps
5
+ * running its own rules — read-only and user-declared safe commands pass, hard-deny
6
+ * patterns are blocked — but a call nothing vouches for is blocked with "Not
7
+ * connected to Jev" instead of being judged. The reason and the key's origin are
8
+ * surfaced in `/jev-auto-mode status` so the situation is visible rather than
9
+ * mysterious.
8
10
  *
9
11
  * `TYPESAFE_API_KEY` wins over the stored secret, so a one-off or CI override does
10
12
  * not require touching the stored credential.
package/src/policy.ts CHANGED
@@ -45,6 +45,7 @@ export interface CommandPattern {
45
45
  export const SAFE_COMMANDS: readonly string[] = [
46
46
  // Shell state and navigation
47
47
  "pwd",
48
+ "cd*",
48
49
  "ls*",
49
50
  "tree*",
50
51
  "whoami",
@@ -392,6 +393,24 @@ export function isReadOnlyCommand(command: string): boolean {
392
393
  return matchesAnyCommandPattern(command, SAFE_COMMANDS, false) !== undefined;
393
394
  }
394
395
 
396
+ /**
397
+ * A chain of read-only commands, such as `cd src && ls -la && git log`.
398
+ *
399
+ * Agents chain commands constantly, and a single `&&` would otherwise take an
400
+ * otherwise harmless line out of the fast path and into a judgment round trip.
401
+ * Every segment must be read-only on its own; `curl … | sh` splits into `curl …`
402
+ * (not on the list) and `sh` (not on the list), so it is still judged, and a
403
+ * segment containing a redirection fails the matcher anyway.
404
+ */
405
+ export function isReadOnlyCommandChain(command: string): boolean {
406
+ const segments = command
407
+ .split(/&&|\|\||;|\||\n/)
408
+ .map((segment) => segment.trim())
409
+ .filter((segment) => segment.length > 0);
410
+ if (segments.length === 0) return false;
411
+ return segments.every((segment) => isReadOnlyCommand(segment));
412
+ }
413
+
395
414
  /**
396
415
  * Patterns the user declared safe.
397
416
  *
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
@@ -28,7 +28,7 @@ export interface JevAutoModeSettings {
28
28
  readonly extraProtectedPaths: readonly string[];
29
29
  /** Shared state + questions budget guard, in characters. */
30
30
  readonly maxStateCharacters: number;
31
- /** What a middle-band judgment means. Default `deny`: no user confirmation. */
31
+ /** What a middle-band judgment means. Default `allow`: no user confirmation. */
32
32
  readonly uncertain: UncertainAction;
33
33
  /**
34
34
  * Which calls reach the semantic layer.
@@ -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";
@@ -52,10 +69,11 @@ export type SettingsScope = "global" | "project";
52
69
  /**
53
70
  * How a judgment that lands in the middle band is resolved.
54
71
  *
55
- * `deny` (the default) means the gate never takes over the screen: Jev's probability
56
- * is the whole answer, and "not sure" fails closed like every other undecidable
57
- * state. `ask` hands the call to the user, which contradicts the point of an auto
58
- * mode and is therefore opt-in. `allow` trusts the middle band.
72
+ * `allow` (the default) keeps an auto mode useful: Jev blocks what it can clearly
73
+ * reject and lets an unclear answer through, so the gate never interrupts. `deny`
74
+ * is the conservative alternative for anyone who wants "not sure" to stop a call.
75
+ * `ask` hands the call to the user, which contradicts the point of an auto mode and
76
+ * is therefore not the default.
59
77
  */
60
78
  export type UncertainAction = "deny" | "ask" | "allow";
61
79
 
@@ -86,9 +104,10 @@ export const DEFAULT_SETTINGS: JevAutoModeSettings = {
86
104
  disallowedCommands: [],
87
105
  extraProtectedPaths: [],
88
106
  maxStateCharacters: 120_000,
89
- uncertain: "deny",
107
+ uncertain: "allow",
90
108
  gateScope: "all",
91
109
  thresholds: {},
110
+ display: "compact",
92
111
  };
93
112
 
94
113
  const MAX_PATTERN_ENTRIES = 200;
@@ -192,6 +211,10 @@ export function parseSettingsPatch(value: unknown): SettingsPatch {
192
211
  patch.gateScope = record.gateScope;
193
212
  }
194
213
 
214
+ if (record.display !== undefined && isDisplayMode(record.display)) {
215
+ patch.display = record.display;
216
+ }
217
+
195
218
  const safeCommands = record.safeCommands === undefined ? undefined : readStringArray(record.safeCommands);
196
219
  if (safeCommands !== undefined) patch.safeCommands = safeCommands;
197
220
 
package/src/ui.ts CHANGED
@@ -21,8 +21,7 @@ export interface StatusInput {
21
21
  export function statusText(input: StatusInput): string {
22
22
  if (!input.enabled) return "🛡 jev off";
23
23
  const scope = input.scope === "project" ? "project" : "global";
24
- const engine = input.engineId === "manual" ? " ask-only" : "";
25
- return `🛡 jev${engine} (${scope})`;
24
+ return input.engineId === "manual" ? `🛡 jev no key (${scope})` : `🛡 jev (${scope})`;
26
25
  }
27
26
 
28
27
  export interface StatusContext {
@@ -45,6 +44,7 @@ export function describeSettings(settings: JevAutoModeSettings, scope: SettingsS
45
44
  `max state characters: ${settings.maxStateCharacters}`,
46
45
  `uncertain band: ${settings.uncertain}`,
47
46
  `gate scope: ${settings.gateScope}`,
47
+ `display: ${settings.display}`,
48
48
  ].join("\n");
49
49
  }
50
50
 
@@ -150,6 +150,15 @@ export const USAGE_TEXT = [
150
150
  " /jev-auto-mode uncertain show what the middle band resolves to",
151
151
  " /jev-auto-mode uncertain deny|ask|allow",
152
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).",
153
162
  ].join("\n");
154
163
 
155
164
  export const GATE_SCOPE_EXPLANATION = [