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 +21 -0
- package/README.md +145 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +508 -0
- package/dist/index.js.map +1 -0
- package/dist/main-context.d.ts +49 -0
- package/dist/main-context.d.ts.map +1 -0
- package/dist/main-context.js +582 -0
- package/dist/main-context.js.map +1 -0
- package/dist/main-session-state.d.ts +63 -0
- package/dist/main-session-state.d.ts.map +1 -0
- package/dist/main-session-state.js +225 -0
- package/dist/main-session-state.js.map +1 -0
- package/dist/mcp-policy.d.ts +2 -0
- package/dist/mcp-policy.d.ts.map +1 -0
- package/dist/mcp-policy.js +2 -0
- package/dist/mcp-policy.js.map +1 -0
- package/dist/overlay.d.ts +104 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +437 -0
- package/dist/overlay.js.map +1 -0
- package/dist/session-ref.d.ts +14 -0
- package/dist/session-ref.d.ts.map +1 -0
- package/dist/session-ref.js +34 -0
- package/dist/session-ref.js.map +1 -0
- package/dist/side-session.d.ts +61 -0
- package/dist/side-session.d.ts.map +1 -0
- package/dist/side-session.js +687 -0
- package/dist/side-session.js.map +1 -0
- package/package.json +64 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|