@jx-grxf/patchpilot 0.2.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/.env.example +13 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +71 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agent.d.ts +21 -0
- package/dist/core/agent.js +346 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/codex.d.ts +22 -0
- package/dist/core/codex.js +242 -0
- package/dist/core/codex.js.map +1 -0
- package/dist/core/compute.d.ts +9 -0
- package/dist/core/compute.js +18 -0
- package/dist/core/compute.js.map +1 -0
- package/dist/core/doctor.d.ts +7 -0
- package/dist/core/doctor.js +226 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/env.d.ts +6 -0
- package/dist/core/env.js +103 -0
- package/dist/core/env.js.map +1 -0
- package/dist/core/gemini.d.ts +20 -0
- package/dist/core/gemini.js +177 -0
- package/dist/core/gemini.js.map +1 -0
- package/dist/core/json.d.ts +3 -0
- package/dist/core/json.js +95 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/modelClient.d.ts +8 -0
- package/dist/core/modelClient.js +42 -0
- package/dist/core/modelClient.js.map +1 -0
- package/dist/core/nvidia.d.ts +19 -0
- package/dist/core/nvidia.js +160 -0
- package/dist/core/nvidia.js.map +1 -0
- package/dist/core/ollama.d.ts +31 -0
- package/dist/core/ollama.js +176 -0
- package/dist/core/ollama.js.map +1 -0
- package/dist/core/openrouter.d.ts +27 -0
- package/dist/core/openrouter.js +168 -0
- package/dist/core/openrouter.js.map +1 -0
- package/dist/core/subagents.d.ts +14 -0
- package/dist/core/subagents.js +89 -0
- package/dist/core/subagents.js.map +1 -0
- package/dist/core/tokenAccounting.d.ts +6 -0
- package/dist/core/tokenAccounting.js +134 -0
- package/dist/core/tokenAccounting.js.map +1 -0
- package/dist/core/types.d.ts +90 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/workspace.d.ts +28 -0
- package/dist/core/workspace.js +616 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/tui/App.d.ts +6 -0
- package/dist/tui/App.js +1717 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/commands.d.ts +14 -0
- package/dist/tui/commands.js +210 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/components/CommandSuggestions.d.ts +12 -0
- package/dist/tui/components/CommandSuggestions.js +12 -0
- package/dist/tui/components/CommandSuggestions.js.map +1 -0
- package/dist/tui/components/Composer.d.ts +13 -0
- package/dist/tui/components/Composer.js +29 -0
- package/dist/tui/components/Composer.js.map +1 -0
- package/dist/tui/components/Header.d.ts +25 -0
- package/dist/tui/components/Header.js +62 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/OnboardingPanel.d.ts +38 -0
- package/dist/tui/components/OnboardingPanel.js +85 -0
- package/dist/tui/components/OnboardingPanel.js.map +1 -0
- package/dist/tui/components/Sidebar.d.ts +22 -0
- package/dist/tui/components/Sidebar.js +133 -0
- package/dist/tui/components/Sidebar.js.map +1 -0
- package/dist/tui/components/Transcript.d.ts +10 -0
- package/dist/tui/components/Transcript.js +111 -0
- package/dist/tui/components/Transcript.js.map +1 -0
- package/dist/tui/format.d.ts +29 -0
- package/dist/tui/format.js +202 -0
- package/dist/tui/format.js.map +1 -0
- package/dist/tui/hosts.d.ts +34 -0
- package/dist/tui/hosts.js +338 -0
- package/dist/tui/hosts.js.map +1 -0
- package/dist/tui/inputRouting.d.ts +8 -0
- package/dist/tui/inputRouting.js +94 -0
- package/dist/tui/inputRouting.js.map +1 -0
- package/dist/tui/platform.d.ts +2 -0
- package/dist/tui/platform.js +13 -0
- package/dist/tui/platform.js.map +1 -0
- package/dist/tui/systemStats.d.ts +25 -0
- package/dist/tui/systemStats.js +88 -0
- package/dist/tui/systemStats.js.map +1 -0
- package/dist/tui/types.d.ts +16 -0
- package/dist/tui/types.js +2 -0
- package/dist/tui/types.js.map +1 -0
- package/docs/showcase/patchpilot-showcase.svg +39 -0
- package/package.json +63 -0
package/dist/tui/App.js
ADDED
|
@@ -0,0 +1,1717 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, useApp, useInput, useStdout } from "ink";
|
|
4
|
+
import { AgentRunner } from "../core/agent.js";
|
|
5
|
+
import { defaultCodexModel, hasCodexCliOAuth } from "../core/codex.js";
|
|
6
|
+
import { describeComputeTarget } from "../core/compute.js";
|
|
7
|
+
import { runDoctor } from "../core/doctor.js";
|
|
8
|
+
import { savePatchPilotEnvValues } from "../core/env.js";
|
|
9
|
+
import { defaultGeminiModel, readGeminiApiKey } from "../core/gemini.js";
|
|
10
|
+
import { createModelClient } from "../core/modelClient.js";
|
|
11
|
+
import { defaultNvidiaModel, readNvidiaApiKey } from "../core/nvidia.js";
|
|
12
|
+
import { defaultOllamaModel, OllamaClient } from "../core/ollama.js";
|
|
13
|
+
import { defaultOpenRouterModel, isOpenRouterFreeModel, readOpenRouterApiKey } from "../core/openrouter.js";
|
|
14
|
+
import { addTelemetryToSession, emptySessionTelemetry, estimateTokens } from "../core/tokenAccounting.js";
|
|
15
|
+
import { CommandSuggestions } from "./components/CommandSuggestions.js";
|
|
16
|
+
import { Composer, FooterHints } from "./components/Composer.js";
|
|
17
|
+
import { Header } from "./components/Header.js";
|
|
18
|
+
import { OnboardingPanel } from "./components/OnboardingPanel.js";
|
|
19
|
+
import { Sidebar } from "./components/Sidebar.js";
|
|
20
|
+
import { Transcript } from "./components/Transcript.js";
|
|
21
|
+
import { filterSlashCommands, formatCommandDetail, formatCommandHelp } from "./commands.js";
|
|
22
|
+
import { formatCost, formatSessionTokens, formatTokens, normalizeModelAlias, readToggle } from "./format.js";
|
|
23
|
+
import { checkOllamaHost, discoverOllamaHosts, normalizeOllamaUrl, readOllamaHostDetails, startLocalOllamaAppAndWait } from "./hosts.js";
|
|
24
|
+
import { readGpuStats, readSystemStats } from "./systemStats.js";
|
|
25
|
+
import { maxTranscriptLines } from "./types.js";
|
|
26
|
+
const modelCacheTtlMs = 5 * 60_000;
|
|
27
|
+
const modelCache = new Map();
|
|
28
|
+
export function App(props) {
|
|
29
|
+
const { exit } = useApp();
|
|
30
|
+
const { stdout } = useStdout();
|
|
31
|
+
const [input, setInput] = useState(props.initialTask ?? "");
|
|
32
|
+
const didRunInitialTask = useRef(false);
|
|
33
|
+
const didOpenDefaultOnboarding = useRef(false);
|
|
34
|
+
const abortControllerRef = useRef(null);
|
|
35
|
+
const usedOllamaModelsRef = useRef(new Set());
|
|
36
|
+
const [lines, setLines] = useState([]);
|
|
37
|
+
const [advisorNotes, setAdvisorNotes] = useState([]);
|
|
38
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
39
|
+
const [status, setStatus] = useState("idle");
|
|
40
|
+
const [telemetry, setTelemetry] = useState(null);
|
|
41
|
+
const [sessionTelemetry, setSessionTelemetry] = useState(() => emptySessionTelemetry());
|
|
42
|
+
const [systemStats, setSystemStats] = useState(() => readSystemStats().stats);
|
|
43
|
+
const [gpuStats, setGpuStats] = useState(null);
|
|
44
|
+
const [agentMode, setAgentMode] = useState(props.allowWrite || props.allowShell ? "build" : "plan");
|
|
45
|
+
const [hostOptions, setHostOptions] = useState([]);
|
|
46
|
+
const [activeHost, setActiveHost] = useState(null);
|
|
47
|
+
const [isLoadingHosts, setIsLoadingHosts] = useState(false);
|
|
48
|
+
const [modelOptions, setModelOptions] = useState([]);
|
|
49
|
+
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
|
50
|
+
const [onboarding, setOnboarding] = useState(null);
|
|
51
|
+
const [onboardingIndex, setOnboardingIndex] = useState(0);
|
|
52
|
+
const [onboardingInput, setOnboardingInput] = useState("");
|
|
53
|
+
const [onboardingBusyMessage, setOnboardingBusyMessage] = useState(null);
|
|
54
|
+
const [paletteIndex, setPaletteIndex] = useState(0);
|
|
55
|
+
const [activeScrollPane, setActiveScrollPane] = useState("transcript");
|
|
56
|
+
const [transcriptScrollOffset, setTranscriptScrollOffset] = useState(0);
|
|
57
|
+
const [sessionScrollOffset, setSessionScrollOffset] = useState(0);
|
|
58
|
+
const [settings, setSettings] = useState({
|
|
59
|
+
provider: props.provider,
|
|
60
|
+
model: props.model,
|
|
61
|
+
ollamaUrl: props.ollamaUrl,
|
|
62
|
+
workspace: props.workspace,
|
|
63
|
+
allowWrite: props.allowWrite,
|
|
64
|
+
allowShell: props.allowShell,
|
|
65
|
+
maxSteps: props.maxSteps,
|
|
66
|
+
thinkingMode: props.thinkingMode,
|
|
67
|
+
reasoningEffort: props.reasoningEffort,
|
|
68
|
+
subagents: props.subagents
|
|
69
|
+
});
|
|
70
|
+
const draftTokens = estimateTokens(input);
|
|
71
|
+
const terminalRows = stdout.rows ?? 40;
|
|
72
|
+
const terminalColumns = stdout.columns ?? 120;
|
|
73
|
+
const paletteItems = !isRunning && !onboarding
|
|
74
|
+
? buildCommandSuggestionItems({
|
|
75
|
+
input,
|
|
76
|
+
provider: settings.provider,
|
|
77
|
+
hostOptions,
|
|
78
|
+
modelOptions,
|
|
79
|
+
currentModel: settings.model,
|
|
80
|
+
isLoadingHosts,
|
|
81
|
+
isLoadingModels
|
|
82
|
+
})
|
|
83
|
+
: [];
|
|
84
|
+
const rootHeight = Math.max(24, terminalRows);
|
|
85
|
+
const headerReservedHeight = 8;
|
|
86
|
+
const paletteReservedHeight = !onboarding && paletteItems.length > 0 ? Math.min(8, paletteItems.length) + 4 : 0;
|
|
87
|
+
const composerReservedHeight = onboarding ? 0 : 2;
|
|
88
|
+
const footerReservedHeight = onboarding ? 0 : 1;
|
|
89
|
+
const panelHeight = Math.max(8, rootHeight - headerReservedHeight - composerReservedHeight - paletteReservedHeight - footerReservedHeight);
|
|
90
|
+
const transcriptWidth = Math.max(42, terminalColumns - 38);
|
|
91
|
+
const scrollStep = Math.max(4, Math.floor(panelHeight * 0.8));
|
|
92
|
+
const appendLine = useCallback((line) => {
|
|
93
|
+
setLines((currentLines) => [
|
|
94
|
+
...currentLines.slice(-maxTranscriptLines),
|
|
95
|
+
{
|
|
96
|
+
...line,
|
|
97
|
+
id: Date.now() + Math.random()
|
|
98
|
+
}
|
|
99
|
+
]);
|
|
100
|
+
}, []);
|
|
101
|
+
const applyMode = useCallback((nextMode, announce = true) => {
|
|
102
|
+
setAgentMode(nextMode);
|
|
103
|
+
setSettings((currentSettings) => ({
|
|
104
|
+
...currentSettings,
|
|
105
|
+
allowWrite: nextMode === "build" ? currentSettings.allowWrite : false,
|
|
106
|
+
allowShell: nextMode === "build" ? currentSettings.allowShell : false
|
|
107
|
+
}));
|
|
108
|
+
if (announce) {
|
|
109
|
+
appendLine({
|
|
110
|
+
tone: "success",
|
|
111
|
+
label: "mode",
|
|
112
|
+
text: `${nextMode} mode ${nextMode === "plan" ? "keeps tools read-only" : "uses enabled write/shell permissions"}`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}, [appendLine]);
|
|
116
|
+
const toggleMode = useCallback(() => {
|
|
117
|
+
applyMode(agentMode === "plan" ? "build" : "plan");
|
|
118
|
+
}, [agentMode, applyMode]);
|
|
119
|
+
const loadHostSuggestions = useCallback(async (refresh = false, announce = false) => {
|
|
120
|
+
if (isLoadingHosts) {
|
|
121
|
+
return hostOptions;
|
|
122
|
+
}
|
|
123
|
+
setIsLoadingHosts(true);
|
|
124
|
+
try {
|
|
125
|
+
const hosts = await discoverOllamaHosts(settings.ollamaUrl, {
|
|
126
|
+
refresh
|
|
127
|
+
});
|
|
128
|
+
setHostOptions(hosts);
|
|
129
|
+
if (announce) {
|
|
130
|
+
appendLine({
|
|
131
|
+
tone: hosts.length > 0 ? "accent" : "warning",
|
|
132
|
+
label: "hosts",
|
|
133
|
+
text: hosts.length > 0
|
|
134
|
+
? `Found ${hosts.length} Ollama host${hosts.length === 1 ? "" : "s"}. Pick one with /connect or the command palette.`
|
|
135
|
+
: "No reachable Ollama hosts found.",
|
|
136
|
+
detail: hosts.length > 0
|
|
137
|
+
? formatHostOptions(hosts)
|
|
138
|
+
: "PatchPilot scanned the local LAN and Tailscale peers. Try /connect <host> for a manual URL or MagicDNS name."
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return hosts;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
setIsLoadingHosts(false);
|
|
145
|
+
}
|
|
146
|
+
}, [appendLine, hostOptions, isLoadingHosts, settings.ollamaUrl]);
|
|
147
|
+
const loadProviderModels = useCallback(async (refresh = false) => {
|
|
148
|
+
if (isLoadingModels) {
|
|
149
|
+
return modelOptions;
|
|
150
|
+
}
|
|
151
|
+
setIsLoadingModels(true);
|
|
152
|
+
try {
|
|
153
|
+
return await loadAvailableModels(settings.provider, settings.ollamaUrl, setModelOptions, refresh);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
setIsLoadingModels(false);
|
|
157
|
+
}
|
|
158
|
+
}, [isLoadingModels, modelOptions, settings.ollamaUrl, settings.provider]);
|
|
159
|
+
const connectToHost = useCallback(async (value, options = {}) => {
|
|
160
|
+
const candidate = typeof value === "string" ? null : value;
|
|
161
|
+
const nextUrl = typeof value === "string" ? normalizeOllamaUrl(value) : value.url;
|
|
162
|
+
const verifiedHost = await checkOllamaHost(nextUrl, {
|
|
163
|
+
...candidate,
|
|
164
|
+
timeoutMs: 1200
|
|
165
|
+
});
|
|
166
|
+
if (!verifiedHost) {
|
|
167
|
+
if (options.announce !== false) {
|
|
168
|
+
appendLine({
|
|
169
|
+
tone: "warning",
|
|
170
|
+
label: "ollama",
|
|
171
|
+
text: `No Ollama server answered at ${nextUrl}.`,
|
|
172
|
+
detail: "Check the IP, MagicDNS name, firewall rules, and whether Ollama is listening on the remote machine."
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const details = await readOllamaHostDetails(verifiedHost, true).catch(() => ({
|
|
178
|
+
host: verifiedHost,
|
|
179
|
+
models: [],
|
|
180
|
+
runningModels: [],
|
|
181
|
+
fetchedAt: Date.now()
|
|
182
|
+
}));
|
|
183
|
+
setTelemetry(null);
|
|
184
|
+
setActiveHost(details);
|
|
185
|
+
setHostOptions((currentHosts) => [verifiedHost, ...currentHosts.filter((host) => host.url !== verifiedHost.url)]);
|
|
186
|
+
setModelOptions(details.models);
|
|
187
|
+
modelCache.set(`ollama:${verifiedHost.url}`, {
|
|
188
|
+
models: details.models,
|
|
189
|
+
expiresAt: Date.now() + modelCacheTtlMs
|
|
190
|
+
});
|
|
191
|
+
setSettings((currentSettings) => ({
|
|
192
|
+
...currentSettings,
|
|
193
|
+
provider: "ollama",
|
|
194
|
+
ollamaUrl: verifiedHost.url
|
|
195
|
+
}));
|
|
196
|
+
savePatchPilotEnvValues({
|
|
197
|
+
PATCHPILOT_PROVIDER: "ollama",
|
|
198
|
+
PATCHPILOT_OLLAMA_URL: verifiedHost.url
|
|
199
|
+
});
|
|
200
|
+
if (options.announce !== false) {
|
|
201
|
+
appendLine({
|
|
202
|
+
tone: "success",
|
|
203
|
+
label: "ollama",
|
|
204
|
+
text: `connected to ${verifiedHost.deviceName}`,
|
|
205
|
+
detail: `Ollama ${verifiedHost.version ?? "unknown version"} at ${verifiedHost.url}. Only inference runs on this host; file reads, writes, shell, Git, and tests stay on this device.`
|
|
206
|
+
});
|
|
207
|
+
if (details.models.length > 0 && !details.models.includes(settings.model)) {
|
|
208
|
+
appendLine({
|
|
209
|
+
tone: "warning",
|
|
210
|
+
label: "model",
|
|
211
|
+
text: `${settings.model} is not available on ${verifiedHost.deviceName}.`,
|
|
212
|
+
detail: `Pick a host model with /models. Available:\n${formatModelOptions(details.models, settings.model)}`
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return details;
|
|
217
|
+
}, [appendLine, settings.model]);
|
|
218
|
+
const openModelSelection = useCallback(async (provider, options = {}) => {
|
|
219
|
+
setTelemetry(null);
|
|
220
|
+
setOnboardingBusyMessage(`Loading ${provider} models...`);
|
|
221
|
+
const nextModel = defaultModelForProvider(provider, options.currentModel ?? settings.model);
|
|
222
|
+
setSettings((currentSettings) => ({
|
|
223
|
+
...currentSettings,
|
|
224
|
+
provider,
|
|
225
|
+
model: nextModel
|
|
226
|
+
}));
|
|
227
|
+
try {
|
|
228
|
+
const models = await loadAvailableModels(provider, options.ollamaUrl ?? settings.ollamaUrl, setModelOptions, true);
|
|
229
|
+
if (models.length === 0) {
|
|
230
|
+
appendLine({
|
|
231
|
+
tone: "warning",
|
|
232
|
+
label: "onboarding",
|
|
233
|
+
text: provider === "ollama"
|
|
234
|
+
? "No Ollama models found on that host."
|
|
235
|
+
: provider === "gemini"
|
|
236
|
+
? "No Gemini models listed. Check the API key."
|
|
237
|
+
: provider === "openrouter"
|
|
238
|
+
? "No OpenRouter models listed. Check the API key."
|
|
239
|
+
: "No Codex OAuth models listed."
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
setOnboarding({
|
|
244
|
+
step: "model",
|
|
245
|
+
provider,
|
|
246
|
+
models,
|
|
247
|
+
deviceName: options.deviceName
|
|
248
|
+
});
|
|
249
|
+
setOnboardingIndex(0);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
appendLine({
|
|
253
|
+
tone: "danger",
|
|
254
|
+
label: "onboarding",
|
|
255
|
+
text: error instanceof Error ? error.message : String(error)
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
setOnboardingBusyMessage(null);
|
|
260
|
+
}
|
|
261
|
+
}, [appendLine, settings.model, settings.ollamaUrl]);
|
|
262
|
+
const closeOnboarding = useCallback(() => {
|
|
263
|
+
setOnboarding(null);
|
|
264
|
+
setOnboardingIndex(0);
|
|
265
|
+
setOnboardingInput("");
|
|
266
|
+
setOnboardingBusyMessage(null);
|
|
267
|
+
}, []);
|
|
268
|
+
const goBackOnboarding = useCallback(() => {
|
|
269
|
+
if (!onboarding) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
setOnboardingBusyMessage(null);
|
|
273
|
+
setOnboardingInput("");
|
|
274
|
+
setOnboardingIndex(0);
|
|
275
|
+
switch (onboarding.step) {
|
|
276
|
+
case "entry":
|
|
277
|
+
setOnboarding(null);
|
|
278
|
+
return;
|
|
279
|
+
case "host":
|
|
280
|
+
case "api-key-choice":
|
|
281
|
+
case "gemini-key":
|
|
282
|
+
case "openrouter-key":
|
|
283
|
+
case "nvidia-key":
|
|
284
|
+
case "codex-login":
|
|
285
|
+
setOnboarding({
|
|
286
|
+
step: "entry"
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
case "host-input":
|
|
290
|
+
setOnboarding({
|
|
291
|
+
step: "host",
|
|
292
|
+
hosts: hostOptions
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
case "model":
|
|
296
|
+
if (onboarding.provider === "ollama" && activeHost?.host.kind !== "local") {
|
|
297
|
+
setOnboarding({
|
|
298
|
+
step: "host",
|
|
299
|
+
hosts: hostOptions
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (onboarding.provider === "gemini") {
|
|
304
|
+
openApiKeyChoice("gemini", setOnboarding, setOnboardingIndex);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (onboarding.provider === "nvidia") {
|
|
308
|
+
openApiKeyChoice("nvidia", setOnboarding, setOnboardingIndex);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (onboarding.provider === "openrouter") {
|
|
312
|
+
openApiKeyChoice("openrouter", setOnboarding, setOnboardingIndex);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (onboarding.provider === "codex" && !hasCodexCliOAuth()) {
|
|
316
|
+
setOnboarding({
|
|
317
|
+
step: "codex-login"
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
setOnboarding({
|
|
322
|
+
step: "entry"
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}, [activeHost?.host.kind, hostOptions, onboarding]);
|
|
326
|
+
const handleOnboardingSubmit = useCallback(async (value) => {
|
|
327
|
+
if (!onboarding) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (onboarding.step === "entry") {
|
|
331
|
+
const selection = readEntrySelection(value, onboardingIndex);
|
|
332
|
+
if (!selection) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (selection === "local") {
|
|
336
|
+
setOnboardingBusyMessage("Checking local Ollama...");
|
|
337
|
+
let details = await connectToHost("local", {
|
|
338
|
+
announce: false
|
|
339
|
+
});
|
|
340
|
+
if (!details && process.platform === "darwin") {
|
|
341
|
+
setOnboardingBusyMessage("Starting Ollama.app and waiting for the local server...");
|
|
342
|
+
const startedHost = await startLocalOllamaAppAndWait();
|
|
343
|
+
details = startedHost ? await connectToHost(startedHost, { announce: false }) : null;
|
|
344
|
+
}
|
|
345
|
+
if (!details) {
|
|
346
|
+
setOnboardingBusyMessage("Local Ollama is not reachable. Start Ollama.app or run `ollama serve`, then press Enter again.");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
await openModelSelection("ollama", {
|
|
350
|
+
deviceName: details.host.deviceName,
|
|
351
|
+
ollamaUrl: details.host.url
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (selection === "host") {
|
|
356
|
+
setOnboardingBusyMessage("Scanning LAN and Tailscale for Ollama hosts...");
|
|
357
|
+
try {
|
|
358
|
+
const hosts = await loadHostSuggestions(true, false);
|
|
359
|
+
setOnboarding({
|
|
360
|
+
step: "host",
|
|
361
|
+
hosts
|
|
362
|
+
});
|
|
363
|
+
setOnboardingIndex(0);
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
setOnboardingBusyMessage(null);
|
|
367
|
+
}
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (selection === "gemini" || selection === "openrouter" || selection === "nvidia") {
|
|
371
|
+
openApiKeyChoice(selection, setOnboarding, setOnboardingIndex);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!hasCodexCliOAuth()) {
|
|
375
|
+
setOnboarding({
|
|
376
|
+
step: "codex-login"
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
await openModelSelection("codex");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (onboarding.step === "host") {
|
|
384
|
+
const selectionIndex = readIndexedSelection(value, onboardingIndex);
|
|
385
|
+
if (selectionIndex === null) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (selectionIndex === 0) {
|
|
389
|
+
setOnboarding({
|
|
390
|
+
step: "host-input"
|
|
391
|
+
});
|
|
392
|
+
setOnboardingInput("");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const selectedHost = onboarding.hosts[selectionIndex - 1];
|
|
396
|
+
if (!selectedHost) {
|
|
397
|
+
appendLine({
|
|
398
|
+
tone: "warning",
|
|
399
|
+
label: "onboarding",
|
|
400
|
+
text: "Unknown host selection."
|
|
401
|
+
});
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
setOnboardingBusyMessage(`Connecting to ${selectedHost.deviceName}...`);
|
|
405
|
+
const details = await connectToHost(selectedHost, {
|
|
406
|
+
announce: false
|
|
407
|
+
});
|
|
408
|
+
if (!details) {
|
|
409
|
+
setOnboardingBusyMessage(null);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
await openModelSelection("ollama", {
|
|
413
|
+
deviceName: details.host.deviceName,
|
|
414
|
+
ollamaUrl: details.host.url
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (onboarding.step === "host-input") {
|
|
419
|
+
const hostValue = value.trim();
|
|
420
|
+
if (!hostValue) {
|
|
421
|
+
appendLine({
|
|
422
|
+
tone: "warning",
|
|
423
|
+
label: "onboarding",
|
|
424
|
+
text: "Host cannot be empty."
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
setOnboardingBusyMessage(`Connecting to ${hostValue}...`);
|
|
429
|
+
const details = await connectToHost(hostValue, {
|
|
430
|
+
announce: false
|
|
431
|
+
});
|
|
432
|
+
if (!details) {
|
|
433
|
+
setOnboardingBusyMessage(null);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
await openModelSelection("ollama", {
|
|
437
|
+
deviceName: details.host.deviceName,
|
|
438
|
+
ollamaUrl: details.host.url
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (onboarding.step === "api-key-choice") {
|
|
443
|
+
const choice = readIndexedSelection(value, onboardingIndex);
|
|
444
|
+
if (choice === null) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (choice === 0 && onboarding.hasExistingKey) {
|
|
448
|
+
await openModelSelection(onboarding.provider, {
|
|
449
|
+
currentModel: defaultModelForProvider(onboarding.provider, settings.model)
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
setOnboarding({
|
|
454
|
+
step: `${onboarding.provider}-key`
|
|
455
|
+
});
|
|
456
|
+
setOnboardingInput("");
|
|
457
|
+
setOnboardingIndex(0);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (onboarding.step === "gemini-key") {
|
|
461
|
+
const apiKey = value.trim();
|
|
462
|
+
if (!apiKey) {
|
|
463
|
+
appendLine({
|
|
464
|
+
tone: "warning",
|
|
465
|
+
label: "onboarding",
|
|
466
|
+
text: "Gemini API key cannot be empty."
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
process.env.GEMINI_API_KEY = apiKey;
|
|
471
|
+
savePatchPilotEnvValues({
|
|
472
|
+
PATCHPILOT_PROVIDER: "gemini",
|
|
473
|
+
PATCHPILOT_MODEL: defaultGeminiModel,
|
|
474
|
+
GEMINI_API_KEY: apiKey
|
|
475
|
+
});
|
|
476
|
+
appendLine({
|
|
477
|
+
tone: "success",
|
|
478
|
+
label: "onboarding",
|
|
479
|
+
text: "Gemini API key saved to PatchPilot config."
|
|
480
|
+
});
|
|
481
|
+
await openModelSelection("gemini", {
|
|
482
|
+
currentModel: defaultGeminiModel
|
|
483
|
+
});
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (onboarding.step === "openrouter-key") {
|
|
487
|
+
const apiKey = value.trim();
|
|
488
|
+
if (!apiKey) {
|
|
489
|
+
appendLine({
|
|
490
|
+
tone: "warning",
|
|
491
|
+
label: "onboarding",
|
|
492
|
+
text: "OpenRouter API key cannot be empty."
|
|
493
|
+
});
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
process.env.OPENROUTER_API_KEY = apiKey;
|
|
497
|
+
savePatchPilotEnvValues({
|
|
498
|
+
PATCHPILOT_PROVIDER: "openrouter",
|
|
499
|
+
PATCHPILOT_MODEL: defaultOpenRouterModel,
|
|
500
|
+
OPENROUTER_API_KEY: apiKey
|
|
501
|
+
});
|
|
502
|
+
appendLine({
|
|
503
|
+
tone: "success",
|
|
504
|
+
label: "onboarding",
|
|
505
|
+
text: "OpenRouter API key saved to PatchPilot config."
|
|
506
|
+
});
|
|
507
|
+
await openModelSelection("openrouter", {
|
|
508
|
+
currentModel: defaultOpenRouterModel
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (onboarding.step === "nvidia-key") {
|
|
513
|
+
const apiKey = value.trim();
|
|
514
|
+
if (!apiKey) {
|
|
515
|
+
appendLine({
|
|
516
|
+
tone: "warning",
|
|
517
|
+
label: "onboarding",
|
|
518
|
+
text: "NVIDIA API key cannot be empty."
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
process.env.NVIDIA_API_KEY = apiKey;
|
|
523
|
+
savePatchPilotEnvValues({
|
|
524
|
+
PATCHPILOT_PROVIDER: "nvidia",
|
|
525
|
+
PATCHPILOT_MODEL: defaultNvidiaModel,
|
|
526
|
+
NVIDIA_API_KEY: apiKey
|
|
527
|
+
});
|
|
528
|
+
appendLine({
|
|
529
|
+
tone: "success",
|
|
530
|
+
label: "onboarding",
|
|
531
|
+
text: "NVIDIA API key saved to PatchPilot config."
|
|
532
|
+
});
|
|
533
|
+
await openModelSelection("nvidia", {
|
|
534
|
+
currentModel: defaultNvidiaModel
|
|
535
|
+
});
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (onboarding.step === "codex-login") {
|
|
539
|
+
if (!hasCodexCliOAuth()) {
|
|
540
|
+
appendLine({
|
|
541
|
+
tone: "warning",
|
|
542
|
+
label: "onboarding",
|
|
543
|
+
text: "Codex OAuth is still missing. Run `codex login`, then press Enter again."
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
await openModelSelection("codex", {
|
|
548
|
+
currentModel: defaultCodexModel
|
|
549
|
+
});
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const selectableModels = filterModelOptions(onboardingInput, onboarding.models);
|
|
553
|
+
const selectedModel = selectModelFromInput(value, selectableModels, onboardingIndex, {
|
|
554
|
+
allowManual: onboarding.provider !== "ollama"
|
|
555
|
+
});
|
|
556
|
+
if (!selectedModel) {
|
|
557
|
+
appendLine({
|
|
558
|
+
tone: "warning",
|
|
559
|
+
label: "onboarding",
|
|
560
|
+
text: "Unknown model selection. Pick a listed model."
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
setTelemetry(null);
|
|
565
|
+
setSettings((currentSettings) => ({
|
|
566
|
+
...currentSettings,
|
|
567
|
+
provider: onboarding.provider,
|
|
568
|
+
model: selectedModel
|
|
569
|
+
}));
|
|
570
|
+
savePatchPilotEnvValues({
|
|
571
|
+
PATCHPILOT_PROVIDER: onboarding.provider,
|
|
572
|
+
PATCHPILOT_MODEL: selectedModel,
|
|
573
|
+
PATCHPILOT_ONBOARDING_COMPLETE: "1",
|
|
574
|
+
...(onboarding.provider === "ollama" ? { PATCHPILOT_OLLAMA_URL: activeHost?.host.url ?? settings.ollamaUrl } : {})
|
|
575
|
+
});
|
|
576
|
+
appendLine({
|
|
577
|
+
tone: "success",
|
|
578
|
+
label: "onboarding",
|
|
579
|
+
text: `ready: ${onboarding.provider} using ${selectedModel}`
|
|
580
|
+
});
|
|
581
|
+
if (onboarding.provider === "openrouter" && isOpenRouterFreeModel(selectedModel)) {
|
|
582
|
+
appendLine({
|
|
583
|
+
tone: "warning",
|
|
584
|
+
label: "openrouter",
|
|
585
|
+
text: "Free OpenRouter models are rate-limited.",
|
|
586
|
+
detail: "OpenRouter documents 20 requests/minute for :free models, plus daily limits depending on account credits."
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
closeOnboarding();
|
|
590
|
+
}, [activeHost?.host.url, appendLine, closeOnboarding, connectToHost, loadHostSuggestions, onboarding, onboardingIndex, openModelSelection, settings.ollamaUrl]);
|
|
591
|
+
const runTask = useCallback(async (task) => {
|
|
592
|
+
if (!task.trim() || isRunning) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
setInput("");
|
|
596
|
+
setTranscriptScrollOffset(0);
|
|
597
|
+
setIsRunning(true);
|
|
598
|
+
appendLine({
|
|
599
|
+
tone: "normal",
|
|
600
|
+
label: "you",
|
|
601
|
+
text: task
|
|
602
|
+
});
|
|
603
|
+
try {
|
|
604
|
+
const runnableSettings = await resolveRunnableSettings(settings, modelOptions, appendLine, setModelOptions);
|
|
605
|
+
if (!runnableSettings) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const abortController = new AbortController();
|
|
609
|
+
abortControllerRef.current = abortController;
|
|
610
|
+
const taskRunner = new AgentRunner({
|
|
611
|
+
...runnableSettings,
|
|
612
|
+
signal: abortController.signal
|
|
613
|
+
});
|
|
614
|
+
for await (const event of taskRunner.run(task)) {
|
|
615
|
+
if (event.type === "metrics") {
|
|
616
|
+
if (runnableSettings.provider === "ollama") {
|
|
617
|
+
usedOllamaModelsRef.current.add(`${runnableSettings.ollamaUrl}|${runnableSettings.model}`);
|
|
618
|
+
}
|
|
619
|
+
setTelemetry(event.metrics);
|
|
620
|
+
setSessionTelemetry((currentSession) => addTelemetryToSession(currentSession, event.metrics));
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (event.type === "subagent") {
|
|
624
|
+
setTelemetry(event.metrics);
|
|
625
|
+
setSessionTelemetry((currentSession) => addTelemetryToSession(currentSession, event.metrics));
|
|
626
|
+
setAdvisorNotes((currentNotes) => upsertAdvisorNote(currentNotes, {
|
|
627
|
+
role: event.role,
|
|
628
|
+
message: event.message
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
setStatus(eventToStatus(event));
|
|
632
|
+
appendLine(eventToLine(event));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
appendLine({
|
|
637
|
+
tone: "danger",
|
|
638
|
+
label: "error",
|
|
639
|
+
text: error instanceof Error ? error.message : String(error)
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
abortControllerRef.current = null;
|
|
644
|
+
setStatus("idle");
|
|
645
|
+
setIsRunning(false);
|
|
646
|
+
}
|
|
647
|
+
}, [appendLine, isRunning, modelOptions, settings]);
|
|
648
|
+
const handleSlashCommand = useCallback(async (rawCommand) => {
|
|
649
|
+
const [commandName = "", ...args] = rawCommand.slice(1).trim().split(/\s+/);
|
|
650
|
+
const command = commandName.toLowerCase();
|
|
651
|
+
switch (command) {
|
|
652
|
+
case "":
|
|
653
|
+
case "help":
|
|
654
|
+
{
|
|
655
|
+
const helpTopic = args.join(" ").trim();
|
|
656
|
+
const detail = helpTopic ? formatCommandHelp(helpTopic) : formatCommandDetail();
|
|
657
|
+
appendLine({
|
|
658
|
+
tone: detail ? "accent" : "warning",
|
|
659
|
+
label: "commands",
|
|
660
|
+
text: helpTopic ? (detail ? `Help for /${helpTopic.replace(/^\//, "")}` : `No help topic for /${helpTopic.replace(/^\//, "")}.`) : "Slash commands. Type / plus a few letters to filter.",
|
|
661
|
+
detail: detail ?? "Use /help to list commands."
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
case "build":
|
|
666
|
+
case "plan":
|
|
667
|
+
case "mode": {
|
|
668
|
+
const nextMode = command === "mode" ? args[0]?.toLowerCase() : command;
|
|
669
|
+
if (nextMode !== "plan" && nextMode !== "build") {
|
|
670
|
+
appendLine({
|
|
671
|
+
tone: "accent",
|
|
672
|
+
label: "mode",
|
|
673
|
+
text: `current ${agentMode}. Use /mode plan, /mode build, or press tab.`
|
|
674
|
+
});
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
applyMode(nextMode);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
case "permissions":
|
|
681
|
+
case "perms":
|
|
682
|
+
appendLine({
|
|
683
|
+
tone: "accent",
|
|
684
|
+
label: "permissions",
|
|
685
|
+
text: `write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | subagents ${settings.subagents ? "on" : "off"}`
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
case "provider": {
|
|
689
|
+
const nextProvider = args[0]?.toLowerCase();
|
|
690
|
+
if (nextProvider !== "ollama" && nextProvider !== "gemini" && nextProvider !== "codex" && nextProvider !== "openrouter" && nextProvider !== "nvidia") {
|
|
691
|
+
appendLine({
|
|
692
|
+
tone: "accent",
|
|
693
|
+
label: "provider",
|
|
694
|
+
text: `current ${settings.provider}. Use /provider ollama, gemini, openrouter, nvidia, or codex.`
|
|
695
|
+
});
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const nextModel = defaultModelForProvider(nextProvider, settings.model);
|
|
699
|
+
setTelemetry(null);
|
|
700
|
+
setModelOptions([]);
|
|
701
|
+
setSettings((currentSettings) => ({
|
|
702
|
+
...currentSettings,
|
|
703
|
+
provider: nextProvider,
|
|
704
|
+
model: nextModel
|
|
705
|
+
}));
|
|
706
|
+
savePatchPilotEnvValues({
|
|
707
|
+
PATCHPILOT_PROVIDER: nextProvider,
|
|
708
|
+
PATCHPILOT_MODEL: nextModel
|
|
709
|
+
});
|
|
710
|
+
if (needsApiKey(nextProvider) && !hasApiKey(nextProvider)) {
|
|
711
|
+
openApiKeyChoice(nextProvider, setOnboarding, setOnboardingIndex);
|
|
712
|
+
}
|
|
713
|
+
appendLine({
|
|
714
|
+
tone: needsApiKey(nextProvider) && !hasApiKey(nextProvider) ? "warning" : "success",
|
|
715
|
+
label: "provider",
|
|
716
|
+
text: needsApiKey(nextProvider) && !hasApiKey(nextProvider)
|
|
717
|
+
? `${nextProvider} needs an API key. Setup opened.`
|
|
718
|
+
: `switched to ${nextProvider} using ${nextModel}`
|
|
719
|
+
});
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
case "onboarding":
|
|
723
|
+
setOnboarding({
|
|
724
|
+
step: "entry"
|
|
725
|
+
});
|
|
726
|
+
setOnboardingIndex(0);
|
|
727
|
+
setOnboardingInput("");
|
|
728
|
+
setOnboardingBusyMessage(null);
|
|
729
|
+
return;
|
|
730
|
+
case "agents":
|
|
731
|
+
case "subagents": {
|
|
732
|
+
const subagentsEnabled = readToggle(args[0], !settings.subagents);
|
|
733
|
+
setSettings((currentSettings) => ({
|
|
734
|
+
...currentSettings,
|
|
735
|
+
subagents: subagentsEnabled
|
|
736
|
+
}));
|
|
737
|
+
appendLine({
|
|
738
|
+
tone: "success",
|
|
739
|
+
label: "agents",
|
|
740
|
+
text: `planner/reviewer subagents ${subagentsEnabled ? "enabled" : "disabled"}`
|
|
741
|
+
});
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
case "think":
|
|
745
|
+
case "thinking": {
|
|
746
|
+
const nextMode = args[0]?.toLowerCase();
|
|
747
|
+
if (nextMode !== "fixed" && nextMode !== "adaptive") {
|
|
748
|
+
appendLine({
|
|
749
|
+
tone: "accent",
|
|
750
|
+
label: "think",
|
|
751
|
+
text: `current ${settings.thinkingMode}. Use /think fixed or /think adaptive.`
|
|
752
|
+
});
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
setSettings((currentSettings) => ({
|
|
756
|
+
...currentSettings,
|
|
757
|
+
thinkingMode: nextMode
|
|
758
|
+
}));
|
|
759
|
+
appendLine({
|
|
760
|
+
tone: "success",
|
|
761
|
+
label: "think",
|
|
762
|
+
text: `thinking mode ${nextMode}`
|
|
763
|
+
});
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
case "reasoning": {
|
|
767
|
+
const nextEffort = args[0]?.toLowerCase();
|
|
768
|
+
if (!isReasoningEffort(nextEffort)) {
|
|
769
|
+
appendLine({
|
|
770
|
+
tone: "accent",
|
|
771
|
+
label: "reasoning",
|
|
772
|
+
text: `current ${settings.reasoningEffort}. Use /reasoning low, medium, high, xhigh, or adaptive.`
|
|
773
|
+
});
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
setSettings((currentSettings) => ({
|
|
777
|
+
...currentSettings,
|
|
778
|
+
reasoningEffort: nextEffort
|
|
779
|
+
}));
|
|
780
|
+
savePatchPilotEnvValues({
|
|
781
|
+
PATCHPILOT_REASONING_EFFORT: nextEffort
|
|
782
|
+
});
|
|
783
|
+
appendLine({
|
|
784
|
+
tone: "success",
|
|
785
|
+
label: "reasoning",
|
|
786
|
+
text: `provider reasoning ${nextEffort}${settings.provider === "ollama" ? " (Ollama ignores common reasoning effort)" : ""}`
|
|
787
|
+
});
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
case "write":
|
|
791
|
+
case "apply": {
|
|
792
|
+
const writeEnabled = readToggle(args[0], !settings.allowWrite);
|
|
793
|
+
if (writeEnabled) {
|
|
794
|
+
setAgentMode("build");
|
|
795
|
+
}
|
|
796
|
+
setSettings((currentSettings) => ({
|
|
797
|
+
...currentSettings,
|
|
798
|
+
allowWrite: writeEnabled
|
|
799
|
+
}));
|
|
800
|
+
appendLine({
|
|
801
|
+
tone: "success",
|
|
802
|
+
label: "write",
|
|
803
|
+
text: `workspace writes ${writeEnabled ? "enabled" : "disabled"}`
|
|
804
|
+
});
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
case "shell": {
|
|
808
|
+
const shellEnabled = readToggle(args[0], !settings.allowShell);
|
|
809
|
+
if (shellEnabled) {
|
|
810
|
+
setAgentMode("build");
|
|
811
|
+
}
|
|
812
|
+
setSettings((currentSettings) => ({
|
|
813
|
+
...currentSettings,
|
|
814
|
+
allowShell: shellEnabled
|
|
815
|
+
}));
|
|
816
|
+
appendLine({
|
|
817
|
+
tone: "success",
|
|
818
|
+
label: "shell",
|
|
819
|
+
text: `shell commands ${shellEnabled ? "enabled" : "disabled"}`
|
|
820
|
+
});
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
case "model": {
|
|
824
|
+
const requestedModel = normalizeModelAlias(args.join(" ").trim());
|
|
825
|
+
if (!requestedModel) {
|
|
826
|
+
const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine);
|
|
827
|
+
if (!models) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
appendLine({
|
|
831
|
+
tone: "accent",
|
|
832
|
+
label: "model",
|
|
833
|
+
text: settings.model,
|
|
834
|
+
detail: models.length > 0 ? formatModelOptions(models, settings.model) : "Use /models to load available models."
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
{
|
|
839
|
+
const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine);
|
|
840
|
+
if (!models) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const nextModel = selectModelFromInput(requestedModel, models, undefined, {
|
|
844
|
+
allowManual: settings.provider !== "ollama"
|
|
845
|
+
});
|
|
846
|
+
if (!nextModel) {
|
|
847
|
+
appendLine({
|
|
848
|
+
tone: "warning",
|
|
849
|
+
label: "model",
|
|
850
|
+
text: `No unique model match for "${requestedModel}".`,
|
|
851
|
+
detail: formatModelOptions(filterModelOptions(requestedModel, models).slice(0, 12), settings.model)
|
|
852
|
+
});
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
await switchModel(settings.provider, nextModel, settings.ollamaUrl, settings.model, appendLine, setModelOptions, setSettings, setTelemetry, models);
|
|
856
|
+
}
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
case "models": {
|
|
860
|
+
const requestedModel = args.join(" ").trim();
|
|
861
|
+
if (requestedModel) {
|
|
862
|
+
const installedModels = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine);
|
|
863
|
+
if (!installedModels) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const nextModel = selectModelFromInput(requestedModel, installedModels, undefined, {
|
|
867
|
+
allowManual: settings.provider !== "ollama"
|
|
868
|
+
});
|
|
869
|
+
if (!nextModel) {
|
|
870
|
+
appendLine({
|
|
871
|
+
tone: "warning",
|
|
872
|
+
label: "models",
|
|
873
|
+
text: "No model selected. Use /models to fetch available models, then choose one from the palette."
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
await switchModel(settings.provider, nextModel, settings.ollamaUrl, settings.model, appendLine, setModelOptions, setSettings, setTelemetry, installedModels);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
appendLine({
|
|
881
|
+
tone: "muted",
|
|
882
|
+
label: "models",
|
|
883
|
+
text: `loading ${settings.provider} models...`
|
|
884
|
+
});
|
|
885
|
+
try {
|
|
886
|
+
const models = await loadProviderModels(true);
|
|
887
|
+
if (models.length === 0) {
|
|
888
|
+
appendLine({
|
|
889
|
+
tone: "warning",
|
|
890
|
+
label: "models",
|
|
891
|
+
text: `No ${settings.provider} models found.`,
|
|
892
|
+
detail: settings.provider === "ollama"
|
|
893
|
+
? "Pull a model on the selected host first."
|
|
894
|
+
: settings.provider === "gemini"
|
|
895
|
+
? "Check GEMINI_API_KEY in PatchPilot config."
|
|
896
|
+
: "Run codex login first."
|
|
897
|
+
});
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
appendLine({
|
|
901
|
+
tone: "accent",
|
|
902
|
+
label: "models",
|
|
903
|
+
text: `Loaded ${models.length} model${models.length === 1 ? "" : "s"} from ${settings.provider}.`,
|
|
904
|
+
detail: formatModelOptions(models, settings.model)
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
appendLine({
|
|
909
|
+
tone: "danger",
|
|
910
|
+
label: "models",
|
|
911
|
+
text: error instanceof Error ? error.message : String(error)
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
case "status":
|
|
917
|
+
appendLine({
|
|
918
|
+
tone: "accent",
|
|
919
|
+
label: "status",
|
|
920
|
+
text: settings.provider === "ollama"
|
|
921
|
+
? `provider ollama | model ${settings.model} | host ${activeHost?.host.deviceName ?? settings.ollamaUrl} | route ${activeHost?.host.url ?? settings.ollamaUrl} | compute ${describeComputeTarget(settings.ollamaUrl).kind} | tools local | agents ${settings.subagents ? "on" : "off"} | write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | draft ${draftTokens} tok | last ${formatTokens(telemetry)} | session ${formatSessionTokens(sessionTelemetry)} | cost ${formatCost(sessionTelemetry.estimatedCostUsd)}`
|
|
922
|
+
: `provider ${settings.provider} | model ${settings.model} | host ${settings.provider} api | compute cloud | agents ${settings.subagents ? "on" : "off"} | think ${settings.thinkingMode} | reasoning ${settings.reasoningEffort} | write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | draft ${draftTokens} tok | last ${formatTokens(telemetry)} | session ${formatSessionTokens(sessionTelemetry)} | cost ${formatCost(sessionTelemetry.estimatedCostUsd)}`
|
|
923
|
+
});
|
|
924
|
+
return;
|
|
925
|
+
case "connect":
|
|
926
|
+
case "host":
|
|
927
|
+
case "ollama":
|
|
928
|
+
if (settings.provider !== "ollama") {
|
|
929
|
+
appendLine({
|
|
930
|
+
tone: "warning",
|
|
931
|
+
label: "provider",
|
|
932
|
+
text: "Ollama host switching is only available with /provider ollama."
|
|
933
|
+
});
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (args.length === 0) {
|
|
937
|
+
appendLine({
|
|
938
|
+
tone: "muted",
|
|
939
|
+
label: "hosts",
|
|
940
|
+
text: "Scanning LAN and Tailscale for Ollama hosts..."
|
|
941
|
+
});
|
|
942
|
+
await loadHostSuggestions(true, true);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (args.join(" ").trim().toLowerCase() === "local") {
|
|
946
|
+
await connectToHost("local");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
{
|
|
950
|
+
const requestedHost = args.join(" ").trim();
|
|
951
|
+
const hostIndex = Number.parseInt(requestedHost, 10);
|
|
952
|
+
const selectedHost = Number.isInteger(hostIndex) ? hostOptions[hostIndex - 1] : undefined;
|
|
953
|
+
if (selectedHost) {
|
|
954
|
+
await connectToHost(selectedHost);
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
await connectToHost(requestedHost);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return;
|
|
961
|
+
case "hosts":
|
|
962
|
+
appendLine({
|
|
963
|
+
tone: "muted",
|
|
964
|
+
label: "hosts",
|
|
965
|
+
text: "Scanning LAN and Tailscale for Ollama hosts..."
|
|
966
|
+
});
|
|
967
|
+
await loadHostSuggestions(true, true);
|
|
968
|
+
return;
|
|
969
|
+
case "eject": {
|
|
970
|
+
if (settings.provider !== "ollama") {
|
|
971
|
+
appendLine({
|
|
972
|
+
tone: "warning",
|
|
973
|
+
label: "eject",
|
|
974
|
+
text: "Eject is only available for Ollama models."
|
|
975
|
+
});
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const target = args.join(" ").trim();
|
|
979
|
+
const ejectedModels = await ejectOllamaModels({
|
|
980
|
+
target,
|
|
981
|
+
settings,
|
|
982
|
+
activeHost,
|
|
983
|
+
usedModels: usedOllamaModelsRef.current
|
|
984
|
+
});
|
|
985
|
+
if (ejectedModels.length === 0) {
|
|
986
|
+
appendLine({
|
|
987
|
+
tone: "warning",
|
|
988
|
+
label: "eject",
|
|
989
|
+
text: "No Ollama model was ejected."
|
|
990
|
+
});
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
appendLine({
|
|
994
|
+
tone: "success",
|
|
995
|
+
label: "eject",
|
|
996
|
+
text: `ejected ${ejectedModels.join(", ")}`
|
|
997
|
+
});
|
|
998
|
+
if (activeHost) {
|
|
999
|
+
const details = await readOllamaHostDetails(activeHost.host, true).catch(() => activeHost);
|
|
1000
|
+
setActiveHost(details);
|
|
1001
|
+
}
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
case "doctor": {
|
|
1005
|
+
appendLine({
|
|
1006
|
+
tone: "muted",
|
|
1007
|
+
label: "doctor",
|
|
1008
|
+
text: "checking local requirements..."
|
|
1009
|
+
});
|
|
1010
|
+
const doctorResults = await runDoctor(settings.provider, settings.ollamaUrl, settings.model);
|
|
1011
|
+
for (const result of doctorResults) {
|
|
1012
|
+
appendLine({
|
|
1013
|
+
tone: result.ok ? "success" : "danger",
|
|
1014
|
+
label: result.name,
|
|
1015
|
+
text: result.details
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
case "clear":
|
|
1021
|
+
setLines([]);
|
|
1022
|
+
setAdvisorNotes([]);
|
|
1023
|
+
setTelemetry(null);
|
|
1024
|
+
setSessionTelemetry(emptySessionTelemetry());
|
|
1025
|
+
setTranscriptScrollOffset(0);
|
|
1026
|
+
setSessionScrollOffset(0);
|
|
1027
|
+
return;
|
|
1028
|
+
case "exit":
|
|
1029
|
+
case "quit":
|
|
1030
|
+
case "q":
|
|
1031
|
+
void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(exit);
|
|
1032
|
+
return;
|
|
1033
|
+
default:
|
|
1034
|
+
appendLine({
|
|
1035
|
+
tone: "warning",
|
|
1036
|
+
label: "unknown",
|
|
1037
|
+
text: `/${command} is not a PatchPilot command. Type /help.`
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}, [
|
|
1041
|
+
activeHost?.host.deviceName,
|
|
1042
|
+
activeHost?.host.url,
|
|
1043
|
+
agentMode,
|
|
1044
|
+
appendLine,
|
|
1045
|
+
applyMode,
|
|
1046
|
+
connectToHost,
|
|
1047
|
+
draftTokens,
|
|
1048
|
+
exit,
|
|
1049
|
+
hostOptions,
|
|
1050
|
+
loadHostSuggestions,
|
|
1051
|
+
loadProviderModels,
|
|
1052
|
+
modelOptions,
|
|
1053
|
+
sessionTelemetry,
|
|
1054
|
+
settings,
|
|
1055
|
+
telemetry
|
|
1056
|
+
]);
|
|
1057
|
+
const handleSubmit = useCallback(async (value) => {
|
|
1058
|
+
const nextValue = value.trim();
|
|
1059
|
+
if (!nextValue || isRunning) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (onboarding) {
|
|
1063
|
+
await handleOnboardingSubmit(nextValue);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (nextValue.startsWith("/")) {
|
|
1067
|
+
const selectedItem = paletteItems[paletteIndex];
|
|
1068
|
+
const commandHasArgs = /^\/\S+\s+\S/.test(nextValue);
|
|
1069
|
+
const shouldApplySuggestion = selectedItem &&
|
|
1070
|
+
(!commandHasArgs || selectedItem.command !== selectedItem.label) &&
|
|
1071
|
+
(selectedItem.execute || selectedItem.command === nextValue || nextValue === "/" || nextValue.endsWith(" "));
|
|
1072
|
+
const commandToRun = shouldApplySuggestion ? selectedItem.command : nextValue;
|
|
1073
|
+
if (selectedItem && !selectedItem.execute && commandToRun !== nextValue) {
|
|
1074
|
+
setInput(commandToRun);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
setInput("");
|
|
1078
|
+
await handleSlashCommand(commandToRun);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
await runTask(nextValue);
|
|
1082
|
+
}, [handleOnboardingSubmit, handleSlashCommand, isRunning, onboarding, paletteIndex, paletteItems, runTask]);
|
|
1083
|
+
useEffect(() => {
|
|
1084
|
+
if (!props.initialTask || didRunInitialTask.current || onboarding) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
didRunInitialTask.current = true;
|
|
1088
|
+
void runTask(props.initialTask);
|
|
1089
|
+
}, [onboarding, props.initialTask, runTask]);
|
|
1090
|
+
useEffect(() => {
|
|
1091
|
+
setPaletteIndex(0);
|
|
1092
|
+
}, [hostOptions, input, modelOptions, onboarding, settings.model, settings.provider]);
|
|
1093
|
+
useEffect(() => {
|
|
1094
|
+
if (didOpenDefaultOnboarding.current || props.initialTask || onboarding || process.env.PATCHPILOT_ONBOARDING_COMPLETE === "1") {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
didOpenDefaultOnboarding.current = true;
|
|
1098
|
+
setOnboarding({
|
|
1099
|
+
step: "entry"
|
|
1100
|
+
});
|
|
1101
|
+
setOnboardingIndex(0);
|
|
1102
|
+
setOnboardingInput("");
|
|
1103
|
+
setOnboardingBusyMessage(null);
|
|
1104
|
+
}, [onboarding, props.initialTask]);
|
|
1105
|
+
useEffect(() => {
|
|
1106
|
+
if (settings.provider !== "ollama") {
|
|
1107
|
+
setActiveHost(null);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
let cancelled = false;
|
|
1111
|
+
async function syncActiveHost() {
|
|
1112
|
+
const verifiedHost = await checkOllamaHost(settings.ollamaUrl, {
|
|
1113
|
+
timeoutMs: 800
|
|
1114
|
+
});
|
|
1115
|
+
if (!verifiedHost) {
|
|
1116
|
+
if (!cancelled) {
|
|
1117
|
+
setActiveHost((currentHost) => (currentHost?.host.url === settings.ollamaUrl ? currentHost : null));
|
|
1118
|
+
}
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const details = await readOllamaHostDetails(verifiedHost).catch(() => ({
|
|
1122
|
+
host: verifiedHost,
|
|
1123
|
+
models: [],
|
|
1124
|
+
runningModels: [],
|
|
1125
|
+
fetchedAt: Date.now()
|
|
1126
|
+
}));
|
|
1127
|
+
if (cancelled) {
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
setActiveHost(details);
|
|
1131
|
+
if (details.models.length > 0) {
|
|
1132
|
+
setModelOptions((currentModels) => (currentModels.length > 0 && currentModels.join("\n") === details.models.join("\n") ? currentModels : details.models));
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
void syncActiveHost();
|
|
1136
|
+
const timer = setInterval(() => {
|
|
1137
|
+
void syncActiveHost();
|
|
1138
|
+
}, 5000);
|
|
1139
|
+
return () => {
|
|
1140
|
+
cancelled = true;
|
|
1141
|
+
clearInterval(timer);
|
|
1142
|
+
};
|
|
1143
|
+
}, [settings.ollamaUrl, settings.provider]);
|
|
1144
|
+
useEffect(() => {
|
|
1145
|
+
if (onboarding || isRunning) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const trimmedInput = input.trim();
|
|
1149
|
+
if (settings.provider === "ollama" && (trimmedInput === "/connect" || trimmedInput === "/hosts") && hostOptions.length === 0 && !isLoadingHosts) {
|
|
1150
|
+
void loadHostSuggestions(false, false);
|
|
1151
|
+
}
|
|
1152
|
+
if ((trimmedInput === "/models" || trimmedInput === "/model") && modelOptions.length === 0 && !isLoadingModels) {
|
|
1153
|
+
void loadProviderModels(false);
|
|
1154
|
+
}
|
|
1155
|
+
}, [hostOptions.length, input, isLoadingHosts, isLoadingModels, isRunning, loadHostSuggestions, loadProviderModels, modelOptions.length, onboarding, settings.provider]);
|
|
1156
|
+
useInput((inputValue, key) => {
|
|
1157
|
+
if (isRunning && key.escape) {
|
|
1158
|
+
abortControllerRef.current?.abort();
|
|
1159
|
+
appendLine({
|
|
1160
|
+
tone: "warning",
|
|
1161
|
+
label: "stop",
|
|
1162
|
+
text: "Stopping current task..."
|
|
1163
|
+
});
|
|
1164
|
+
setStatus("stopping");
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (onboarding) {
|
|
1168
|
+
if (key.escape || key.leftArrow) {
|
|
1169
|
+
goBackOnboarding();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const optionCount = onboarding.step === "model" ? filterModelOptions(onboardingInput, onboarding.models).length : getOnboardingOptionCount(onboarding);
|
|
1173
|
+
if (optionCount > 0 && key.upArrow) {
|
|
1174
|
+
setOnboardingIndex((currentIndex) => (currentIndex - 1 + optionCount) % optionCount);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (optionCount > 0 && key.downArrow) {
|
|
1178
|
+
setOnboardingIndex((currentIndex) => (currentIndex + 1) % optionCount);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (optionCount > 0 && key.return && onboarding.step !== "model") {
|
|
1182
|
+
void handleOnboardingSubmit(String(onboardingIndex + 1));
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (onboarding.step === "codex-login" && key.return) {
|
|
1186
|
+
void handleOnboardingSubmit("");
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (paletteItems.length > 0) {
|
|
1192
|
+
if (key.upArrow) {
|
|
1193
|
+
setPaletteIndex((currentIndex) => (currentIndex - 1 + paletteItems.length) % paletteItems.length);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (key.downArrow) {
|
|
1197
|
+
setPaletteIndex((currentIndex) => (currentIndex + 1) % paletteItems.length);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (key.escape) {
|
|
1201
|
+
setInput("");
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const canUsePanelKeys = input.length === 0 || isRunning;
|
|
1206
|
+
if (canUsePanelKeys && key.leftArrow) {
|
|
1207
|
+
setActiveScrollPane("session");
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (canUsePanelKeys && key.rightArrow) {
|
|
1211
|
+
setActiveScrollPane("transcript");
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (canUsePanelKeys && (key.pageUp || key.pageDown || key.home || key.end)) {
|
|
1215
|
+
const setOffset = activeScrollPane === "session" ? setSessionScrollOffset : setTranscriptScrollOffset;
|
|
1216
|
+
if (key.pageUp) {
|
|
1217
|
+
setOffset((currentOffset) => currentOffset + scrollStep);
|
|
1218
|
+
}
|
|
1219
|
+
else if (key.pageDown) {
|
|
1220
|
+
setOffset((currentOffset) => Math.max(0, currentOffset - scrollStep));
|
|
1221
|
+
}
|
|
1222
|
+
else if (key.home) {
|
|
1223
|
+
setOffset(1_000_000);
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
setOffset(0);
|
|
1227
|
+
}
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (!isRunning && key.tab) {
|
|
1231
|
+
toggleMode();
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (!isRunning && input.length === 0 && inputValue === "q") {
|
|
1235
|
+
void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(exit);
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
useEffect(() => {
|
|
1239
|
+
const unloadAndExit = () => {
|
|
1240
|
+
void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(() => {
|
|
1241
|
+
process.exit(0);
|
|
1242
|
+
});
|
|
1243
|
+
};
|
|
1244
|
+
process.once("SIGINT", unloadAndExit);
|
|
1245
|
+
process.once("SIGTERM", unloadAndExit);
|
|
1246
|
+
return () => {
|
|
1247
|
+
process.off("SIGINT", unloadAndExit);
|
|
1248
|
+
process.off("SIGTERM", unloadAndExit);
|
|
1249
|
+
void unloadUsedOllamaModels(usedOllamaModelsRef.current);
|
|
1250
|
+
};
|
|
1251
|
+
}, []);
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
let previousSnapshot = readSystemStats().snapshot;
|
|
1254
|
+
const timer = setInterval(() => {
|
|
1255
|
+
const nextReading = readSystemStats(previousSnapshot);
|
|
1256
|
+
previousSnapshot = nextReading.snapshot;
|
|
1257
|
+
setSystemStats(nextReading.stats);
|
|
1258
|
+
}, 1000);
|
|
1259
|
+
return () => {
|
|
1260
|
+
clearInterval(timer);
|
|
1261
|
+
};
|
|
1262
|
+
}, []);
|
|
1263
|
+
useEffect(() => {
|
|
1264
|
+
let isMounted = true;
|
|
1265
|
+
async function updateGpuStats() {
|
|
1266
|
+
const nextGpuStats = await readGpuStats();
|
|
1267
|
+
if (isMounted) {
|
|
1268
|
+
setGpuStats(nextGpuStats);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
void updateGpuStats();
|
|
1272
|
+
const timer = setInterval(() => {
|
|
1273
|
+
void updateGpuStats();
|
|
1274
|
+
}, 2500);
|
|
1275
|
+
return () => {
|
|
1276
|
+
isMounted = false;
|
|
1277
|
+
clearInterval(timer);
|
|
1278
|
+
};
|
|
1279
|
+
}, []);
|
|
1280
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: rootHeight, overflowY: "hidden", children: [_jsx(Header, { model: settings.model, provider: settings.provider, workspace: settings.workspace, status: status, allowWrite: settings.allowWrite, allowShell: settings.allowShell, agentMode: agentMode, subagents: settings.subagents, thinkingMode: settings.thinkingMode, reasoningEffort: settings.reasoningEffort, ollamaUrl: settings.ollamaUrl, telemetry: telemetry, sessionTelemetry: sessionTelemetry, draftTokens: draftTokens, systemStats: systemStats, gpuStats: gpuStats, activeHost: activeHost }), onboarding ? (_jsx(OnboardingPanel, { state: onboarding, height: panelHeight, selectedIndex: onboardingIndex, input: onboardingInput, busyMessage: onboardingBusyMessage, onInputChange: setOnboardingInput, onInputSubmit: (value) => void handleOnboardingSubmit(value) })) : (_jsxs(Box, { flexDirection: "row", height: panelHeight + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Sidebar, { workspace: settings.workspace, model: settings.model, provider: settings.provider, ollamaUrl: settings.ollamaUrl, agentMode: agentMode, allowWrite: settings.allowWrite, allowShell: settings.allowShell, subagents: settings.subagents, telemetry: telemetry, sessionTelemetry: sessionTelemetry, draftTokens: draftTokens, height: panelHeight, scrollOffset: sessionScrollOffset, advisors: advisorNotes, isActive: activeScrollPane === "session", activeHost: activeHost }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, height: panelHeight + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Transcript, { lines: lines, isRunning: isRunning, isActive: activeScrollPane === "transcript", height: panelHeight, width: transcriptWidth, scrollOffset: transcriptScrollOffset }), _jsx(Composer, { input: input, isRunning: isRunning, status: status, draftTokens: draftTokens, onChange: setInput, onSubmit: (value) => void handleSubmit(value) }), paletteItems.length > 0 ? _jsx(CommandSuggestions, { items: paletteItems, selectedIndex: paletteIndex }) : null, _jsx(FooterHints, { activePane: activeScrollPane })] })] }))] }));
|
|
1281
|
+
}
|
|
1282
|
+
async function loadAvailableModels(provider, ollamaUrl, setModelOptions, refresh = false) {
|
|
1283
|
+
const cacheKey = `${provider}:${provider === "ollama" ? ollamaUrl : "default"}`;
|
|
1284
|
+
const cachedModels = modelCache.get(cacheKey);
|
|
1285
|
+
if (!refresh && cachedModels && cachedModels.expiresAt > Date.now()) {
|
|
1286
|
+
setModelOptions(cachedModels.models);
|
|
1287
|
+
return cachedModels.models;
|
|
1288
|
+
}
|
|
1289
|
+
const models = await createModelClient({
|
|
1290
|
+
provider,
|
|
1291
|
+
ollamaUrl
|
|
1292
|
+
}).listModels();
|
|
1293
|
+
modelCache.set(cacheKey, {
|
|
1294
|
+
models,
|
|
1295
|
+
expiresAt: Date.now() + modelCacheTtlMs
|
|
1296
|
+
});
|
|
1297
|
+
setModelOptions(models);
|
|
1298
|
+
return models;
|
|
1299
|
+
}
|
|
1300
|
+
async function loadKnownOrAvailableModels(provider, ollamaUrl, modelOptions, setModelOptions, appendLine) {
|
|
1301
|
+
try {
|
|
1302
|
+
return modelOptions.length > 0 ? modelOptions : await loadAvailableModels(provider, ollamaUrl, setModelOptions);
|
|
1303
|
+
}
|
|
1304
|
+
catch (error) {
|
|
1305
|
+
appendLine({
|
|
1306
|
+
tone: "danger",
|
|
1307
|
+
label: "models",
|
|
1308
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1309
|
+
});
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendLine, setModelOptions, setSettings, setTelemetry, knownModels) {
|
|
1314
|
+
const installedModels = knownModels ??
|
|
1315
|
+
(await loadAvailableModels(provider, ollamaUrl, setModelOptions).catch((error) => {
|
|
1316
|
+
appendLine({
|
|
1317
|
+
tone: "danger",
|
|
1318
|
+
label: "models",
|
|
1319
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1320
|
+
});
|
|
1321
|
+
return null;
|
|
1322
|
+
}));
|
|
1323
|
+
if (!installedModels) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if (!installedModels.includes(nextModel)) {
|
|
1327
|
+
appendLine({
|
|
1328
|
+
tone: "warning",
|
|
1329
|
+
label: "model",
|
|
1330
|
+
text: `${nextModel} is not available for ${provider}.`,
|
|
1331
|
+
detail: installedModels.length > 0
|
|
1332
|
+
? `Use /models and pick one of:\n${formatModelOptions(installedModels, currentModel)}`
|
|
1333
|
+
: provider === "ollama"
|
|
1334
|
+
? "No models installed on the selected host."
|
|
1335
|
+
: provider === "gemini"
|
|
1336
|
+
? "Check GEMINI_API_KEY in PatchPilot config."
|
|
1337
|
+
: provider === "openrouter"
|
|
1338
|
+
? "Check OPENROUTER_API_KEY in PatchPilot config."
|
|
1339
|
+
: "Run codex login first."
|
|
1340
|
+
});
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
setTelemetry(null);
|
|
1344
|
+
setSettings((currentSettings) => ({
|
|
1345
|
+
...currentSettings,
|
|
1346
|
+
model: nextModel
|
|
1347
|
+
}));
|
|
1348
|
+
savePatchPilotEnvValues({
|
|
1349
|
+
PATCHPILOT_PROVIDER: provider,
|
|
1350
|
+
PATCHPILOT_MODEL: nextModel
|
|
1351
|
+
});
|
|
1352
|
+
appendLine({
|
|
1353
|
+
tone: "success",
|
|
1354
|
+
label: "model",
|
|
1355
|
+
text: `switched to ${nextModel}`
|
|
1356
|
+
});
|
|
1357
|
+
if (provider === "openrouter" && isOpenRouterFreeModel(nextModel)) {
|
|
1358
|
+
appendLine({
|
|
1359
|
+
tone: "warning",
|
|
1360
|
+
label: "openrouter",
|
|
1361
|
+
text: "Free OpenRouter models are rate-limited.",
|
|
1362
|
+
detail: "OpenRouter documents 20 requests/minute for :free models, plus daily limits depending on account credits."
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async function resolveRunnableSettings(settings, modelOptions, appendLine, setModelOptions) {
|
|
1367
|
+
let installedModels;
|
|
1368
|
+
try {
|
|
1369
|
+
installedModels = modelOptions.includes(settings.model)
|
|
1370
|
+
? modelOptions
|
|
1371
|
+
: await loadAvailableModels(settings.provider, settings.ollamaUrl, setModelOptions);
|
|
1372
|
+
}
|
|
1373
|
+
catch (error) {
|
|
1374
|
+
appendLine({
|
|
1375
|
+
tone: "danger",
|
|
1376
|
+
label: settings.provider,
|
|
1377
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1378
|
+
});
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
if (installedModels.includes(settings.model)) {
|
|
1382
|
+
return settings;
|
|
1383
|
+
}
|
|
1384
|
+
appendLine({
|
|
1385
|
+
tone: "warning",
|
|
1386
|
+
label: "model",
|
|
1387
|
+
text: `${settings.model} is not available for ${settings.provider}.`,
|
|
1388
|
+
detail: installedModels.length > 0
|
|
1389
|
+
? `Pick an installed model first:\n${formatModelOptions(installedModels, settings.model)}`
|
|
1390
|
+
: settings.provider === "ollama"
|
|
1391
|
+
? "No models installed on the selected host."
|
|
1392
|
+
: settings.provider === "gemini"
|
|
1393
|
+
? "No Gemini models listed. Check GEMINI_API_KEY in PatchPilot config."
|
|
1394
|
+
: settings.provider === "openrouter"
|
|
1395
|
+
? "No OpenRouter models listed. Check OPENROUTER_API_KEY in PatchPilot config."
|
|
1396
|
+
: "Codex OAuth is not ready. Run codex login."
|
|
1397
|
+
});
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
function buildCommandSuggestionItems(options) {
|
|
1401
|
+
if (!options.input.startsWith("/")) {
|
|
1402
|
+
return [];
|
|
1403
|
+
}
|
|
1404
|
+
const trimmedInput = options.input.trimStart().toLowerCase();
|
|
1405
|
+
const items = filterSlashCommands(options.input)
|
|
1406
|
+
.slice(0, 6)
|
|
1407
|
+
.map((command) => {
|
|
1408
|
+
const baseCommand = `/${command.name}`;
|
|
1409
|
+
return {
|
|
1410
|
+
key: `command-${command.name}`,
|
|
1411
|
+
category: command.category,
|
|
1412
|
+
label: baseCommand,
|
|
1413
|
+
detail: command.description,
|
|
1414
|
+
hint: command.usage.includes("<") || command.usage.includes("[") ? "fill" : "run",
|
|
1415
|
+
command: baseCommand,
|
|
1416
|
+
execute: !command.usage.includes("<") && !command.usage.includes("[")
|
|
1417
|
+
};
|
|
1418
|
+
});
|
|
1419
|
+
if (options.provider === "ollama" && (trimmedInput === "/connect" || trimmedInput.startsWith("/connect ") || trimmedInput.startsWith("/host"))) {
|
|
1420
|
+
if (options.isLoadingHosts) {
|
|
1421
|
+
items.unshift({
|
|
1422
|
+
key: "hosts-loading",
|
|
1423
|
+
category: "host",
|
|
1424
|
+
label: "Loading Hosts",
|
|
1425
|
+
detail: "Scanning LAN and Tailscale peers...",
|
|
1426
|
+
command: "/connect",
|
|
1427
|
+
execute: false
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
else {
|
|
1431
|
+
items.unshift(...options.hostOptions.slice(0, 5).map((host) => ({
|
|
1432
|
+
key: `host-${host.url}`,
|
|
1433
|
+
category: "host",
|
|
1434
|
+
label: host.deviceName,
|
|
1435
|
+
detail: `${host.kind} ${host.url}${host.version ? ` Ollama ${host.version}` : ""}`,
|
|
1436
|
+
command: `/connect ${host.url}`,
|
|
1437
|
+
execute: true
|
|
1438
|
+
})));
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (trimmedInput === "/models" || trimmedInput.startsWith("/models") || trimmedInput === "/model" || trimmedInput.startsWith("/model")) {
|
|
1442
|
+
const modelQuery = trimmedInput.replace(/^\/models?/, "").trim();
|
|
1443
|
+
if (options.isLoadingModels) {
|
|
1444
|
+
items.unshift({
|
|
1445
|
+
key: "models-loading",
|
|
1446
|
+
category: "model",
|
|
1447
|
+
label: "Loading Models",
|
|
1448
|
+
detail: `Fetching ${options.provider} models...`,
|
|
1449
|
+
command: "/models",
|
|
1450
|
+
execute: false
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
items.unshift(...filterModelOptions(modelQuery, options.modelOptions).slice(0, 8).map((model) => ({
|
|
1455
|
+
key: `model-${model}`,
|
|
1456
|
+
category: "model",
|
|
1457
|
+
label: model,
|
|
1458
|
+
detail: `${model === options.currentModel ? "current" : "available"} ${options.provider}`,
|
|
1459
|
+
command: `/model ${model}`,
|
|
1460
|
+
execute: true
|
|
1461
|
+
})));
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return items.slice(0, 8);
|
|
1465
|
+
}
|
|
1466
|
+
function getOnboardingOptionCount(onboarding) {
|
|
1467
|
+
switch (onboarding.step) {
|
|
1468
|
+
case "entry":
|
|
1469
|
+
return 6;
|
|
1470
|
+
case "host":
|
|
1471
|
+
return onboarding.hosts.length + 1;
|
|
1472
|
+
case "api-key-choice":
|
|
1473
|
+
return 2;
|
|
1474
|
+
case "model":
|
|
1475
|
+
return onboarding.models.length;
|
|
1476
|
+
default:
|
|
1477
|
+
return 0;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function readEntrySelection(value, selectedIndex) {
|
|
1481
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
1482
|
+
if (!normalizedValue) {
|
|
1483
|
+
return ["local", "host", "gemini", "openrouter", "nvidia", "codex"][selectedIndex];
|
|
1484
|
+
}
|
|
1485
|
+
if (normalizedValue === "1" || normalizedValue === "local" || normalizedValue === "this device") {
|
|
1486
|
+
return "local";
|
|
1487
|
+
}
|
|
1488
|
+
if (normalizedValue === "2" || normalizedValue === "host" || normalizedValue === "remote host" || normalizedValue === "remote") {
|
|
1489
|
+
return "host";
|
|
1490
|
+
}
|
|
1491
|
+
if (normalizedValue === "3" || normalizedValue === "gemini" || normalizedValue === "google") {
|
|
1492
|
+
return "gemini";
|
|
1493
|
+
}
|
|
1494
|
+
if (normalizedValue === "4" || normalizedValue === "openrouter" || normalizedValue === "open-router") {
|
|
1495
|
+
return "openrouter";
|
|
1496
|
+
}
|
|
1497
|
+
if (normalizedValue === "5" || normalizedValue === "nvidia" || normalizedValue === "nim") {
|
|
1498
|
+
return "nvidia";
|
|
1499
|
+
}
|
|
1500
|
+
if (normalizedValue === "6" || normalizedValue === "codex") {
|
|
1501
|
+
return "codex";
|
|
1502
|
+
}
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
function readIndexedSelection(value, selectedIndex) {
|
|
1506
|
+
const normalizedValue = value.trim();
|
|
1507
|
+
if (!normalizedValue) {
|
|
1508
|
+
return selectedIndex;
|
|
1509
|
+
}
|
|
1510
|
+
const parsedIndex = Number.parseInt(normalizedValue, 10);
|
|
1511
|
+
return Number.isInteger(parsedIndex) ? parsedIndex - 1 : null;
|
|
1512
|
+
}
|
|
1513
|
+
function selectModelFromInput(value, models, selectedIndex, options = {}) {
|
|
1514
|
+
const normalizedValue = normalizeModelAlias(value.trim());
|
|
1515
|
+
if (!normalizedValue && selectedIndex !== undefined) {
|
|
1516
|
+
return models[selectedIndex] ?? null;
|
|
1517
|
+
}
|
|
1518
|
+
if (!normalizedValue) {
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
const modelIndex = Number.parseInt(normalizedValue, 10);
|
|
1522
|
+
if (Number.isInteger(modelIndex)) {
|
|
1523
|
+
return models[modelIndex - 1] ?? null;
|
|
1524
|
+
}
|
|
1525
|
+
if (models.includes(normalizedValue)) {
|
|
1526
|
+
return normalizedValue;
|
|
1527
|
+
}
|
|
1528
|
+
const matches = filterModelOptions(normalizedValue, models);
|
|
1529
|
+
if (matches.length === 1) {
|
|
1530
|
+
return matches[0] ?? null;
|
|
1531
|
+
}
|
|
1532
|
+
return options.allowManual && isPlausibleCloudModelId(normalizedValue) ? normalizedValue : null;
|
|
1533
|
+
}
|
|
1534
|
+
function isPlausibleCloudModelId(value) {
|
|
1535
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(value) && value.length >= 3;
|
|
1536
|
+
}
|
|
1537
|
+
function filterModelOptions(query, models) {
|
|
1538
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1539
|
+
if (!normalizedQuery) {
|
|
1540
|
+
return models;
|
|
1541
|
+
}
|
|
1542
|
+
return models
|
|
1543
|
+
.map((model) => ({
|
|
1544
|
+
model,
|
|
1545
|
+
score: scoreModelMatch(model, normalizedQuery)
|
|
1546
|
+
}))
|
|
1547
|
+
.filter((item) => item.score !== null)
|
|
1548
|
+
.sort((left, right) => left.score - right.score || left.model.localeCompare(right.model))
|
|
1549
|
+
.map((item) => item.model);
|
|
1550
|
+
}
|
|
1551
|
+
function scoreModelMatch(model, query) {
|
|
1552
|
+
const normalizedModel = model.toLowerCase();
|
|
1553
|
+
if (normalizedModel === query) {
|
|
1554
|
+
return 0;
|
|
1555
|
+
}
|
|
1556
|
+
if (normalizedModel.startsWith(query)) {
|
|
1557
|
+
return 1;
|
|
1558
|
+
}
|
|
1559
|
+
if (normalizedModel.includes(query)) {
|
|
1560
|
+
return 2 + normalizedModel.indexOf(query) / 1000;
|
|
1561
|
+
}
|
|
1562
|
+
const tokens = query.split(/[\s/:_-]+/).filter(Boolean);
|
|
1563
|
+
return tokens.length > 0 && tokens.every((token) => normalizedModel.includes(token)) ? 10 : null;
|
|
1564
|
+
}
|
|
1565
|
+
function defaultModelForProvider(provider, currentModel) {
|
|
1566
|
+
if (provider === "nvidia") {
|
|
1567
|
+
return currentModel.includes("/") && !currentModel.startsWith("openrouter/") ? currentModel : defaultNvidiaModel;
|
|
1568
|
+
}
|
|
1569
|
+
if (provider === "openrouter") {
|
|
1570
|
+
return currentModel.includes("/") ? currentModel : defaultOpenRouterModel;
|
|
1571
|
+
}
|
|
1572
|
+
if (provider === "gemini") {
|
|
1573
|
+
return currentModel.startsWith("gemini-") ? currentModel : defaultGeminiModel;
|
|
1574
|
+
}
|
|
1575
|
+
if (provider === "codex") {
|
|
1576
|
+
return currentModel.includes("codex") || currentModel === "codex-mini-latest" ? currentModel : defaultCodexModel;
|
|
1577
|
+
}
|
|
1578
|
+
return currentModel.startsWith("gemini-") || currentModel.includes("codex") || currentModel.includes("/") ? defaultOllamaModel : currentModel;
|
|
1579
|
+
}
|
|
1580
|
+
function openApiKeyChoice(provider, setOnboarding, setOnboardingIndex) {
|
|
1581
|
+
setOnboarding({
|
|
1582
|
+
step: "api-key-choice",
|
|
1583
|
+
provider,
|
|
1584
|
+
hasExistingKey: hasApiKey(provider)
|
|
1585
|
+
});
|
|
1586
|
+
setOnboardingIndex(0);
|
|
1587
|
+
}
|
|
1588
|
+
function needsApiKey(provider) {
|
|
1589
|
+
return provider === "gemini" || provider === "openrouter" || provider === "nvidia";
|
|
1590
|
+
}
|
|
1591
|
+
function hasApiKey(provider) {
|
|
1592
|
+
if (provider === "gemini") {
|
|
1593
|
+
return Boolean(readGeminiApiKey());
|
|
1594
|
+
}
|
|
1595
|
+
if (provider === "openrouter") {
|
|
1596
|
+
return Boolean(readOpenRouterApiKey());
|
|
1597
|
+
}
|
|
1598
|
+
return Boolean(readNvidiaApiKey());
|
|
1599
|
+
}
|
|
1600
|
+
async function unloadUsedOllamaModels(usedModels) {
|
|
1601
|
+
const entries = [...usedModels];
|
|
1602
|
+
usedModels.clear();
|
|
1603
|
+
await Promise.allSettled(entries.map(async (entry) => {
|
|
1604
|
+
const [url, model] = entry.split("|");
|
|
1605
|
+
if (!url || !model) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
await new OllamaClient(url).unloadModel(model);
|
|
1609
|
+
}));
|
|
1610
|
+
}
|
|
1611
|
+
async function ejectOllamaModels(options) {
|
|
1612
|
+
const target = options.target.trim();
|
|
1613
|
+
const client = new OllamaClient(options.settings.ollamaUrl);
|
|
1614
|
+
const models = target === "all"
|
|
1615
|
+
? [
|
|
1616
|
+
...new Set([
|
|
1617
|
+
...[...options.usedModels]
|
|
1618
|
+
.map((entry) => entry.split("|"))
|
|
1619
|
+
.filter(([url]) => url === options.settings.ollamaUrl)
|
|
1620
|
+
.map(([, model]) => model)
|
|
1621
|
+
.filter((model) => Boolean(model)),
|
|
1622
|
+
...(options.activeHost?.runningModels.map((model) => model.name) ?? [])
|
|
1623
|
+
])
|
|
1624
|
+
]
|
|
1625
|
+
: [target || options.settings.model];
|
|
1626
|
+
const ejected = [];
|
|
1627
|
+
for (const model of models) {
|
|
1628
|
+
await client.unloadModel(model).then(() => {
|
|
1629
|
+
ejected.push(model);
|
|
1630
|
+
options.usedModels.delete(`${options.settings.ollamaUrl}|${model}`);
|
|
1631
|
+
}, () => undefined);
|
|
1632
|
+
}
|
|
1633
|
+
return ejected;
|
|
1634
|
+
}
|
|
1635
|
+
function isReasoningEffort(value) {
|
|
1636
|
+
return value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "adaptive";
|
|
1637
|
+
}
|
|
1638
|
+
function upsertAdvisorNote(notes, nextNote) {
|
|
1639
|
+
const nextNotes = notes.filter((note) => note.role !== nextNote.role);
|
|
1640
|
+
return [...nextNotes, nextNote].slice(-2);
|
|
1641
|
+
}
|
|
1642
|
+
function eventToLine(event) {
|
|
1643
|
+
switch (event.type) {
|
|
1644
|
+
case "status":
|
|
1645
|
+
return {
|
|
1646
|
+
tone: "muted",
|
|
1647
|
+
label: "thinking",
|
|
1648
|
+
text: event.message
|
|
1649
|
+
};
|
|
1650
|
+
case "assistant":
|
|
1651
|
+
return {
|
|
1652
|
+
tone: "accent",
|
|
1653
|
+
label: "pilot",
|
|
1654
|
+
text: event.message
|
|
1655
|
+
};
|
|
1656
|
+
case "subagent":
|
|
1657
|
+
return {
|
|
1658
|
+
tone: "accent",
|
|
1659
|
+
label: event.role,
|
|
1660
|
+
text: "advisor brief updated",
|
|
1661
|
+
detail: event.message
|
|
1662
|
+
};
|
|
1663
|
+
case "tool":
|
|
1664
|
+
return {
|
|
1665
|
+
tone: event.ok ? "success" : "warning",
|
|
1666
|
+
label: event.name,
|
|
1667
|
+
text: event.summary
|
|
1668
|
+
};
|
|
1669
|
+
case "final":
|
|
1670
|
+
return {
|
|
1671
|
+
tone: "success",
|
|
1672
|
+
label: "final",
|
|
1673
|
+
text: event.message
|
|
1674
|
+
};
|
|
1675
|
+
case "error":
|
|
1676
|
+
return {
|
|
1677
|
+
tone: "danger",
|
|
1678
|
+
label: "error",
|
|
1679
|
+
text: event.message
|
|
1680
|
+
};
|
|
1681
|
+
case "metrics":
|
|
1682
|
+
return {
|
|
1683
|
+
tone: "muted",
|
|
1684
|
+
label: "metrics",
|
|
1685
|
+
text: formatTokens(event.metrics)
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function eventToStatus(event) {
|
|
1690
|
+
if (event.type === "status") {
|
|
1691
|
+
return event.message;
|
|
1692
|
+
}
|
|
1693
|
+
if (event.type === "tool") {
|
|
1694
|
+
return `${event.name}: ${event.summary}`;
|
|
1695
|
+
}
|
|
1696
|
+
if (event.type === "subagent") {
|
|
1697
|
+
return `${event.role} subagent`;
|
|
1698
|
+
}
|
|
1699
|
+
return event.type;
|
|
1700
|
+
}
|
|
1701
|
+
function formatHostOptions(hosts) {
|
|
1702
|
+
return hosts
|
|
1703
|
+
.map((host, index) => {
|
|
1704
|
+
const version = host.version ? ` Ollama ${host.version}` : "";
|
|
1705
|
+
return `${index + 1}. ${host.deviceName} ${host.kind} ${host.url}${version}`;
|
|
1706
|
+
})
|
|
1707
|
+
.join("\n");
|
|
1708
|
+
}
|
|
1709
|
+
function formatModelOptions(models, currentModel) {
|
|
1710
|
+
return models
|
|
1711
|
+
.map((model, index) => {
|
|
1712
|
+
const currentMarker = model === currentModel ? " current" : "";
|
|
1713
|
+
return `${index + 1}. ${model}${currentMarker}`;
|
|
1714
|
+
})
|
|
1715
|
+
.join("\n");
|
|
1716
|
+
}
|
|
1717
|
+
//# sourceMappingURL=App.js.map
|