pi-jarvis 1.1.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marc Mironescu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # pi-jarvis
2
+
3
+ `pi-jarvis` adds a polished `/jarvis` side-conversation overlay to Pi so you can ask for help without derailing the main session.
4
+
5
+ ## Why use it
6
+
7
+ `/jarvis` gives you a second lane for quick questions, triage, and local investigation while the main session keeps moving. Instead of interrupting the primary flow, it opens a separate persistent side thread with explicit controls for tool access and main-session handoff.
8
+
9
+ ## What you get
10
+
11
+ - A floating `/jarvis` overlay inside the current Pi session
12
+ - A separate side-session history that survives reopening the overlay
13
+ - Automatic follow-main model behavior, with optional `/jarvis-model` pinning
14
+ - Permission-gated local `read`, `bash`, `edit`, and `write` access, plus `mcp` when available
15
+ - Optional non-interrupting notes or confirmed redirects back to the main session
16
+ - A focused regression suite covering the risky runtime and overlay paths
17
+
18
+ ## Quick start
19
+
20
+ ### 1. Install the package
21
+
22
+ ```bash
23
+ npm install pi-jarvis
24
+ ```
25
+
26
+ ### 2. Register the extension entrypoint in Pi
27
+
28
+ Use the published entrypoint:
29
+
30
+ ```text
31
+ ./dist/index.js
32
+ ```
33
+
34
+ This package is intended to run inside a Pi installation that provides the required peer dependencies.
35
+
36
+ ### 3. Open `/jarvis`
37
+
38
+ ```bash
39
+ /jarvis
40
+ ```
41
+
42
+ You can also send the first message immediately:
43
+
44
+ ```bash
45
+ /jarvis check this file for obvious regressions
46
+ ```
47
+
48
+ Or use it as a background helper while continuing the main flow:
49
+
50
+ ```bash
51
+ /jarvis summarize last 20 lines of build output and suggest next action
52
+ ```
53
+
54
+ ## Commands
55
+
56
+ ### `/jarvis`
57
+ Opens the side overlay. If you pass text after the command, that text becomes the first side-session prompt.
58
+
59
+ ### `/jarvis-model <provider/model>`
60
+ Pins `/jarvis` to a specific model without changing the main session model.
61
+
62
+ ### `/jarvis-model follow-main`
63
+ Returns `/jarvis` to the default mode where it follows the current main model.
64
+
65
+ ## Overlay behavior
66
+
67
+ The overlay is designed to stay explicit about what `/jarvis` can do right now. It surfaces:
68
+
69
+ - the current main-session state
70
+ - the active `/jarvis` model and mode
71
+ - what changed since the last `/jarvis` turn
72
+ - whether local tools are off, enabled, or enabled without MCP
73
+
74
+ The main header controls are:
75
+
76
+ - `Repo tools`
77
+ - `Note main`
78
+ - `Redirect`
79
+
80
+ All three are off by default.
81
+
82
+ ## Permission model
83
+
84
+ ### Repo tools
85
+ When enabled, `/jarvis` may use local:
86
+
87
+ - `read`
88
+ - `bash`
89
+ - `edit`
90
+ - `write`
91
+ - `mcp` when the MCP adapter is available in the current Pi environment
92
+
93
+ When disabled, `/jarvis` works from injected session context and the bridge controls only.
94
+
95
+ ### Note main
96
+ Allows `/jarvis` to send a concise, non-interrupting note back to the main session.
97
+
98
+ ### Redirect
99
+ Allows `/jarvis` to send a redirecting instruction to the main session, but every actual redirect still requires explicit confirmation.
100
+
101
+ ## Session model
102
+
103
+ - `/jarvis` keeps its own isolated conversation state
104
+ - prior side-session history is restored from a session file under `jarvis-sessions/`
105
+ - `/jarvis` follows the main model by default
106
+ - thinking-step streaming is collapsed to a cleaner animated fallback for readability
107
+
108
+ ## Development
109
+
110
+ Install dependencies:
111
+
112
+ ```bash
113
+ npm install
114
+ ```
115
+
116
+ Type-check:
117
+
118
+ ```bash
119
+ npm run check
120
+ ```
121
+
122
+ Run tests:
123
+
124
+ ```bash
125
+ npm test
126
+ ```
127
+
128
+ Build the published package contents:
129
+
130
+ ```bash
131
+ npm run build
132
+ ```
133
+
134
+ ## Published package contents
135
+
136
+ The npm package publishes:
137
+
138
+ - `dist/`
139
+ - `README.md`
140
+ - `LICENSE`
141
+ - `package.json`
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,3 @@
1
+ import { type ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export default function jarvisExtension(pi: ExtensionAPI): void;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAEA,OAAO,EAGN,KAAK,YAAY,EAGjB,MAAM,+BAA+B,CAAC;AA+BvC,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAyP9D"}
package/dist/index.js ADDED
@@ -0,0 +1,508 @@
1
+ import { existsSync } from "node:fs";
2
+ import { ModelSelectorComponent, SettingsManager, } from "@mariozechner/pi-coding-agent";
3
+ import { buildMainSessionContext } from "./main-context.js";
4
+ import { MainSessionTracker } from "./main-session-state.js";
5
+ import { attachOverlayBridge, JarvisOverlayBridge, JarvisOverlayComponent } from "./overlay.js";
6
+ import { createJarvisSessionRef, readJarvisSessionRef, JARVIS_SESSION_REF_CUSTOM_TYPE } from "./session-ref.js";
7
+ import { JarvisSideSessionRuntime, createSideSessionFile } from "./side-session.js";
8
+ export default function jarvisExtension(pi) {
9
+ const mainSession = new MainSessionTracker();
10
+ const state = {
11
+ bridge: new JarvisOverlayBridge(),
12
+ mainSession,
13
+ mainContext: buildMainSessionContext(mainSession.snapshot()),
14
+ lastJarvisSeenMainContext: undefined,
15
+ queuedMessages: [],
16
+ jarvisModelSelection: { mode: "follow-main" },
17
+ systemPrompt: "",
18
+ themeProvider: () => {
19
+ throw new Error("/jarvis theme requested before UI was available.");
20
+ },
21
+ allowSideTools: false,
22
+ allowFollowUpToMain: false,
23
+ allowSteerToMain: false,
24
+ };
25
+ pi.registerCommand("jarvis", {
26
+ description: "Open the /jarvis side conversation overlay",
27
+ handler: async (args, ctx) => {
28
+ updateContextState(pi, state, ctx);
29
+ void ensureRuntime(pi, state, ctx).catch((error) => {
30
+ state.bridge.notify(error instanceof Error ? error.message : String(error), "error");
31
+ });
32
+ const initialMessage = normalizeInitialMessage(args);
33
+ if (initialMessage) {
34
+ queueMessage(state, initialMessage);
35
+ void flushQueuedMessages(pi, state, ctx);
36
+ }
37
+ const overlayView = createOverlayView(pi, state, ctx);
38
+ await ctx.ui.custom((tui, theme, _keybindings, done) => {
39
+ state.themeProvider = () => theme;
40
+ const closeOverlay = () => {
41
+ done(undefined);
42
+ queueMicrotask(() => tui.requestRender());
43
+ };
44
+ return attachOverlayBridge(new JarvisOverlayComponent(tui, theme, state.bridge, overlayView, closeOverlay), state.bridge, tui);
45
+ }, {
46
+ overlay: true,
47
+ overlayOptions: {
48
+ width: "68%",
49
+ minWidth: 68,
50
+ maxHeight: "82%",
51
+ anchor: "center",
52
+ },
53
+ });
54
+ },
55
+ });
56
+ pi.registerCommand("jarvis-model", {
57
+ description: "Set the model used by /jarvis without changing the main agent model",
58
+ handler: async (args, ctx) => {
59
+ updateContextState(pi, state, ctx);
60
+ const request = args.trim();
61
+ const selectModelFromMenu = async (initialSearchInput) => {
62
+ const selectorSettingsManager = SettingsManager.inMemory();
63
+ return ctx.ui.custom((tui, _theme, _keybindings, done) => new ModelSelectorComponent(tui, getDesiredJarvisModel(state), selectorSettingsManager, ctx.modelRegistry, [], (model) => done(model), () => done(undefined), initialSearchInput));
64
+ };
65
+ const pinSelectedModel = async (model) => {
66
+ try {
67
+ await applyJarvisModelSelection(state, { mode: "pinned", model });
68
+ }
69
+ catch (error) {
70
+ ctx.ui.notify(`Failed to pin /jarvis to ${formatModelLabel(model)}: ${error instanceof Error ? error.message : String(error)}`, "error");
71
+ return;
72
+ }
73
+ ctx.ui.notify(`Pinned /jarvis to ${formatModelLabel(model)}. The main model is still ${formatModelLabel(state.model)}.`, "info");
74
+ };
75
+ if (request.toLowerCase() === "follow-main") {
76
+ try {
77
+ await applyJarvisModelSelection(state, { mode: "follow-main" });
78
+ }
79
+ catch (error) {
80
+ ctx.ui.notify(`Failed to switch /jarvis back to follow-main: ${error instanceof Error ? error.message : String(error)}`, "error");
81
+ return;
82
+ }
83
+ ctx.ui.notify(`Set /jarvis to follow the main model (${formatModelLabel(state.model)}).`, "info");
84
+ return;
85
+ }
86
+ if (!request) {
87
+ if (!ctx.hasUI) {
88
+ ctx.ui.notify(`/jarvis is ${describeJarvisModelSelection(state)}. Use /jarvis-model follow-main or /jarvis-model <provider/model>.`, "info");
89
+ return;
90
+ }
91
+ const selectedModel = await selectModelFromMenu();
92
+ if (!selectedModel) {
93
+ return;
94
+ }
95
+ await pinSelectedModel(selectedModel);
96
+ return;
97
+ }
98
+ const availableModels = getAvailableJarvisModels(ctx.modelRegistry);
99
+ const exactModel = findExactAvailableModelMatch(request, availableModels);
100
+ if (exactModel) {
101
+ await pinSelectedModel(exactModel);
102
+ return;
103
+ }
104
+ if (!ctx.hasUI) {
105
+ const errorMessage = availableModels.length === 0
106
+ ? "No /jarvis models are currently available from the main model registry."
107
+ : `Unknown /jarvis model "${request}". Use /jarvis-model follow-main or an exact provider/model from the current model registry.`;
108
+ ctx.ui.notify(errorMessage, "error");
109
+ return;
110
+ }
111
+ const selectedModel = await selectModelFromMenu(request);
112
+ if (!selectedModel) {
113
+ return;
114
+ }
115
+ await pinSelectedModel(selectedModel);
116
+ },
117
+ });
118
+ pi.on("session_start", async (_event, ctx) => {
119
+ state.runtime?.dispose();
120
+ state.runtime = undefined;
121
+ state.bootPromise = undefined;
122
+ state.flushPromise = undefined;
123
+ state.queuedMessages = [];
124
+ state.lastJarvisSeenMainContext = undefined;
125
+ state.jarvisModelSelection = { mode: "follow-main" };
126
+ state.allowSideTools = false;
127
+ state.allowFollowUpToMain = false;
128
+ state.allowSteerToMain = false;
129
+ state.mainSession.reset(formatModelLabel(ctx.model));
130
+ updateContextState(pi, state, ctx);
131
+ enforceCompatibilityGuard(state);
132
+ state.sessionRef = readJarvisSessionRef(ctx.sessionManager.getBranch());
133
+ state.bridge.reset();
134
+ });
135
+ pi.on("agent_start", async (_event, ctx) => {
136
+ updateContextState(pi, state, ctx);
137
+ state.mainSession.handleAgentStart();
138
+ refreshMainContext(state);
139
+ });
140
+ pi.on("agent_end", async (_event, ctx) => {
141
+ updateContextState(pi, state, ctx);
142
+ state.mainSession.handleAgentEnd();
143
+ refreshMainContext(state);
144
+ });
145
+ pi.on("message_start", async (event, ctx) => {
146
+ updateContextState(pi, state, ctx);
147
+ state.mainSession.handleMessageStart(event);
148
+ refreshMainContext(state);
149
+ });
150
+ pi.on("message_update", async (event, ctx) => {
151
+ updateContextState(pi, state, ctx);
152
+ state.mainSession.handleMessageUpdate(event);
153
+ refreshMainContext(state);
154
+ });
155
+ pi.on("message_end", async (event, ctx) => {
156
+ updateContextState(pi, state, ctx);
157
+ state.mainSession.handleMessageEnd(event);
158
+ refreshMainContext(state);
159
+ });
160
+ pi.on("tool_execution_start", async (event, ctx) => {
161
+ updateContextState(pi, state, ctx);
162
+ state.mainSession.handleToolExecutionStart(event);
163
+ refreshMainContext(state);
164
+ });
165
+ pi.on("tool_execution_end", async (event, ctx) => {
166
+ updateContextState(pi, state, ctx);
167
+ state.mainSession.handleToolExecutionEnd(event);
168
+ refreshMainContext(state);
169
+ });
170
+ pi.on("model_select", async (event, ctx) => {
171
+ updateContextState(pi, state, ctx);
172
+ state.model = event.model;
173
+ state.mainSession.handleModelSelect(formatModelLabel(event.model));
174
+ refreshMainContext(state);
175
+ enforceCompatibilityGuard(state);
176
+ if (!state.runtime || state.jarvisModelSelection.mode !== "follow-main") {
177
+ return;
178
+ }
179
+ try {
180
+ await syncRuntimeModelSelection(state);
181
+ }
182
+ catch (error) {
183
+ state.bridge.notify(`Failed to sync /jarvis model: ${error instanceof Error ? error.message : String(error)}`, "error");
184
+ }
185
+ });
186
+ pi.on("session_shutdown", async () => {
187
+ state.runtime?.dispose();
188
+ state.runtime = undefined;
189
+ state.bootPromise = undefined;
190
+ state.flushPromise = undefined;
191
+ state.queuedMessages = [];
192
+ state.lastJarvisSeenMainContext = undefined;
193
+ state.jarvisModelSelection = { mode: "follow-main" };
194
+ state.allowSideTools = false;
195
+ state.allowFollowUpToMain = false;
196
+ state.allowSteerToMain = false;
197
+ state.mainSession.reset(formatModelLabel(state.model));
198
+ refreshMainContext(state);
199
+ // Cancel any pending confirmation and clear transient overlay state so a
200
+ // side-session tool execute still waiting on confirmSteerToMain does not
201
+ // hang while the session is torn down.
202
+ state.bridge.reset();
203
+ });
204
+ }
205
+ function updateContextState(pi, state, ctx) {
206
+ state.model = ctx.model;
207
+ state.thinkingLevel = pi.getThinkingLevel();
208
+ state.systemPrompt = ctx.getSystemPrompt();
209
+ state.themeProvider = () => ctx.ui.theme;
210
+ state.mainSession.refreshFromContext(ctx, formatModelLabel(state.model));
211
+ refreshMainContext(state);
212
+ }
213
+ function refreshMainContext(state) {
214
+ state.mainContext = buildMainSessionContext(state.mainSession.snapshot());
215
+ }
216
+ function getDesiredJarvisModel(state) {
217
+ return state.jarvisModelSelection.mode === "pinned" ? state.jarvisModelSelection.model : state.model;
218
+ }
219
+ function getDesiredJarvisThinkingLevel(state) {
220
+ const desiredModel = getDesiredJarvisModel(state);
221
+ if (desiredModel?.provider === "xai") {
222
+ return "off";
223
+ }
224
+ return state.jarvisModelSelection.mode === "follow-main" ? state.thinkingLevel : "off";
225
+ }
226
+ function isModelBridgeCompatible(model) {
227
+ if (!model) {
228
+ return true;
229
+ }
230
+ if (model.provider === "xai" && model.id.includes("multi-agent")) {
231
+ return false;
232
+ }
233
+ return true;
234
+ }
235
+ function enforceCompatibilityGuard(state) {
236
+ const desired = getDesiredJarvisModel(state);
237
+ if (!isModelBridgeCompatible(desired)) {
238
+ state.allowFollowUpToMain = false;
239
+ state.allowSteerToMain = false;
240
+ state.bridge.notify(`Disabled Follow-up/Steer: ${formatModelLabel(desired)} does not support bridge tools.`, "warning");
241
+ }
242
+ }
243
+ function getJarvisModelModeLabel(selection) {
244
+ return selection.mode === "follow-main" ? "follow main" : "pinned";
245
+ }
246
+ function describeJarvisModelSelection(state) {
247
+ const activeModelLabel = formatModelLabel(getDesiredJarvisModel(state));
248
+ return state.jarvisModelSelection.mode === "follow-main"
249
+ ? `following the main model (${activeModelLabel})`
250
+ : `pinned to ${activeModelLabel}`;
251
+ }
252
+ function getAvailableJarvisModels(modelRegistry) {
253
+ modelRegistry.refresh();
254
+ try {
255
+ return modelRegistry.getAvailable();
256
+ }
257
+ catch {
258
+ return [];
259
+ }
260
+ }
261
+ function findExactAvailableModelMatch(modelReference, availableModels) {
262
+ const trimmedReference = modelReference.trim();
263
+ if (!trimmedReference) {
264
+ return undefined;
265
+ }
266
+ const normalizedReference = trimmedReference.toLowerCase();
267
+ const canonicalMatches = availableModels.filter((model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference);
268
+ if (canonicalMatches.length === 1) {
269
+ return canonicalMatches[0];
270
+ }
271
+ if (canonicalMatches.length > 1) {
272
+ return undefined;
273
+ }
274
+ const slashIndex = trimmedReference.indexOf("/");
275
+ if (slashIndex !== -1) {
276
+ const provider = trimmedReference.substring(0, slashIndex).trim();
277
+ const modelId = trimmedReference.substring(slashIndex + 1).trim();
278
+ if (provider && modelId) {
279
+ const providerMatches = availableModels.filter((model) => model.provider.toLowerCase() === provider.toLowerCase() &&
280
+ model.id.toLowerCase() === modelId.toLowerCase());
281
+ if (providerMatches.length === 1) {
282
+ return providerMatches[0];
283
+ }
284
+ if (providerMatches.length > 1) {
285
+ return undefined;
286
+ }
287
+ }
288
+ }
289
+ const idMatches = availableModels.filter((model) => model.id.toLowerCase() === normalizedReference);
290
+ return idMatches.length === 1 ? idMatches[0] : undefined;
291
+ }
292
+ async function syncRuntimeModelSelection(state) {
293
+ if (!state.runtime) {
294
+ return;
295
+ }
296
+ await state.runtime.syncModel(getDesiredJarvisModel(state), getDesiredJarvisThinkingLevel(state));
297
+ }
298
+ async function applyJarvisModelSelection(state, selection) {
299
+ const previousSelection = state.jarvisModelSelection;
300
+ state.jarvisModelSelection = selection;
301
+ try {
302
+ await syncRuntimeModelSelection(state);
303
+ enforceCompatibilityGuard(state);
304
+ }
305
+ catch (error) {
306
+ state.jarvisModelSelection = previousSelection;
307
+ throw error;
308
+ }
309
+ }
310
+ function normalizeInitialMessage(args) {
311
+ const message = args.trim();
312
+ return message.length > 0 ? message : undefined;
313
+ }
314
+ function queueMessage(state, message) {
315
+ state.queuedMessages.push(message);
316
+ }
317
+ function createOverlayView(pi, state, ctx) {
318
+ const syncToolAccess = () => {
319
+ state.runtime?.setToolAccessEnabled(state.allowSideTools);
320
+ };
321
+ return {
322
+ isReady: () => state.runtime?.isReady() ?? false,
323
+ isStreaming: () => state.runtime?.isStreaming() ?? false,
324
+ getModelLabel: () => state.runtime?.getModelLabel() ?? formatModelLabel(getDesiredJarvisModel(state)),
325
+ getModelModeLabel: () => getJarvisModelModeLabel(state.jarvisModelSelection),
326
+ getMainStatusLabel: () => state.mainContext.summary.mainStatus,
327
+ getMainModelLabel: () => state.mainContext.summary.mainModelLabel,
328
+ getMainFocusLabel: () => state.mainContext.summary.workState.currentAction,
329
+ getMainDeltaLabel: () => formatMainContextDeltaLabel(state.lastJarvisSeenMainContext, state.mainContext),
330
+ getRepoToolsDetailLabel: () => state.runtime?.getRepoToolsDetailLabel() ?? (state.allowSideTools ? "local tools only" : "repo tools off"),
331
+ isToolAccessEnabled: () => state.allowSideTools,
332
+ isFollowUpToMainEnabled: () => state.allowFollowUpToMain,
333
+ isSteerToMainEnabled: () => state.allowSteerToMain,
334
+ toggleToolAccess: () => {
335
+ state.allowSideTools = !state.allowSideTools;
336
+ syncToolAccess();
337
+ },
338
+ toggleFollowUpToMain: () => {
339
+ if (!isModelBridgeCompatible(getDesiredJarvisModel(state))) {
340
+ state.bridge.notify("Follow-up is not supported by the current /jarvis model.", "warning");
341
+ return;
342
+ }
343
+ state.allowFollowUpToMain = !state.allowFollowUpToMain;
344
+ syncToolAccess();
345
+ },
346
+ toggleSteerToMain: () => {
347
+ if (!isModelBridgeCompatible(getDesiredJarvisModel(state))) {
348
+ state.bridge.notify("Steer is not supported by the current /jarvis model.", "warning");
349
+ return;
350
+ }
351
+ state.allowSteerToMain = !state.allowSteerToMain;
352
+ syncToolAccess();
353
+ },
354
+ getDisplayEntries: () => getOverlayEntries(state),
355
+ sendMessage: async (text) => {
356
+ queueMessage(state, text);
357
+ await flushQueuedMessages(pi, state, ctx);
358
+ },
359
+ };
360
+ }
361
+ function getOverlayEntries(state) {
362
+ let entries;
363
+ if (state.runtime) {
364
+ entries = state.runtime.getDisplayEntries();
365
+ }
366
+ else {
367
+ const hasRestorableSessionRef = Boolean(state.sessionRef?.file && existsSync(state.sessionRef.file));
368
+ entries = [
369
+ {
370
+ kind: "system",
371
+ text: hasRestorableSessionRef
372
+ ? "Connecting to your prior /jarvis conversation…"
373
+ : "Starting /jarvis side conversation…",
374
+ },
375
+ ];
376
+ if (state.queuedMessages.length > 0) {
377
+ entries.push({ kind: "status", text: `Queued ${state.queuedMessages.length} message${state.queuedMessages.length === 1 ? "" : "s"}…` });
378
+ }
379
+ }
380
+ entries.push({ kind: "status", text: `Since last /jarvis turn: ${formatMainContextDeltaLabel(state.lastJarvisSeenMainContext, state.mainContext)}` });
381
+ entries.push({ kind: "status", text: `Repo tools: ${state.runtime?.getRepoToolsDetailLabel() ?? (state.allowSideTools ? "local tools only" : "repo tools off")}` });
382
+ if (!isModelBridgeCompatible(getDesiredJarvisModel(state))) {
383
+ entries.push({ kind: "status", text: "Relay disabled: current /jarvis model is incompatible with bridge tools" });
384
+ }
385
+ return entries;
386
+ }
387
+ function formatMainContextDeltaLabel(previousContext, currentContext) {
388
+ if (!previousContext) {
389
+ return "first /jarvis turn";
390
+ }
391
+ if (currentContext.summary.workState.currentAction !== previousContext.summary.workState.currentAction) {
392
+ return `focus → ${currentContext.summary.workState.currentAction}`;
393
+ }
394
+ if (currentContext.summary.validation.summary !== previousContext.summary.validation.summary) {
395
+ return `validation → ${currentContext.summary.validation.summary}`;
396
+ }
397
+ if (currentContext.summary.mainStatus !== previousContext.summary.mainStatus) {
398
+ return `main status → ${currentContext.summary.mainStatus}`;
399
+ }
400
+ if (currentContext.summary.mainModelLabel !== previousContext.summary.mainModelLabel) {
401
+ return `model → ${currentContext.summary.mainModelLabel}`;
402
+ }
403
+ const previousFiles = new Set(previousContext.summary.workState.recentFiles);
404
+ const newFiles = currentContext.summary.workState.recentFiles.filter((file) => !previousFiles.has(file));
405
+ if (newFiles.length > 0) {
406
+ return `new files → ${newFiles.join(", ")}`;
407
+ }
408
+ if (currentContext.summary.latestUserRequest && currentContext.summary.latestUserRequest !== previousContext.summary.latestUserRequest) {
409
+ return `request → ${currentContext.summary.latestUserRequest}`;
410
+ }
411
+ return "no significant change";
412
+ }
413
+ async function flushQueuedMessages(pi, state, ctx) {
414
+ updateContextState(pi, state, ctx);
415
+ if (state.flushPromise) {
416
+ return state.flushPromise;
417
+ }
418
+ // flushQueuedMessages is awaited in fire-and-forget paths (the /jarvis command
419
+ // handler's initial message flush and the overlay input submit). Any error
420
+ // that propagates out of here becomes an unhandled promise rejection, so the
421
+ // IIFE must catch and surface every failure via bridge.notify rather than
422
+ // rethrowing.
423
+ state.flushPromise = (async () => {
424
+ try {
425
+ const runtime = await ensureRuntime(pi, state, ctx);
426
+ await runtime.syncModel(getDesiredJarvisModel(state), getDesiredJarvisThinkingLevel(state));
427
+ while (state.queuedMessages.length > 0) {
428
+ const message = state.queuedMessages.shift();
429
+ try {
430
+ await runtime.sendMessage(message);
431
+ state.lastJarvisSeenMainContext = state.mainContext;
432
+ }
433
+ catch (error) {
434
+ state.bridge.notify(`Failed to send /jarvis message: ${error instanceof Error ? error.message : String(error)}`, "error");
435
+ break;
436
+ }
437
+ }
438
+ }
439
+ catch (error) {
440
+ state.bridge.notify(`/jarvis startup failed: ${error instanceof Error ? error.message : String(error)}`, "error");
441
+ }
442
+ })().finally(() => {
443
+ state.flushPromise = undefined;
444
+ });
445
+ return state.flushPromise;
446
+ }
447
+ async function ensureRuntime(pi, state, ctx) {
448
+ if (state.runtime) {
449
+ return state.runtime;
450
+ }
451
+ if (state.bootPromise) {
452
+ return state.bootPromise;
453
+ }
454
+ state.bridge.setWorkingMessage("Starting /jarvis…");
455
+ state.bootPromise = (async () => {
456
+ let sessionFile = state.sessionRef?.file;
457
+ if (!sessionFile || !existsSync(sessionFile)) {
458
+ sessionFile = await createSideSessionFile(ctx.cwd);
459
+ state.sessionRef = createJarvisSessionRef(sessionFile);
460
+ pi.appendEntry(JARVIS_SESSION_REF_CUSTOM_TYPE, state.sessionRef);
461
+ }
462
+ const runtime = await JarvisSideSessionRuntime.create({
463
+ bridge: state.bridge,
464
+ cwd: ctx.cwd,
465
+ modelRegistry: ctx.modelRegistry,
466
+ model: getDesiredJarvisModel(state),
467
+ jarvisModelModeProvider: () => state.jarvisModelSelection.mode,
468
+ thinkingLevel: getDesiredJarvisThinkingLevel(state),
469
+ sessionFile,
470
+ systemPromptProvider: () => state.systemPrompt,
471
+ mainContextProvider: () => state.mainContext,
472
+ toolAccessProvider: () => state.allowSideTools,
473
+ communicationPermissionsProvider: () => ({
474
+ allowFollowUpToMain: state.allowFollowUpToMain,
475
+ allowSteerToMain: state.allowSteerToMain,
476
+ }),
477
+ sendFollowUpToMain: (message) => {
478
+ pi.sendUserMessage(message, { deliverAs: "followUp" });
479
+ },
480
+ // Route the confirmation through the /jarvis overlay itself via the
481
+ // bridge. The main session's UI confirm would otherwise render inside
482
+ // the base layer (the pi editor container) and sit hidden behind the
483
+ // /jarvis overlay, leaving the side-session tool execute hung on a
484
+ // promise the user could never answer.
485
+ confirmSteerToMain: (message) => state.bridge.requestConfirmation("Send /jarvis steer to main?", `This will steer the main agent with:\n\n${message}`),
486
+ sendSteerToMain: (message) => {
487
+ pi.sendUserMessage(message, { deliverAs: "steer" });
488
+ },
489
+ themeProvider: state.themeProvider,
490
+ });
491
+ state.runtime = runtime;
492
+ state.bridge.setWorkingMessage(undefined);
493
+ return runtime;
494
+ })().catch((error) => {
495
+ state.bridge.setWorkingMessage(undefined);
496
+ throw error;
497
+ }).finally(() => {
498
+ state.bootPromise = undefined;
499
+ });
500
+ return state.bootPromise;
501
+ }
502
+ function formatModelLabel(model) {
503
+ if (!model) {
504
+ return "model unavailable";
505
+ }
506
+ return `${model.provider}/${model.id}`;
507
+ }
508
+ //# sourceMappingURL=index.js.map