pi-jarvis 1.2.3 → 1.3.1

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/AGENTS.md CHANGED
@@ -4,18 +4,24 @@
4
4
  - `pi-jarvis` is a Pi extension that opens a `/jarvis` side-conversation overlay.
5
5
  - Core runtime files: `index.ts`, `side-session.ts`, `overlay.ts`, `jarvis-config.ts`, `session-ref.ts`.
6
6
 
7
- ## Current `/jarvis-model` Behavior
7
+ ## Current `/jarvis-model` and `/jarvis-thinking` Behavior
8
8
  - `/jarvis-model` with no request opens a model picker when Pi has a UI.
9
9
  - `/jarvis-model <provider/model>` writes a project-scoped override to `.pi/jarvis.json`.
10
10
  - `/jarvis-model --global <provider/model>` writes the default global override to the active Pi agent directory's `extensions/pi-jarvis.json` (by default `~/.pi/agent/extensions/pi-jarvis.json`).
11
11
  - `/jarvis-model [--project|--global] follow-main` stores a scoped `follow-main` override.
12
12
  - `/jarvis-model [--project|--global] clear` removes that scope's override so fallback applies.
13
- - Resolution order is: project config, then global config, then built-in `follow-main`.
13
+ - `/jarvis-thinking [--project|--global] auto|follow-main|off|minimal|low|medium|high|xhigh` stores a scoped thinking override without changing the main session thinking level.
14
+ - `/jarvis-thinking [--project|--global] clear` removes that scope's thinking override so fallback applies.
15
+ - Resolution order is: project config, then global config, then built-in defaults (`follow-main` for model, `auto` for thinking).
16
+ - `auto` thinking follows main only when `/jarvis` follows the main model; pinned `/jarvis` models default to thinking `off`.
17
+ - `follow-main` thinking follows the main thinking level regardless of model selection, except xAI `/jarvis` models force thinking `off`.
18
+ - `/compact`, `/tree`, and `/new` entered inside `/jarvis` operate on the isolated `/jarvis` side-session, not the main Pi session.
14
19
  - `Note main` and `Redirect` stay disabled when the active `/jarvis` model is incompatible with bridge tools.
15
20
 
16
21
  ## Validation
17
22
  - Run `npm test` for full validation.
18
23
  - Run `npm run build` before release packaging.
24
+ - Run `npm run verify:release` to validate npm pack payload and release metadata expectations.
19
25
 
20
26
  ## Docs To Keep In Sync
21
27
  - `README.md`
