pi-jarvis 1.3.2 → 1.4.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/AGENTS.md +12 -5
- package/README.md +26 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +175 -75
- package/dist/index.js.map +1 -1
- package/dist/jarvis-config.d.ts +3 -1
- package/dist/jarvis-config.d.ts.map +1 -1
- package/dist/jarvis-config.js +75 -25
- package/dist/jarvis-config.js.map +1 -1
- package/dist/main-context.d.ts.map +1 -1
- package/dist/main-context.js +70 -69
- package/dist/main-context.js.map +1 -1
- package/dist/main-session-state.d.ts +3 -1
- package/dist/main-session-state.d.ts.map +1 -1
- package/dist/main-session-state.js +25 -23
- package/dist/main-session-state.js.map +1 -1
- package/dist/model-picker.d.ts +23 -0
- package/dist/model-picker.d.ts.map +1 -0
- package/dist/model-picker.js +93 -0
- package/dist/model-picker.js.map +1 -0
- package/dist/overlay.d.ts +24 -6
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +244 -109
- package/dist/overlay.js.map +1 -1
- package/dist/side-session.d.ts +18 -5
- package/dist/side-session.d.ts.map +1 -1
- package/dist/side-session.js +230 -47
- package/dist/side-session.js.map +1 -1
- package/package.json +17 -19
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { JarvisModelPicker } from "./model-picker.js";
|
|
3
3
|
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, clearJarvisThinkingSelectionSetting, loadJarvisModelSelectionSetting, loadJarvisThinkingSelectionSetting, saveJarvisModelSelectionSetting, saveJarvisThinkingSelectionSetting, } from "./jarvis-config.js";
|
|
7
|
+
import { MalformedJarvisConfigError, 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() {
|
|
@@ -39,7 +39,16 @@ export default function jarvisExtension(pi) {
|
|
|
39
39
|
pi.registerCommand("jarvis", {
|
|
40
40
|
description: "Open the /jarvis side conversation overlay",
|
|
41
41
|
handler: async (args, ctx) => {
|
|
42
|
+
if (ctx.mode !== "tui") {
|
|
43
|
+
ctx.ui.notify("/jarvis requires Pi's interactive terminal UI.", "warning");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (state.overlayOpen) {
|
|
47
|
+
ctx.ui.notify("/jarvis is already open.", "info");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
42
50
|
updateContextState(pi, state, ctx);
|
|
51
|
+
state.overlayOpen = true;
|
|
43
52
|
void ensureRuntime(pi, state, ctx).catch((error) => {
|
|
44
53
|
if (isStaleJarvisBootError(error)) {
|
|
45
54
|
return;
|
|
@@ -52,23 +61,37 @@ export default function jarvisExtension(pi) {
|
|
|
52
61
|
void flushQueuedMessages(pi, state, ctx);
|
|
53
62
|
}
|
|
54
63
|
const overlayView = createOverlayView(pi, state, ctx);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
},
|
|
71
|
-
|
|
64
|
+
try {
|
|
65
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => {
|
|
66
|
+
state.themeProvider = () => theme;
|
|
67
|
+
let closed = false;
|
|
68
|
+
const closeOverlay = () => {
|
|
69
|
+
if (closed)
|
|
70
|
+
return;
|
|
71
|
+
closed = true;
|
|
72
|
+
resetTransientAccessControls(state);
|
|
73
|
+
state.bridge.resolveConfirmation(false);
|
|
74
|
+
done(undefined);
|
|
75
|
+
queueMicrotask(() => tui.requestRender());
|
|
76
|
+
};
|
|
77
|
+
state.closeOverlay = closeOverlay;
|
|
78
|
+
return attachOverlayBridge(new JarvisOverlayComponent(tui, theme, state.bridge, overlayView, closeOverlay, keybindings), state.bridge, tui);
|
|
79
|
+
}, {
|
|
80
|
+
overlay: true,
|
|
81
|
+
overlayOptions: {
|
|
82
|
+
width: "68%",
|
|
83
|
+
minWidth: 68,
|
|
84
|
+
maxHeight: "82%",
|
|
85
|
+
anchor: "center",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
state.overlayOpen = false;
|
|
91
|
+
state.closeOverlay = undefined;
|
|
92
|
+
resetTransientAccessControls(state);
|
|
93
|
+
state.bridge.resolveConfirmation(false);
|
|
94
|
+
}
|
|
72
95
|
},
|
|
73
96
|
});
|
|
74
97
|
pi.registerCommand("jarvis-model", {
|
|
@@ -85,9 +108,22 @@ export default function jarvisExtension(pi) {
|
|
|
85
108
|
}
|
|
86
109
|
const { request, scope, clearScope } = parsedCommand;
|
|
87
110
|
const scopeLabel = formatJarvisModelSelectionScope(scope);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
const loadModels = async () => {
|
|
112
|
+
try {
|
|
113
|
+
return await getAvailableJarvisModels(ctx.modelRegistry);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
ctx.ui.notify(`Failed to refresh /jarvis models: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const selectModelFromMenu = async (models, initialSearchInput) => {
|
|
121
|
+
models = models.filter((model) => model.api !== "pi-virtual");
|
|
122
|
+
if (models.length === 0) {
|
|
123
|
+
ctx.ui.notify("No /jarvis models are currently available from the main model registry.", "warning");
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return ctx.ui.custom((tui, theme, keybindings, done) => new JarvisModelPicker(tui, theme, keybindings, models, (model) => done(model), () => done(undefined), initialSearchInput));
|
|
91
127
|
};
|
|
92
128
|
const rollbackSelection = async (previousSelection, previousSource) => {
|
|
93
129
|
await applyJarvisModelSelection(state, previousSelection);
|
|
@@ -96,10 +132,12 @@ export default function jarvisExtension(pi) {
|
|
|
96
132
|
const persistSelection = async (selection) => {
|
|
97
133
|
const previousSelection = state.jarvisModelSelection;
|
|
98
134
|
const previousSource = state.jarvisModelSelectionSource;
|
|
99
|
-
|
|
135
|
+
const stored = toStoredJarvisModelSelection(selection);
|
|
136
|
+
const resolved = resolveJarvisModelSelectionFromSettings(scope === "project" ? stored : loadJarvisModelSelectionForClear(ctx.cwd, "project", ctx), scope === "global" ? stored : loadJarvisModelSelectionForClear(ctx.cwd, "global", ctx), ctx.modelRegistry);
|
|
137
|
+
await applyJarvisModelSelection(state, resolved.selection);
|
|
100
138
|
try {
|
|
101
|
-
saveJarvisModelSelectionSetting(ctx.cwd, scope,
|
|
102
|
-
state.jarvisModelSelectionSource =
|
|
139
|
+
saveJarvisModelSelectionSetting(ctx.cwd, scope, stored);
|
|
140
|
+
state.jarvisModelSelectionSource = resolved.source;
|
|
103
141
|
}
|
|
104
142
|
catch (error) {
|
|
105
143
|
await rollbackSelection(previousSelection, previousSource);
|
|
@@ -131,7 +169,7 @@ export default function jarvisExtension(pi) {
|
|
|
131
169
|
ctx.ui.notify(`Failed to pin /jarvis to ${formatModelLabel(model)} ${scopeLabel}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
132
170
|
return;
|
|
133
171
|
}
|
|
134
|
-
ctx.ui.notify(`
|
|
172
|
+
ctx.ui.notify(`Saved /jarvis model ${formatModelLabel(model)} ${scopeLabel}. /jarvis is ${describeJarvisModelSelection(state)}. The main model is still ${formatModelLabel(state.model)}.`, "info");
|
|
135
173
|
};
|
|
136
174
|
if (clearScope) {
|
|
137
175
|
let resolvedSelection;
|
|
@@ -156,35 +194,35 @@ export default function jarvisExtension(pi) {
|
|
|
156
194
|
ctx.ui.notify(`Failed to switch /jarvis back to follow-main ${scopeLabel}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
157
195
|
return;
|
|
158
196
|
}
|
|
159
|
-
ctx.ui.notify(`
|
|
197
|
+
ctx.ui.notify(`Saved follow-main ${scopeLabel}. /jarvis is ${describeJarvisModelSelection(state)}.`, "info");
|
|
160
198
|
return;
|
|
161
199
|
}
|
|
162
200
|
if (!request) {
|
|
163
|
-
if (
|
|
201
|
+
if (ctx.mode !== "tui") {
|
|
164
202
|
ctx.ui.notify(`/jarvis is ${describeJarvisModelSelection(state)}. Use /jarvis-model [--project|--global] clear, /jarvis-model [--project|--global] follow-main, or /jarvis-model [--project|--global] <provider/model>.`, "info");
|
|
165
203
|
return;
|
|
166
204
|
}
|
|
167
|
-
const selectedModel = await selectModelFromMenu();
|
|
205
|
+
const selectedModel = await selectModelFromMenu(await loadModels());
|
|
168
206
|
if (!selectedModel) {
|
|
169
207
|
return;
|
|
170
208
|
}
|
|
171
209
|
await pinSelectedModel(selectedModel);
|
|
172
210
|
return;
|
|
173
211
|
}
|
|
174
|
-
const availableModels =
|
|
212
|
+
const availableModels = await loadModels();
|
|
175
213
|
const exactModel = findExactAvailableModelMatch(request, availableModels);
|
|
176
214
|
if (exactModel) {
|
|
177
215
|
await pinSelectedModel(exactModel);
|
|
178
216
|
return;
|
|
179
217
|
}
|
|
180
|
-
if (
|
|
218
|
+
if (ctx.mode !== "tui") {
|
|
181
219
|
const errorMessage = availableModels.length === 0
|
|
182
220
|
? "No /jarvis models are currently available from the main model registry."
|
|
183
221
|
: `Unknown /jarvis model "${request}". Use /jarvis-model [--project|--global] clear, /jarvis-model [--project|--global] follow-main, or an exact provider/model from the current model registry.`;
|
|
184
222
|
ctx.ui.notify(errorMessage, "error");
|
|
185
223
|
return;
|
|
186
224
|
}
|
|
187
|
-
const selectedModel = await selectModelFromMenu(request);
|
|
225
|
+
const selectedModel = await selectModelFromMenu(availableModels, request);
|
|
188
226
|
if (!selectedModel) {
|
|
189
227
|
return;
|
|
190
228
|
}
|
|
@@ -212,10 +250,11 @@ export default function jarvisExtension(pi) {
|
|
|
212
250
|
const persistSelection = async (selection) => {
|
|
213
251
|
const previousSelection = state.jarvisThinkingSelection;
|
|
214
252
|
const previousSource = state.jarvisThinkingSelectionSource;
|
|
215
|
-
|
|
253
|
+
const resolved = resolveJarvisThinkingSelectionFromSettings(scope === "project" ? selection : loadJarvisThinkingSelectionForClear(ctx.cwd, "project", ctx), scope === "global" ? selection : loadJarvisThinkingSelectionForClear(ctx.cwd, "global", ctx));
|
|
254
|
+
await applyJarvisThinkingSelection(state, resolved.selection);
|
|
216
255
|
try {
|
|
217
256
|
saveJarvisThinkingSelectionSetting(ctx.cwd, scope, selection);
|
|
218
|
-
state.jarvisThinkingSelectionSource =
|
|
257
|
+
state.jarvisThinkingSelectionSource = resolved.source;
|
|
219
258
|
}
|
|
220
259
|
catch (error) {
|
|
221
260
|
await rollbackSelection(previousSelection, previousSource);
|
|
@@ -262,7 +301,7 @@ export default function jarvisExtension(pi) {
|
|
|
262
301
|
selection = { mode: "pinned", thinkingLevel: normalizedRequest };
|
|
263
302
|
}
|
|
264
303
|
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
|
|
304
|
+
ctx.ui.notify(`/jarvis is ${describeJarvisThinkingSelection(state)}. Use /jarvis-thinking [--project|--global] clear, auto, follow-main, off, minimal, low, medium, high, xhigh, or max.`, request ? "error" : "info");
|
|
266
305
|
return;
|
|
267
306
|
}
|
|
268
307
|
try {
|
|
@@ -276,6 +315,7 @@ export default function jarvisExtension(pi) {
|
|
|
276
315
|
},
|
|
277
316
|
});
|
|
278
317
|
pi.on("session_start", async (_event, ctx) => {
|
|
318
|
+
state.closeOverlay?.();
|
|
279
319
|
state.bootGeneration += 1;
|
|
280
320
|
state.runtime?.dispose();
|
|
281
321
|
state.runtime = undefined;
|
|
@@ -324,6 +364,29 @@ export default function jarvisExtension(pi) {
|
|
|
324
364
|
state.mainSession.handleAgentEnd();
|
|
325
365
|
refreshMainContext(state);
|
|
326
366
|
});
|
|
367
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
368
|
+
updateContextState(pi, state, ctx);
|
|
369
|
+
state.mainSession.handleAgentSettled();
|
|
370
|
+
refreshMainContext(state);
|
|
371
|
+
});
|
|
372
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
373
|
+
const sessionRef = readJarvisSessionRef(ctx.sessionManager.getBranch());
|
|
374
|
+
if (sessionRef?.file !== state.sessionRef?.file) {
|
|
375
|
+
state.closeOverlay?.();
|
|
376
|
+
state.bootGeneration += 1;
|
|
377
|
+
state.runtime?.dispose();
|
|
378
|
+
state.runtime = undefined;
|
|
379
|
+
state.bootPromise = undefined;
|
|
380
|
+
state.flushPromise = undefined;
|
|
381
|
+
state.queuedMessages = [];
|
|
382
|
+
state.lastJarvisSeenMainContext = undefined;
|
|
383
|
+
state.sessionRef = sessionRef;
|
|
384
|
+
resetTransientAccessControls(state);
|
|
385
|
+
state.bridge.reset();
|
|
386
|
+
}
|
|
387
|
+
state.mainSession.reset(formatModelLabel(ctx.model));
|
|
388
|
+
updateContextState(pi, state, ctx);
|
|
389
|
+
});
|
|
327
390
|
pi.on("message_start", async (event, ctx) => {
|
|
328
391
|
updateContextState(pi, state, ctx);
|
|
329
392
|
state.mainSession.handleMessageStart(event);
|
|
@@ -366,7 +429,17 @@ export default function jarvisExtension(pi) {
|
|
|
366
429
|
state.bridge.notify(`Failed to sync /jarvis model: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
367
430
|
}
|
|
368
431
|
});
|
|
432
|
+
pi.on("thinking_level_select", async (_event, ctx) => {
|
|
433
|
+
updateContextState(pi, state, ctx);
|
|
434
|
+
try {
|
|
435
|
+
await syncRuntimeModelSelection(state);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
state.bridge.notify(`Failed to sync /jarvis thinking: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
439
|
+
}
|
|
440
|
+
});
|
|
369
441
|
pi.on("session_shutdown", async () => {
|
|
442
|
+
state.closeOverlay?.();
|
|
370
443
|
state.bootGeneration += 1;
|
|
371
444
|
state.runtime?.dispose();
|
|
372
445
|
state.runtime = undefined;
|
|
@@ -399,6 +472,7 @@ function updateContextState(pi, state, ctx) {
|
|
|
399
472
|
}
|
|
400
473
|
function refreshMainContext(state) {
|
|
401
474
|
state.mainContext = buildMainSessionContext(state.mainSession.snapshot());
|
|
475
|
+
state.bridge.refresh();
|
|
402
476
|
}
|
|
403
477
|
function resetTransientAccessControls(state) {
|
|
404
478
|
state.allowSideTools = false;
|
|
@@ -471,7 +545,7 @@ function parseJarvisThinkingCommand(args) {
|
|
|
471
545
|
};
|
|
472
546
|
}
|
|
473
547
|
function isJarvisThinkingLevel(value) {
|
|
474
|
-
return value === "off" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh";
|
|
548
|
+
return value === "off" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
|
|
475
549
|
}
|
|
476
550
|
function formatJarvisThinkingSelection(selection) {
|
|
477
551
|
if (selection.mode === "auto") {
|
|
@@ -582,7 +656,9 @@ function loadJarvisModelSelectionForClear(cwd, scope, ctx) {
|
|
|
582
656
|
return loadJarvisModelSelectionSetting(cwd, scope);
|
|
583
657
|
}
|
|
584
658
|
catch (error) {
|
|
585
|
-
|
|
659
|
+
if (!(error instanceof MalformedJarvisConfigError))
|
|
660
|
+
throw error;
|
|
661
|
+
ctx.ui.notify(`Ignoring malformed ${scope} /jarvis model setting while resolving settings: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
586
662
|
return undefined;
|
|
587
663
|
}
|
|
588
664
|
}
|
|
@@ -591,7 +667,9 @@ function loadJarvisThinkingSelectionForClear(cwd, scope, ctx) {
|
|
|
591
667
|
return loadJarvisThinkingSelectionSetting(cwd, scope);
|
|
592
668
|
}
|
|
593
669
|
catch (error) {
|
|
594
|
-
|
|
670
|
+
if (!(error instanceof MalformedJarvisConfigError))
|
|
671
|
+
throw error;
|
|
672
|
+
ctx.ui.notify(`Ignoring malformed ${scope} /jarvis thinking setting while resolving settings: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
595
673
|
return undefined;
|
|
596
674
|
}
|
|
597
675
|
}
|
|
@@ -623,14 +701,9 @@ function describeJarvisModelSelection(state) {
|
|
|
623
701
|
? `following the main model (${activeModelLabel}; ${sourceLabel})`
|
|
624
702
|
: `pinned to ${activeModelLabel} (${sourceLabel})`;
|
|
625
703
|
}
|
|
626
|
-
function getAvailableJarvisModels(modelRegistry) {
|
|
627
|
-
modelRegistry.refresh();
|
|
628
|
-
|
|
629
|
-
return modelRegistry.getAvailable();
|
|
630
|
-
}
|
|
631
|
-
catch {
|
|
632
|
-
return [];
|
|
633
|
-
}
|
|
704
|
+
async function getAvailableJarvisModels(modelRegistry) {
|
|
705
|
+
await modelRegistry.refresh();
|
|
706
|
+
return modelRegistry.getAvailable();
|
|
634
707
|
}
|
|
635
708
|
function findExactAvailableModelMatch(modelReference, availableModels) {
|
|
636
709
|
const trimmedReference = modelReference.trim();
|
|
@@ -670,6 +743,9 @@ async function syncRuntimeModelSelection(state) {
|
|
|
670
743
|
await state.runtime.syncModel(getDesiredJarvisModel(state), getDesiredJarvisThinkingLevel(state));
|
|
671
744
|
}
|
|
672
745
|
async function applyJarvisModelSelection(state, selection) {
|
|
746
|
+
if (selection.mode === "pinned" && selection.model.api === "pi-virtual") {
|
|
747
|
+
throw new Error("/jarvis requires a physical model; Pi's public registry does not expose virtual routing.");
|
|
748
|
+
}
|
|
673
749
|
const previousSelection = state.jarvisModelSelection;
|
|
674
750
|
state.jarvisModelSelection = selection;
|
|
675
751
|
try {
|
|
@@ -721,14 +797,16 @@ async function executeJarvisSideCommand(pi, state, ctx, runtime, command) {
|
|
|
721
797
|
});
|
|
722
798
|
return runtime;
|
|
723
799
|
}
|
|
724
|
-
state.bootGeneration
|
|
800
|
+
const generation = ++state.bootGeneration;
|
|
725
801
|
state.runtime?.dispose();
|
|
726
802
|
state.runtime = undefined;
|
|
727
803
|
state.bootPromise = undefined;
|
|
728
|
-
state.flushPromise = undefined;
|
|
729
804
|
state.lastJarvisSeenMainContext = undefined;
|
|
730
805
|
resetTransientAccessControls(state);
|
|
806
|
+
state.bridge.reset();
|
|
731
807
|
const sessionFile = await createSideSessionFile(ctx.cwd);
|
|
808
|
+
if (generation !== state.bootGeneration)
|
|
809
|
+
throw new StaleJarvisBootError();
|
|
732
810
|
const sessionRef = createJarvisSessionRef(sessionFile);
|
|
733
811
|
state.sessionRef = sessionRef;
|
|
734
812
|
pi.appendEntry(JARVIS_SESSION_REF_CUSTOM_TYPE, sessionRef);
|
|
@@ -759,6 +837,8 @@ function parseJarvisSideCommand(message) {
|
|
|
759
837
|
}
|
|
760
838
|
if (text.startsWith("/tree ")) {
|
|
761
839
|
const args = text.slice("/tree ".length).trim();
|
|
840
|
+
if (args === "--summarize")
|
|
841
|
+
throw new Error("Use /tree --summarize <entry-id> [instructions].");
|
|
762
842
|
const summarizePrefix = "--summarize ";
|
|
763
843
|
if (args.startsWith(summarizePrefix)) {
|
|
764
844
|
const rest = args.slice(summarizePrefix.length).trim();
|
|
@@ -776,6 +856,7 @@ function parseJarvisSideCommand(message) {
|
|
|
776
856
|
return undefined;
|
|
777
857
|
}
|
|
778
858
|
function createOverlayView(pi, state, ctx) {
|
|
859
|
+
const queue = state.queuedMessages;
|
|
779
860
|
const syncToolAccess = () => {
|
|
780
861
|
state.runtime?.setToolAccessEnabled(state.allowSideTools);
|
|
781
862
|
};
|
|
@@ -814,6 +895,8 @@ function createOverlayView(pi, state, ctx) {
|
|
|
814
895
|
},
|
|
815
896
|
getDisplayEntries: () => getOverlayEntries(state),
|
|
816
897
|
sendMessage: async (text) => {
|
|
898
|
+
if (state.queuedMessages !== queue)
|
|
899
|
+
return;
|
|
817
900
|
queueMessage(state, text);
|
|
818
901
|
await flushQueuedMessages(pi, state, ctx);
|
|
819
902
|
},
|
|
@@ -875,49 +958,59 @@ function formatMainContextDeltaLabel(previousContext, currentContext) {
|
|
|
875
958
|
return "no significant change";
|
|
876
959
|
}
|
|
877
960
|
async function flushQueuedMessages(pi, state, ctx) {
|
|
878
|
-
|
|
879
|
-
if (state.flushPromise) {
|
|
961
|
+
if (state.flushPromise)
|
|
880
962
|
return state.flushPromise;
|
|
881
|
-
|
|
882
|
-
//
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
state.flushPromise = (async () => {
|
|
963
|
+
// Queue identity survives a side /new, but not a main-session replacement.
|
|
964
|
+
// A stale completion must never consume or unlock a replacement queue.
|
|
965
|
+
const queue = state.queuedMessages;
|
|
966
|
+
const isCurrent = () => state.queuedMessages === queue;
|
|
967
|
+
let flushPromise;
|
|
968
|
+
flushPromise = (async () => {
|
|
888
969
|
try {
|
|
970
|
+
updateContextState(pi, state, ctx);
|
|
889
971
|
let runtime = await ensureRuntime(pi, state, ctx);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
972
|
+
if (!isCurrent())
|
|
973
|
+
return;
|
|
974
|
+
while (queue.length > 0 && isCurrent()) {
|
|
975
|
+
// Consume once: a failed command must not poison the queue, and an
|
|
976
|
+
// uncertain provider/tool send must not be retried automatically.
|
|
977
|
+
const message = queue.shift();
|
|
894
978
|
try {
|
|
979
|
+
const command = parseJarvisSideCommand(message);
|
|
895
980
|
if (command) {
|
|
896
981
|
runtime = await executeJarvisSideCommand(pi, state, ctx, runtime, command);
|
|
897
982
|
}
|
|
898
983
|
else {
|
|
984
|
+
await runtime.syncModel(getDesiredJarvisModel(state), getDesiredJarvisThinkingLevel(state));
|
|
985
|
+
if (!isCurrent())
|
|
986
|
+
return;
|
|
987
|
+
const seenContext = state.mainContext;
|
|
899
988
|
await runtime.sendMessage(message);
|
|
900
|
-
|
|
989
|
+
if (isCurrent())
|
|
990
|
+
state.lastJarvisSeenMainContext = seenContext;
|
|
901
991
|
}
|
|
902
|
-
|
|
992
|
+
if (!isCurrent())
|
|
993
|
+
return;
|
|
903
994
|
}
|
|
904
995
|
catch (error) {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
996
|
+
if (!isCurrent() || isStaleJarvisBootError(error))
|
|
997
|
+
return;
|
|
998
|
+
state.bridge.notify(`Failed to process /jarvis input: ${error instanceof Error ? error.message : String(error)}. Input was not retried.`, "error");
|
|
908
999
|
}
|
|
909
1000
|
}
|
|
910
1001
|
}
|
|
911
1002
|
catch (error) {
|
|
912
|
-
if (isStaleJarvisBootError(error))
|
|
1003
|
+
if (!isCurrent() || isStaleJarvisBootError(error))
|
|
913
1004
|
return;
|
|
914
|
-
|
|
915
|
-
state.bridge.notify(`/jarvis startup failed: ${error instanceof Error ? error.message : String(error)}
|
|
1005
|
+
queue.length = 0;
|
|
1006
|
+
state.bridge.notify(`/jarvis startup failed: ${error instanceof Error ? error.message : String(error)}. Please submit again.`, "error");
|
|
916
1007
|
}
|
|
917
1008
|
})().finally(() => {
|
|
918
|
-
state.flushPromise
|
|
1009
|
+
if (state.flushPromise === flushPromise)
|
|
1010
|
+
state.flushPromise = undefined;
|
|
919
1011
|
});
|
|
920
|
-
|
|
1012
|
+
state.flushPromise = flushPromise;
|
|
1013
|
+
return flushPromise;
|
|
921
1014
|
}
|
|
922
1015
|
async function ensureRuntime(pi, state, ctx) {
|
|
923
1016
|
if (state.runtime) {
|
|
@@ -944,6 +1037,7 @@ async function ensureRuntime(pi, state, ctx) {
|
|
|
944
1037
|
const runtime = await JarvisSideSessionRuntime.create({
|
|
945
1038
|
bridge: state.bridge,
|
|
946
1039
|
cwd: ctx.cwd,
|
|
1040
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
947
1041
|
modelRegistry: ctx.modelRegistry,
|
|
948
1042
|
model: getDesiredJarvisModel(state),
|
|
949
1043
|
jarvisModelModeProvider: () => state.jarvisModelSelection.mode,
|
|
@@ -951,12 +1045,14 @@ async function ensureRuntime(pi, state, ctx) {
|
|
|
951
1045
|
sessionFile,
|
|
952
1046
|
systemPromptProvider: () => state.systemPrompt,
|
|
953
1047
|
mainContextProvider: () => state.mainContext,
|
|
954
|
-
toolAccessProvider: () => state.allowSideTools,
|
|
1048
|
+
toolAccessProvider: () => isCurrentBoot() && state.allowSideTools,
|
|
955
1049
|
communicationPermissionsProvider: () => ({
|
|
956
|
-
allowFollowUpToMain: state.allowFollowUpToMain,
|
|
957
|
-
allowSteerToMain: state.allowSteerToMain,
|
|
1050
|
+
allowFollowUpToMain: isCurrentBoot() && state.allowFollowUpToMain,
|
|
1051
|
+
allowSteerToMain: isCurrentBoot() && state.allowSteerToMain,
|
|
958
1052
|
}),
|
|
959
1053
|
sendFollowUpToMain: (message) => {
|
|
1054
|
+
if (!isCurrentBoot() || !state.allowFollowUpToMain)
|
|
1055
|
+
throw new Error("/jarvis follow-up permission expired.");
|
|
960
1056
|
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
961
1057
|
},
|
|
962
1058
|
// Route the confirmation through the /jarvis overlay itself via the
|
|
@@ -964,8 +1060,10 @@ async function ensureRuntime(pi, state, ctx) {
|
|
|
964
1060
|
// the base layer (the pi editor container) and sit hidden behind the
|
|
965
1061
|
// /jarvis overlay, leaving the side-session tool execute hung on a
|
|
966
1062
|
// promise the user could never answer.
|
|
967
|
-
confirmSteerToMain: (message) => state.bridge.requestConfirmation("Send /jarvis steer to main?", `This will steer the main agent with:\n\n${message}
|
|
1063
|
+
confirmSteerToMain: (message, signal) => state.bridge.requestConfirmation("Send /jarvis steer to main?", `This will steer the main agent with:\n\n${message}`, signal),
|
|
968
1064
|
sendSteerToMain: (message) => {
|
|
1065
|
+
if (!isCurrentBoot() || !state.allowSteerToMain)
|
|
1066
|
+
throw new Error("/jarvis redirect permission expired.");
|
|
969
1067
|
pi.sendUserMessage(message, { deliverAs: "steer" });
|
|
970
1068
|
},
|
|
971
1069
|
themeProvider: state.themeProvider,
|
|
@@ -978,6 +1076,8 @@ async function ensureRuntime(pi, state, ctx) {
|
|
|
978
1076
|
state.bridge.setWorkingMessage(undefined);
|
|
979
1077
|
return runtime;
|
|
980
1078
|
})().catch((error) => {
|
|
1079
|
+
if (!isCurrentBoot())
|
|
1080
|
+
throw new StaleJarvisBootError();
|
|
981
1081
|
state.bridge.setWorkingMessage(undefined);
|
|
982
1082
|
throw error;
|
|
983
1083
|
}).finally(() => {
|