package/README.md CHANGED
@@ -13,6 +13,8 @@
13
13
  [![Pi extension](https://img.shields.io/badge/Pi-extension-06b6d4?style=for-the-badge)](https://github.com/fluxgear/pi-jarvis)
14
14
  [![TypeScript](https://img.shields.io/badge/TypeScript-powered-2563eb?style=for-the-badge)](./package.json)
15
15
 
16
+ <p><strong>Current version:</strong> 1.3.1</p>
17
+
16
18
  <p>
17
19
  <strong>Persistent side session</strong> ·
18
20
  <strong>Live main-session awareness</strong> ·
@@ -79,15 +81,15 @@ flowchart LR
79
81
 
80
82
  ```mermaid
81
83
  flowchart TD
82
- A[Main session keeps moving] --> B[/jarvis opens in overlay]
83
- B --> C[Jarvis sees current main-session context]
84
- C --> D{What do you need?}
85
- D -->|Status / summary / analysis| E[Jarvis handles the side task]
86
- D -->|Repo inspection| F[Enable Repo tools]
87
- D -->|Influence the main lane| G[Enable Note main or Redirect]
88
- G --> H{Redirect?}
89
- H -->|Yes| I[Per-send confirmation]
90
- H -->|No| J[Quiet follow-up note]
84
+ A["Main session keeps moving"] --> B["/jarvis opens in overlay"]
85
+ B --> C["Jarvis sees current main-session context"]
86
+ C --> D{"What do you need?"}
87
+ D -->|Status / summary / analysis| E["Jarvis handles the side task"]
88
+ D -->|Repo inspection| F["Enable Repo tools"]
89
+ D -->|Influence the main lane| G["Enable Note main or Redirect"]
90
+ G --> H{"Redirect?"}
91
+ H -->|Yes| I["Per-send confirmation"]
92
+ H -->|No| J["Quiet follow-up note"]
91
93
  ```
92
94
 
93
95
  ---
@@ -165,26 +167,49 @@ Restores the chosen scope to `follow-main`. A project-scoped `follow-main` overr
165
167
  ### `/jarvis-model [--project|--global] clear`
166
168
  Removes the selected scope so `/jarvis` falls back through the remaining config layers to the built-in default.
167
169
 
170
+ ### `/jarvis-thinking [--project|--global] auto|follow-main|off|minimal|low|medium|high|xhigh`
171
+ Sets the thinking level used by `/jarvis` without changing the main session thinking level. A plain `/jarvis-thinking <level>` writes a **project-local** override to `.pi/jarvis.json`.
172
+
173
+ - `auto` preserves the built-in behavior: follow the main thinking level only when `/jarvis` follows the main model; pinned `/jarvis` models use `off`.
174
+ - `follow-main` follows the main thinking level even when `/jarvis` is pinned to a separate model.
175
+ - Explicit levels pin `/jarvis` thinking to that level.
176
+ - xAI `/jarvis` models still force thinking `off`.
177
+
178
+ ### `/jarvis-thinking [--project|--global] clear`
179
+ Removes the selected scope so `/jarvis` thinking falls back through the remaining config layers to the built-in `auto` default.
180
+
181
+ ### Side-session commands inside `/jarvis`
182
+ The `/jarvis` input handles a small set of built-in commands against the isolated side-session:
183
+
184
+ - `/compact [instructions]` compacts the `/jarvis` conversation context.
185
+ - `/tree` prints the `/jarvis` session tree with entry IDs.
186
+ - `/tree <entry-id>` navigates the `/jarvis` session tree to that entry.
187
+ - `/tree --summarize <entry-id> [instructions]` navigates and summarizes the branch being left.
188
+ - `/new` starts a fresh `/jarvis` side-session without changing the main Pi session.
189
+
168
190
  ---
169
191
 
170
- ## Model resolution
192
+ ## Model and thinking resolution
171
193
 
172
194
  ```mermaid
173
195
  flowchart TD
174
196
  A[Project config\n.pi/jarvis.json] -->|if present| B{valid?}
175
- B -->|yes| P[Use project selection]
197
+ B -->|yes| P[Use project overrides]
176
198
  B -->|no or missing| C[Global config\n&lt;agentDir&gt;/extensions/pi-jarvis.json]
177
199
  C -->|if present| D{valid?}
178
- D -->|yes| G[Use global selection]
179
- D -->|no or missing| E[Built-in default]
180
- E --> F[follow-main]
200
+ D -->|yes| G[Use global overrides]
201
+ D -->|no or missing| E[Built-in defaults]
202
+ E --> F[Model default: follow-main]
203
+ E --> H[Thinking default: auto]
181
204
  ```
182
205
 
183
206
  ### Resolution order
184
207
 
208
+ Model and thinking settings resolve through the same config layers, then fall back to separate built-in defaults:
209
+
185
210
  1. project config: `.pi/jarvis.json`
186
211
  2. global config: `~/.pi/agent/extensions/pi-jarvis.json` or the equivalent path under a custom Pi agent dir
187
- 3. built-in default: `follow-main`
212
+ 3. built-in defaults: model `follow-main`, thinking `auto`
188
213
 
189
214
  ---
190
215
 
@@ -246,7 +271,9 @@ sequenceDiagram
246
271
  - `/jarvis` keeps its own isolated conversation state
247
272
  - prior side-session history is restored from a session file under `jarvis-sessions/`
248
273
  - Jarvis sees current main-session state plus a compact delta since the last `/jarvis` turn
249
- - plain `/jarvis-model <provider/model>` writes the project override; use `--global` to change the global default
274
+ - `/compact`, `/tree`, and `/new` entered inside `/jarvis` operate on the side-session, not the main session
275
+ - plain `/jarvis-model <provider/model>` writes the project model override; use `--global` to change the global default
276
+ - plain `/jarvis-thinking <level>` writes the project thinking override; use `--global` to change the global default
250
277
  - thinking-step streaming is intentionally collapsed to a cleaner animated fallback for readability
251
278
 
252
279
  ---
@@ -320,10 +347,10 @@ Build the published package contents:
320
347
  npm run build
321
348
  ```
322
349
 
323
- Preview the npm payload:
350
+ Preview and verify the npm payload contract:
324
351
 
325
352
  ```bash
326
- npm pack --dry-run
353
+ npm run verify:release
327
354
  ```
328
355
 
329
356
  ---
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,EAGN,KAAK,YAAY,EAGjB,MAAM,+BAA+B,CAAC;AA8DvC,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAyV9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,EAGN,KAAK,YAAY,EAGjB,MAAM,+BAA+B,CAAC;AA8FvC,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA8c9D"}
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { buildMainSessionContext } from "./main-context.js";
4
4
  import { MainSessionTracker } from "./main-session-state.js";
5
5
  import { attachOverlayBridge, JarvisOverlayBridge, JarvisOverlayComponent } from "./overlay.js";
6
6
  import { createJarvisSessionRef, readJarvisSessionRef, JARVIS_SESSION_REF_CUSTOM_TYPE } from "./session-ref.js";
7
- import { clearJarvisModelSelectionSetting, loadJarvisModelSelectionSetting, saveJarvisModelSelectionSetting } from "./jarvis-config.js";
7
+ import { clearJarvisModelSelectionSetting, clearJarvisThinkingSelectionSetting, loadJarvisModelSelectionSetting, loadJarvisThinkingSelectionSetting, saveJarvisModelSelectionSetting, saveJarvisThinkingSelectionSetting, } from "./jarvis-config.js";
8
8
  import { JarvisSideSessionRuntime, createSideSessionFile } from "./side-session.js";
9
9
  class StaleJarvisBootError extends Error {
10
10
  constructor() {
@@ -25,6 +25,8 @@ export default function jarvisExtension(pi) {
25
25
  queuedMessages: [],
26
26
  jarvisModelSelection: { mode: "follow-main" },
27
27
  jarvisModelSelectionSource: "default",
28
+ jarvisThinkingSelection: { mode: "auto" },
29
+ jarvisThinkingSelectionSource: "default",
28
30
  systemPrompt: "",
29
31
  themeProvider: () => {
30
32
  throw new Error("/jarvis theme requested before UI was available.");
@@ -53,6 +55,7 @@ export default function jarvisExtension(pi) {
53
55
  await ctx.ui.custom((tui, theme, _keybindings, done) => {
54
56
  state.themeProvider = () => theme;
55
57
  const closeOverlay = () => {
58
+ resetTransientAccessControls(state);
56
59
  done(undefined);
57
60
  queueMicrotask(() => tui.requestRender());
58
61
  };
@@ -106,8 +109,8 @@ export default function jarvisExtension(pi) {
106
109
  const clearScopedSelection = async () => {
107
110
  const previousSelection = state.jarvisModelSelection;
108
111
  const previousSource = state.jarvisModelSelectionSource;
109
- const projectSelection = scope === "project" ? undefined : loadJarvisModelSelectionSetting(ctx.cwd, "project");
110
- const globalSelection = scope === "global" ? undefined : loadJarvisModelSelectionSetting(ctx.cwd, "global");
112
+ const projectSelection = scope === "project" ? undefined : loadJarvisModelSelectionForClear(ctx.cwd, "project", ctx);
113
+ const globalSelection = scope === "global" ? undefined : loadJarvisModelSelectionForClear(ctx.cwd, "global", ctx);
111
114
  const resolvedSelection = resolveJarvisModelSelectionFromSettings(projectSelection, globalSelection, ctx.modelRegistry);
112
115
  await applyJarvisModelSelection(state, resolvedSelection.selection);
113
116
  try {
@@ -188,6 +191,90 @@ export default function jarvisExtension(pi) {
188
191
  await pinSelectedModel(selectedModel);
189
192
  },
190
193
  });
194
+ pi.registerCommand("jarvis-thinking", {
195
+ description: "Set the thinking level used by /jarvis without changing the main agent thinking level",
196
+ handler: async (args, ctx) => {
197
+ updateContextState(pi, state, ctx);
198
+ let parsedCommand;
199
+ try {
200
+ parsedCommand = parseJarvisThinkingCommand(args);
201
+ }
202
+ catch (error) {
203
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
204
+ return;
205
+ }
206
+ const { request, scope, clearScope } = parsedCommand;
207
+ const scopeLabel = formatJarvisModelSelectionScope(scope);
208
+ const rollbackSelection = async (previousSelection, previousSource) => {
209
+ await applyJarvisThinkingSelection(state, previousSelection);
210
+ state.jarvisThinkingSelectionSource = previousSource;
211
+ };
212
+ const persistSelection = async (selection) => {
213
+ const previousSelection = state.jarvisThinkingSelection;
214
+ const previousSource = state.jarvisThinkingSelectionSource;
215
+ await applyJarvisThinkingSelection(state, selection);
216
+ try {
217
+ saveJarvisThinkingSelectionSetting(ctx.cwd, scope, selection);
218
+ state.jarvisThinkingSelectionSource = scope;
219
+ }
220
+ catch (error) {
221
+ await rollbackSelection(previousSelection, previousSource);
222
+ throw error;
223
+ }
224
+ };
225
+ const clearScopedSelection = async () => {
226
+ const previousSelection = state.jarvisThinkingSelection;
227
+ const previousSource = state.jarvisThinkingSelectionSource;
228
+ const projectSelection = scope === "project" ? undefined : loadJarvisThinkingSelectionForClear(ctx.cwd, "project", ctx);
229
+ const globalSelection = scope === "global" ? undefined : loadJarvisThinkingSelectionForClear(ctx.cwd, "global", ctx);
230
+ const resolvedSelection = resolveJarvisThinkingSelectionFromSettings(projectSelection, globalSelection);
231
+ await applyJarvisThinkingSelection(state, resolvedSelection.selection);
232
+ try {
233
+ clearJarvisThinkingSelectionSetting(ctx.cwd, scope);
234
+ state.jarvisThinkingSelectionSource = resolvedSelection.source;
235
+ return resolvedSelection;
236
+ }
237
+ catch (error) {
238
+ await rollbackSelection(previousSelection, previousSource);
239
+ throw error;
240
+ }
241
+ };
242
+ if (clearScope) {
243
+ try {
244
+ await clearScopedSelection();
245
+ }
246
+ catch (error) {
247
+ ctx.ui.notify(`Failed to clear the /jarvis thinking setting ${scopeLabel}: ${error instanceof Error ? error.message : String(error)}`, "error");
248
+ return;
249
+ }
250
+ ctx.ui.notify(`Cleared the /jarvis thinking setting ${scopeLabel}. /jarvis is now ${describeJarvisThinkingSelection(state)}.`, "info");
251
+ return;
252
+ }
253
+ const normalizedRequest = request.toLowerCase();
254
+ let selection;
255
+ if (normalizedRequest === "auto") {
256
+ selection = { mode: "auto" };
257
+ }
258
+ else if (normalizedRequest === "follow-main") {
259
+ selection = { mode: "follow-main" };
260
+ }
261
+ else if (isJarvisThinkingLevel(normalizedRequest)) {
262
+ selection = { mode: "pinned", thinkingLevel: normalizedRequest };
263
+ }
264
+ if (!selection) {
265
+ ctx.ui.notify(`/jarvis is ${describeJarvisThinkingSelection(state)}. Use /jarvis-thinking [--project|--global] clear, auto, follow-main, off, minimal, low, medium, high, or xhigh.`, request ? "error" : "info");
266
+ return;
267
+ }
268
+ try {
269
+ await persistSelection(selection);
270
+ }
271
+ catch (error) {
272
+ ctx.ui.notify(`Failed to set /jarvis thinking ${scopeLabel}: ${error instanceof Error ? error.message : String(error)}`, "error");
273
+ return;
274
+ }
275
+ ctx.ui.notify(`Set /jarvis thinking to ${formatJarvisThinkingSelection(selection)} ${scopeLabel}. Effective /jarvis thinking is ${getDesiredJarvisThinkingLevel(state) ?? "off"}.`, "info");
276
+ },
277
+ });
191
278
  pi.on("session_start", async (_event, ctx) => {
192
279
  state.bootGeneration += 1;
193
280
  state.runtime?.dispose();
@@ -206,11 +293,20 @@ export default function jarvisExtension(pi) {
206
293
  selection: { mode: "follow-main" },
207
294
  source: "default",
208
295
  };
296
+ let resolvedThinkingSelection = {
297
+ selection: { mode: "auto" },
298
+ source: "default",
299
+ };
209
300
  resolvedSelection = resolveConfiguredJarvisModelSelection(ctx.cwd, ctx.modelRegistry, (scope, error) => {
210
301
  ctx.ui.notify(`Failed to load the ${scope} /jarvis model setting: ${error instanceof Error ? error.message : String(error)}`, "error");
211
302
  });
303
+ resolvedThinkingSelection = resolveConfiguredJarvisThinkingSelection(ctx.cwd, (scope, error) => {
304
+ ctx.ui.notify(`Failed to load the ${scope} /jarvis thinking setting: ${error instanceof Error ? error.message : String(error)}`, "error");
305
+ });
212
306
  state.jarvisModelSelection = resolvedSelection.selection;
213
307
  state.jarvisModelSelectionSource = resolvedSelection.source;
308
+ state.jarvisThinkingSelection = resolvedThinkingSelection.selection;
309
+ state.jarvisThinkingSelectionSource = resolvedThinkingSelection.source;
214
310
  updateContextState(pi, state, ctx);
215
311
  enforceCompatibilityGuard(state);
216
312
  if (resolvedSelection.unavailable) {
@@ -259,7 +355,8 @@ export default function jarvisExtension(pi) {
259
355
  state.mainSession.handleModelSelect(formatModelLabel(event.model));
260
356
  refreshMainContext(state);
261
357
  enforceCompatibilityGuard(state);
262
- if (!state.runtime || state.jarvisModelSelection.mode !== "follow-main") {
358
+ if (!state.runtime ||
359
+ (state.jarvisModelSelection.mode !== "follow-main" && state.jarvisThinkingSelection.mode !== "follow-main")) {
263
360
  return;
264
361
  }
265
362
  try {
@@ -279,6 +376,8 @@ export default function jarvisExtension(pi) {
279
376
  state.lastJarvisSeenMainContext = undefined;
280
377
  state.jarvisModelSelection = { mode: "follow-main" };
281
378
  state.jarvisModelSelectionSource = "default";
379
+ state.jarvisThinkingSelection = { mode: "auto" };
380
+ state.jarvisThinkingSelectionSource = "default";
282
381
  state.allowSideTools = false;
283
382
  state.allowFollowUpToMain = false;
284
383
  state.allowSteerToMain = false;
@@ -301,6 +400,12 @@ function updateContextState(pi, state, ctx) {
301
400
  function refreshMainContext(state) {
302
401
  state.mainContext = buildMainSessionContext(state.mainSession.snapshot());
303
402
  }
403
+ function resetTransientAccessControls(state) {
404
+ state.allowSideTools = false;
405
+ state.allowFollowUpToMain = false;
406
+ state.allowSteerToMain = false;
407
+ state.runtime?.setToolAccessEnabled(false);
408
+ }
304
409
  function getDesiredJarvisModel(state) {
305
410
  return state.jarvisModelSelection.mode === "pinned" ? state.jarvisModelSelection.model : state.model;
306
411
  }
@@ -309,6 +414,12 @@ function getDesiredJarvisThinkingLevel(state) {
309
414
  if (desiredModel?.provider === "xai") {
310
415
  return "off";
311
416
  }
417
+ if (state.jarvisThinkingSelection.mode === "pinned") {
418
+ return state.jarvisThinkingSelection.thinkingLevel;
419
+ }
420
+ if (state.jarvisThinkingSelection.mode === "follow-main") {
421
+ return state.thinkingLevel;
422
+ }
312
423
  return state.jarvisModelSelection.mode === "follow-main" ? state.thinkingLevel : "off";
313
424
  }
314
425
  function toStoredJarvisModelSelection(selection) {
@@ -351,6 +462,30 @@ function parseJarvisModelCommand(args) {
351
462
  clearScope: request.toLowerCase() === "clear",
352
463
  };
353
464
  }
465
+ function parseJarvisThinkingCommand(args) {
466
+ const parsed = parseJarvisModelCommand(args);
467
+ return {
468
+ request: parsed.request,
469
+ scope: parsed.scope,
470
+ clearScope: parsed.clearScope,
471
+ };
472
+ }
473
+ function isJarvisThinkingLevel(value) {
474
+ return value === "off" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
475
+ }
476
+ function formatJarvisThinkingSelection(selection) {
477
+ if (selection.mode === "auto") {
478
+ return "auto";
479
+ }
480
+ if (selection.mode === "follow-main") {
481
+ return "follow-main";
482
+ }
483
+ return selection.thinkingLevel;
484
+ }
485
+ function describeJarvisThinkingSelection(state) {
486
+ const sourceLabel = formatJarvisModelSelectionSource(state.jarvisThinkingSelectionSource);
487
+ return `thinking ${formatJarvisThinkingSelection(state.jarvisThinkingSelection)} (effective ${getDesiredJarvisThinkingLevel(state) ?? "off"}; ${sourceLabel})`;
488
+ }
354
489
  function resolveStoredJarvisModelSelection(storedSelection, source, modelRegistry) {
355
490
  if (storedSelection.mode === "follow-main") {
356
491
  return {
@@ -416,6 +551,50 @@ function resolveConfiguredJarvisModelSelection(cwd, modelRegistry, onScopeError)
416
551
  }
417
552
  return resolveJarvisModelSelectionFromSettings(projectSelection, globalSelection, modelRegistry);
418
553
  }
554
+ function resolveJarvisThinkingSelectionFromSettings(projectSelection, globalSelection) {
555
+ if (projectSelection) {
556
+ return { selection: projectSelection, source: "project" };
557
+ }
558
+ if (globalSelection) {
559
+ return { selection: globalSelection, source: "global" };
560
+ }
561
+ return { selection: { mode: "auto" }, source: "default" };
562
+ }
563
+ function resolveConfiguredJarvisThinkingSelection(cwd, onScopeError) {
564
+ let projectSelection;
565
+ let globalSelection;
566
+ try {
567
+ projectSelection = loadJarvisThinkingSelectionSetting(cwd, "project");
568
+ }
569
+ catch (error) {
570
+ onScopeError?.("project", error);
571
+ }
572
+ try {
573
+ globalSelection = loadJarvisThinkingSelectionSetting(cwd, "global");
574
+ }
575
+ catch (error) {
576
+ onScopeError?.("global", error);
577
+ }
578
+ return resolveJarvisThinkingSelectionFromSettings(projectSelection, globalSelection);
579
+ }
580
+ function loadJarvisModelSelectionForClear(cwd, scope, ctx) {
581
+ try {
582
+ return loadJarvisModelSelectionSetting(cwd, scope);
583
+ }
584
+ catch (error) {
585
+ ctx.ui.notify(`Ignoring malformed ${scope} /jarvis model setting while clearing the requested scope: ${error instanceof Error ? error.message : String(error)}`, "warning");
586
+ return undefined;
587
+ }
588
+ }
589
+ function loadJarvisThinkingSelectionForClear(cwd, scope, ctx) {
590
+ try {
591
+ return loadJarvisThinkingSelectionSetting(cwd, scope);
592
+ }
593
+ catch (error) {
594
+ ctx.ui.notify(`Ignoring malformed ${scope} /jarvis thinking setting while clearing the requested scope: ${error instanceof Error ? error.message : String(error)}`, "warning");
595
+ return undefined;
596
+ }
597
+ }
419
598
  function isModelBridgeCompatible(model) {
420
599
  if (!model) {
421
600
  return true;
@@ -509,6 +688,54 @@ async function applyJarvisModelSelection(state, selection) {
509
688
  throw error;
510
689
  }
511
690
  }
691
+ async function applyJarvisThinkingSelection(state, selection) {
692
+ const previousSelection = state.jarvisThinkingSelection;
693
+ state.jarvisThinkingSelection = selection;
694
+ try {
695
+ await syncRuntimeModelSelection(state);
696
+ }
697
+ catch (error) {
698
+ state.jarvisThinkingSelection = previousSelection;
699
+ try {
700
+ await syncRuntimeModelSelection(state);
701
+ }
702
+ catch {
703
+ // Keep the original sync failure; rollback is best-effort only.
704
+ }
705
+ throw error;
706
+ }
707
+ }
708
+ async function executeJarvisSideCommand(pi, state, ctx, runtime, command) {
709
+ if (command.name === "compact") {
710
+ await runtime.compactJarvisContext(command.customInstructions);
711
+ return runtime;
712
+ }
713
+ if (command.name === "tree") {
714
+ if (!command.targetId) {
715
+ runtime.addSystemMessage(runtime.describeSessionTree());
716
+ return runtime;
717
+ }
718
+ await runtime.navigateSessionTree(command.targetId, {
719
+ summarize: command.summarize,
720
+ customInstructions: command.customInstructions,
721
+ });
722
+ return runtime;
723
+ }
724
+ state.bootGeneration += 1;
725
+ state.runtime?.dispose();
726
+ state.runtime = undefined;
727
+ state.bootPromise = undefined;
728
+ state.flushPromise = undefined;
729
+ state.lastJarvisSeenMainContext = undefined;
730
+ resetTransientAccessControls(state);
731
+ const sessionFile = await createSideSessionFile(ctx.cwd);
732
+ const sessionRef = createJarvisSessionRef(sessionFile);
733
+ state.sessionRef = sessionRef;
734
+ pi.appendEntry(JARVIS_SESSION_REF_CUSTOM_TYPE, sessionRef);
735
+ const nextRuntime = await ensureRuntime(pi, state, ctx);
736
+ nextRuntime.addSystemMessage("Started a new /jarvis side session.");
737
+ return nextRuntime;
738
+ }
512
739
  function normalizeInitialMessage(args) {
513
740
  const message = args.trim();
514
741
  return message.length > 0 ? message : undefined;
@@ -516,6 +743,38 @@ function normalizeInitialMessage(args) {
516
743
  function queueMessage(state, message) {
517
744
  state.queuedMessages.push(message);
518
745
  }
746
+ function parseJarvisSideCommand(message) {
747
+ const text = message.trim();
748
+ if (text === "/new") {
749
+ return { name: "new" };
750
+ }
751
+ if (text === "/compact") {
752
+ return { name: "compact" };
753
+ }
754
+ if (text.startsWith("/compact ")) {
755
+ return { name: "compact", customInstructions: text.slice("/compact ".length).trim() || undefined };
756
+ }
757
+ if (text === "/tree") {
758
+ return { name: "tree", summarize: false };
759
+ }
760
+ if (text.startsWith("/tree ")) {
761
+ const args = text.slice("/tree ".length).trim();
762
+ const summarizePrefix = "--summarize ";
763
+ if (args.startsWith(summarizePrefix)) {
764
+ const rest = args.slice(summarizePrefix.length).trim();
765
+ const [targetId, ...instructionTokens] = rest.split(/\s+/).filter(Boolean);
766
+ return {
767
+ name: "tree",
768
+ targetId,
769
+ summarize: true,
770
+ customInstructions: instructionTokens.join(" ").trim() || undefined,
771
+ };
772
+ }
773
+ const [targetId] = args.split(/\s+/).filter(Boolean);
774
+ return { name: "tree", targetId, summarize: false };
775
+ }
776
+ return undefined;
777
+ }
519
778
  function createOverlayView(pi, state, ctx) {
520
779
  const syncToolAccess = () => {
521
780
  state.runtime?.setToolAccessEnabled(state.allowSideTools);
@@ -610,6 +869,9 @@ function formatMainContextDeltaLabel(previousContext, currentContext) {
610
869
  if (currentContext.summary.latestUserRequest && currentContext.summary.latestUserRequest !== previousContext.summary.latestUserRequest) {
611
870
  return `request → ${currentContext.summary.latestUserRequest}`;
612
871
  }
872
+ if (currentContext.summary.latestAssistantText && currentContext.summary.latestAssistantText !== previousContext.summary.latestAssistantText) {
873
+ return `assistant → ${currentContext.summary.latestAssistantText}`;
874
+ }
613
875
  return "no significant change";
614
876
  }
615
877
  async function flushQueuedMessages(pi, state, ctx) {
@@ -624,17 +886,24 @@ async function flushQueuedMessages(pi, state, ctx) {
624
886
  // rethrowing.
625
887
  state.flushPromise = (async () => {
626
888
  try {
627
- const runtime = await ensureRuntime(pi, state, ctx);
889
+ let runtime = await ensureRuntime(pi, state, ctx);
628
890
  await runtime.syncModel(getDesiredJarvisModel(state), getDesiredJarvisThinkingLevel(state));
629
891
  while (state.queuedMessages.length > 0) {
630
892
  const message = state.queuedMessages[0];
893
+ const command = parseJarvisSideCommand(message);
631
894
  try {
632
- await runtime.sendMessage(message);
895
+ if (command) {
896
+ runtime = await executeJarvisSideCommand(pi, state, ctx, runtime, command);
897
+ }
898
+ else {
899
+ await runtime.sendMessage(message);
900
+ state.lastJarvisSeenMainContext = state.mainContext;
901
+ }
633
902
  state.queuedMessages.shift();
634
- state.lastJarvisSeenMainContext = state.mainContext;
635
903
  }
636
904
  catch (error) {
637
- state.bridge.notify(`Failed to send /jarvis message: ${error instanceof Error ? error.message : String(error)}`, "error");
905
+ const action = command ? `run ${message.split(/\s+/, 1)[0]}` : "send /jarvis message";
906
+ state.bridge.notify(`Failed to ${action}: ${error instanceof Error ? error.message : String(error)}`, "error");
638
907
  break;
639
908
  }
640
909
  }