@jx-grxf/patchpilot 0.4.0 → 1.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 +17 -1
- package/README.md +113 -23
- package/SECURITY.md +7 -1
- package/dist/cli.js +103 -14
- package/dist/cli.js.map +1 -1
- package/dist/core/agent.d.ts +47 -1
- package/dist/core/agent.js +667 -76
- package/dist/core/agent.js.map +1 -1
- package/dist/core/cleanup.d.ts +3 -0
- package/dist/core/cleanup.js +29 -0
- package/dist/core/cleanup.js.map +1 -0
- package/dist/core/clipboard.d.ts +14 -0
- package/dist/core/clipboard.js +134 -0
- package/dist/core/clipboard.js.map +1 -0
- package/dist/core/codex.d.ts +8 -0
- package/dist/core/codex.js +28 -2
- package/dist/core/codex.js.map +1 -1
- package/dist/core/compaction.d.ts +23 -0
- package/dist/core/compaction.js +145 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/contextFormat.d.ts +21 -0
- package/dist/core/contextFormat.js +87 -0
- package/dist/core/contextFormat.js.map +1 -0
- package/dist/core/contextItem.d.ts +41 -0
- package/dist/core/contextItem.js +93 -0
- package/dist/core/contextItem.js.map +1 -0
- package/dist/core/contextStore.d.ts +48 -0
- package/dist/core/contextStore.js +306 -0
- package/dist/core/contextStore.js.map +1 -0
- package/dist/core/doctor.d.ts +4 -1
- package/dist/core/doctor.js +122 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/gemini.js +10 -4
- package/dist/core/gemini.js.map +1 -1
- package/dist/core/geminiWrapper.d.ts +92 -0
- package/dist/core/geminiWrapper.js +1258 -0
- package/dist/core/geminiWrapper.js.map +1 -0
- package/dist/core/http.js +70 -6
- package/dist/core/http.js.map +1 -1
- package/dist/core/json.d.ts +1 -1
- package/dist/core/json.js +81 -19
- package/dist/core/json.js.map +1 -1
- package/dist/core/memory.d.ts +16 -0
- package/dist/core/memory.js +108 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/modelClient.js +7 -0
- package/dist/core/modelClient.js.map +1 -1
- package/dist/core/nvidia.d.ts +1 -1
- package/dist/core/nvidia.js +13 -4
- package/dist/core/nvidia.js.map +1 -1
- package/dist/core/ollama.js +13 -3
- package/dist/core/ollama.js.map +1 -1
- package/dist/core/openrouter.js +15 -6
- package/dist/core/openrouter.js.map +1 -1
- package/dist/core/projectInit.d.ts +6 -0
- package/dist/core/projectInit.js +44 -0
- package/dist/core/projectInit.js.map +1 -0
- package/dist/core/reasoning.js +6 -0
- package/dist/core/reasoning.js.map +1 -1
- package/dist/core/session.d.ts +1 -0
- package/dist/core/session.js +55 -3
- package/dist/core/session.js.map +1 -1
- package/dist/core/tokenAccounting.d.ts +4 -0
- package/dist/core/tokenAccounting.js +75 -13
- package/dist/core/tokenAccounting.js.map +1 -1
- package/dist/core/types.d.ts +65 -5
- package/dist/core/types.js +30 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/updateCheck.d.ts +19 -0
- package/dist/core/updateCheck.js +103 -0
- package/dist/core/updateCheck.js.map +1 -0
- package/dist/core/workspace.d.ts +37 -0
- package/dist/core/workspace.js +1535 -84
- package/dist/core/workspace.js.map +1 -1
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +1841 -140
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/commands.js +141 -9
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/components/ApprovalPanel.js +16 -1
- package/dist/tui/components/ApprovalPanel.js.map +1 -1
- package/dist/tui/components/CommandSuggestions.js +33 -5
- package/dist/tui/components/CommandSuggestions.js.map +1 -1
- package/dist/tui/components/Composer.d.ts +3 -0
- package/dist/tui/components/Composer.js +57 -5
- package/dist/tui/components/Composer.js.map +1 -1
- package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
- package/dist/tui/components/ExperimentalPanel.js +38 -0
- package/dist/tui/components/ExperimentalPanel.js.map +1 -0
- package/dist/tui/components/Header.js +3 -3
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/OnboardingPanel.d.ts +25 -1
- package/dist/tui/components/OnboardingPanel.js +87 -25
- package/dist/tui/components/OnboardingPanel.js.map +1 -1
- package/dist/tui/components/Sidebar.js +17 -13
- package/dist/tui/components/Sidebar.js.map +1 -1
- package/dist/tui/components/StartupBanner.d.ts +4 -0
- package/dist/tui/components/StartupBanner.js +9 -0
- package/dist/tui/components/StartupBanner.js.map +1 -0
- package/dist/tui/components/Transcript.d.ts +7 -0
- package/dist/tui/components/Transcript.js +87 -17
- package/dist/tui/components/Transcript.js.map +1 -1
- package/dist/tui/contextCommands.d.ts +8 -0
- package/dist/tui/contextCommands.js +205 -0
- package/dist/tui/contextCommands.js.map +1 -0
- package/dist/tui/experimental/AnimatedText.d.ts +38 -0
- package/dist/tui/experimental/AnimatedText.js +55 -0
- package/dist/tui/experimental/AnimatedText.js.map +1 -0
- package/dist/tui/experimental/Banner.d.ts +10 -0
- package/dist/tui/experimental/Banner.js +33 -0
- package/dist/tui/experimental/Banner.js.map +1 -0
- package/dist/tui/experimental/CommandPalette.d.ts +11 -0
- package/dist/tui/experimental/CommandPalette.js +25 -0
- package/dist/tui/experimental/CommandPalette.js.map +1 -0
- package/dist/tui/experimental/ExperimentalShell.d.ts +58 -0
- package/dist/tui/experimental/ExperimentalShell.js +366 -0
- package/dist/tui/experimental/ExperimentalShell.js.map +1 -0
- package/dist/tui/experimental/ThemePicker.d.ts +13 -0
- package/dist/tui/experimental/ThemePicker.js +12 -0
- package/dist/tui/experimental/ThemePicker.js.map +1 -0
- package/dist/tui/experimental/attachments.d.ts +35 -0
- package/dist/tui/experimental/attachments.js +244 -0
- package/dist/tui/experimental/attachments.js.map +1 -0
- package/dist/tui/experimental/composer.d.ts +24 -0
- package/dist/tui/experimental/composer.js +84 -0
- package/dist/tui/experimental/composer.js.map +1 -0
- package/dist/tui/experimental/geminiPricing.d.ts +16 -0
- package/dist/tui/experimental/geminiPricing.js +39 -0
- package/dist/tui/experimental/geminiPricing.js.map +1 -0
- package/dist/tui/experimental/layout.d.ts +46 -0
- package/dist/tui/experimental/layout.js +112 -0
- package/dist/tui/experimental/layout.js.map +1 -0
- package/dist/tui/experimental/theme.d.ts +35 -0
- package/dist/tui/experimental/theme.js +86 -0
- package/dist/tui/experimental/theme.js.map +1 -0
- package/dist/tui/experimental/transcriptRows.d.ts +20 -0
- package/dist/tui/experimental/transcriptRows.js +169 -0
- package/dist/tui/experimental/transcriptRows.js.map +1 -0
- package/dist/tui/experimental/ultraModes.d.ts +46 -0
- package/dist/tui/experimental/ultraModes.js +95 -0
- package/dist/tui/experimental/ultraModes.js.map +1 -0
- package/dist/tui/experimental/ultramaxx.d.ts +19 -0
- package/dist/tui/experimental/ultramaxx.js +43 -0
- package/dist/tui/experimental/ultramaxx.js.map +1 -0
- package/dist/tui/format.d.ts +4 -2
- package/dist/tui/format.js +21 -7
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/hosts.js +7 -1
- package/dist/tui/hosts.js.map +1 -1
- package/dist/tui/layout.d.ts +26 -0
- package/dist/tui/layout.js +66 -0
- package/dist/tui/layout.js.map +1 -0
- package/dist/tui/modelSelection.d.ts +1 -1
- package/dist/tui/modelSelection.js +8 -6
- package/dist/tui/modelSelection.js.map +1 -1
- package/dist/tui/modes.d.ts +8 -1
- package/dist/tui/modes.js +20 -2
- package/dist/tui/modes.js.map +1 -1
- package/dist/tui/onboardingPreferences.d.ts +37 -0
- package/dist/tui/onboardingPreferences.js +118 -0
- package/dist/tui/onboardingPreferences.js.map +1 -0
- package/dist/tui/runStatus.d.ts +50 -0
- package/dist/tui/runStatus.js +164 -0
- package/dist/tui/runStatus.js.map +1 -0
- package/dist/tui/types.d.ts +8 -0
- package/dist/tui/types.js.map +1 -1
- package/docs/architecture.md +115 -0
- package/docs/gemini-wrapper.md +110 -0
- package/docs/product-context.md +43 -0
- package/docs/releases/v0.1.1-beta.md +18 -0
- package/docs/releases/v0.2.1.md +1 -1
- package/docs/releases/v0.3.1-beta.md +4 -0
- package/docs/releases/v0.4.0.md +1 -1
- package/docs/releases/v1.0.0.md +28 -0
- package/docs/releases/v1.0.1.md +25 -0
- package/docs/releases/v1.1.0.md +30 -0
- package/docs/releases/v1.2.0.md +28 -0
- package/docs/showcase/patchpilot-banner.png +0 -0
- package/docs/showcase/patchpilot-logo.png +0 -0
- package/package.json +8 -3
package/dist/tui/App.js
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { statSync } from "node:fs";
|
|
4
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
4
5
|
import { AgentRunner } from "../core/agent.js";
|
|
6
|
+
import { cleanupPatchPilot, readCleanupTarget } from "../core/cleanup.js";
|
|
5
7
|
import { defaultCodexModel, hasCodexCliOAuth } from "../core/codex.js";
|
|
6
8
|
import { describeComputeTarget } from "../core/compute.js";
|
|
9
|
+
import { ContextStore } from "../core/contextStore.js";
|
|
7
10
|
import { runDoctor } from "../core/doctor.js";
|
|
8
11
|
import { savePatchPilotEnvValues } from "../core/env.js";
|
|
9
12
|
import { defaultGeminiModel, readGeminiApiKey } from "../core/gemini.js";
|
|
13
|
+
import { defaultGeminiWrapperModel, geminiWrapperCuratedModels, geminiWrapperShortcutModels, geminiWrapperRequiresApiKey, readGeminiWrapperApiKey, readGeminiWrapperBaseUrl, readGeminiWrapperCookiesJson, readGeminiWrapperMode, readGeminiWrapperPythonCommand, importGeminiWrapperBrowserCookies, saveGeminiWrapperCookieFile } from "../core/geminiWrapper.js";
|
|
10
14
|
import { createModelClient } from "../core/modelClient.js";
|
|
11
15
|
import { defaultNvidiaModel, readNvidiaApiKey } from "../core/nvidia.js";
|
|
12
16
|
import { defaultOllamaModel, OllamaClient } from "../core/ollama.js";
|
|
13
17
|
import { defaultOpenRouterModel, isOpenRouterFreeModel, readOpenRouterApiKey } from "../core/openrouter.js";
|
|
18
|
+
import { ensurePatchPilotGitignore, patchPilotInitPrompt } from "../core/projectInit.js";
|
|
14
19
|
import { formatReasoningSupport } from "../core/reasoning.js";
|
|
15
|
-
import { listWorkspaceSessions, loadSessionSummary, SessionStore } from "../core/session.js";
|
|
16
|
-
import { addTelemetryToSession, emptySessionTelemetry, estimateTokens } from "../core/tokenAccounting.js";
|
|
17
|
-
import {
|
|
20
|
+
import { buildSessionResumeContext, listWorkspaceSessions, loadSessionSummary, SessionStore } from "../core/session.js";
|
|
21
|
+
import { addTelemetryToSession, emptySessionTelemetry, estimateComparableApiCost, estimateTokens } from "../core/tokenAccounting.js";
|
|
22
|
+
import { checkForPatchPilotUpdate, installPatchPilotUpdate } from "../core/updateCheck.js";
|
|
23
|
+
import { getToolSpec, WorkspaceTools } from "../core/workspace.js";
|
|
18
24
|
import { ApprovalPanel } from "./components/ApprovalPanel.js";
|
|
25
|
+
import { clipboardHasImage, clipboardImageHint, readClipboardImage } from "../core/clipboard.js";
|
|
19
26
|
import { CommandSuggestions } from "./components/CommandSuggestions.js";
|
|
20
27
|
import { Composer, FooterHints } from "./components/Composer.js";
|
|
28
|
+
import { ExperimentalPanel, experimentalFlagAt, experimentalFlagCount } from "./components/ExperimentalPanel.js";
|
|
29
|
+
import { runContextSlashCommand } from "./contextCommands.js";
|
|
30
|
+
import { ExperimentalShell } from "./experimental/ExperimentalShell.js";
|
|
31
|
+
import { ThemePicker } from "./experimental/ThemePicker.js";
|
|
32
|
+
import { attachmentKindForPath, attachmentLabel, attachmentTypeForPath, formatSessionArtifactContext } from "./experimental/attachments.js";
|
|
33
|
+
import { describeUltraModes, parseUltraModes } from "./experimental/ultraModes.js";
|
|
34
|
+
import { formatCompletionSummary } from "./runStatus.js";
|
|
21
35
|
import { Header } from "./components/Header.js";
|
|
22
36
|
import { OnboardingPanel } from "./components/OnboardingPanel.js";
|
|
23
37
|
import { Sidebar } from "./components/Sidebar.js";
|
|
@@ -25,21 +39,53 @@ import { Transcript } from "./components/Transcript.js";
|
|
|
25
39
|
import { filterSlashCommands, formatCommandDetail, formatCommandHelp } from "./commands.js";
|
|
26
40
|
import { formatCost, formatSessionTokens, formatTokens, normalizeModelAlias, readToggle } from "./format.js";
|
|
27
41
|
import { checkOllamaHost, discoverOllamaHosts, normalizeOllamaUrl, readOllamaHostDetails, startLocalOllamaAppAndWait } from "./hosts.js";
|
|
28
|
-
import {
|
|
42
|
+
import { computeComposerLayout } from "./layout.js";
|
|
43
|
+
import { initialAgentMode, modeDescription, modePermissionLabel, nextAgentMode, permissionsForMode, shouldBypassApproval } from "./modes.js";
|
|
29
44
|
import { selectableModels } from "./modelSelection.js";
|
|
45
|
+
import { cyclePreference, modePermissions as preferencesModePermissions, preferenceRows, preferencesEnvValues, readOnboardingPreferences } from "./onboardingPreferences.js";
|
|
30
46
|
import { readGpuStats, readSystemStats } from "./systemStats.js";
|
|
31
47
|
import { maxTranscriptLines } from "./types.js";
|
|
48
|
+
const themeOptions = [
|
|
49
|
+
{
|
|
50
|
+
value: "new",
|
|
51
|
+
label: "New",
|
|
52
|
+
description: "Experimental fullscreen shell: compact header, scrolling transcript, command palette, animated run status."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
value: "legacy",
|
|
56
|
+
label: "Legacy",
|
|
57
|
+
description: "Original PatchPilot TUI with the sidebar and split-pane layout."
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
function readUiTheme() {
|
|
61
|
+
return process.env.PATCHPILOT_UI_THEME?.trim().toLowerCase() === "legacy" ? "legacy" : "new";
|
|
62
|
+
}
|
|
63
|
+
/** Heuristic: does this Gemini-Wrapper error look like expired/invalid cookies? */
|
|
64
|
+
function isGeminiCookieError(message) {
|
|
65
|
+
return /cookie|secure_1psid|psidts|expired|sign[ -]?in|auth(?:enticat|oriz)|401|403|session.*(?:invalid|expired)/i.test(message);
|
|
66
|
+
}
|
|
32
67
|
const modelCacheTtlMs = 5 * 60_000;
|
|
33
68
|
const modelCache = new Map();
|
|
69
|
+
const modelDescriptorIndex = new Map();
|
|
34
70
|
export function App(props) {
|
|
35
71
|
const { exit } = useApp();
|
|
36
72
|
const { stdout } = useStdout();
|
|
37
73
|
const [input, setInput] = useState(props.initialTask ?? "");
|
|
38
74
|
const didRunInitialTask = useRef(false);
|
|
39
75
|
const didOpenDefaultOnboarding = useRef(false);
|
|
76
|
+
const didCheckForUpdates = useRef(false);
|
|
40
77
|
const abortControllerRef = useRef(null);
|
|
78
|
+
const softStopRequestedRef = useRef(false);
|
|
79
|
+
const lastEscapeStopAtRef = useRef(0);
|
|
80
|
+
const lastAttachmentWarningRef = useRef("");
|
|
41
81
|
const sessionStoreRef = useRef(new SessionStore({ workspace: props.workspace }));
|
|
82
|
+
const contextStoreRef = useRef(new ContextStore({ workspace: props.workspace, sessionId: sessionStoreRef.current.sessionId }));
|
|
42
83
|
const approvalResolverRef = useRef(null);
|
|
84
|
+
const runtimeStateRef = useRef({
|
|
85
|
+
isRunning: false,
|
|
86
|
+
hasPendingApproval: false,
|
|
87
|
+
lastSigintAt: 0
|
|
88
|
+
});
|
|
43
89
|
const grantedPermissionsRef = useRef({
|
|
44
90
|
allowWrite: props.allowWrite,
|
|
45
91
|
allowShell: props.allowShell
|
|
@@ -47,14 +93,24 @@ export function App(props) {
|
|
|
47
93
|
const activeHostSyncInFlightRef = useRef(false);
|
|
48
94
|
const autoLoadKeysRef = useRef(new Set());
|
|
49
95
|
const usedOllamaModelsRef = useRef(new Set());
|
|
96
|
+
// Rolling session memory: short digests of earlier turns so a later prompt
|
|
97
|
+
// ("now do X") still knows what the user asked for and where.
|
|
98
|
+
const conversationTurnsRef = useRef([]);
|
|
50
99
|
const [lines, setLines] = useState([]);
|
|
51
100
|
const [advisorNotes, setAdvisorNotes] = useState([]);
|
|
101
|
+
const [todos, setTodos] = useState([]);
|
|
102
|
+
const [todoFrame, setTodoFrame] = useState(0);
|
|
103
|
+
const [verbTick, setVerbTick] = useState(0);
|
|
52
104
|
const [isRunning, setIsRunning] = useState(false);
|
|
53
105
|
const [status, setStatus] = useState("idle");
|
|
54
106
|
const [workState, setWorkState] = useState("idle");
|
|
55
107
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
108
|
+
const [updatePrompt, setUpdatePrompt] = useState(null);
|
|
109
|
+
const [updateBusy, setUpdateBusy] = useState(false);
|
|
56
110
|
const [telemetry, setTelemetry] = useState(null);
|
|
57
111
|
const [sessionTelemetry, setSessionTelemetry] = useState(() => emptySessionTelemetry());
|
|
112
|
+
const [toolTelemetry, setToolTelemetry] = useState(() => emptyToolTelemetry());
|
|
113
|
+
const [resumeContext, setResumeContext] = useState("");
|
|
58
114
|
const [systemStats, setSystemStats] = useState(() => readSystemStats().stats);
|
|
59
115
|
const [gpuStats, setGpuStats] = useState(null);
|
|
60
116
|
const [agentMode, setAgentMode] = useState(() => initialAgentMode({ allowWrite: props.allowWrite, allowShell: props.allowShell }));
|
|
@@ -65,6 +121,26 @@ export function App(props) {
|
|
|
65
121
|
const [modelOptions, setModelOptions] = useState([]);
|
|
66
122
|
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
|
67
123
|
const [onboarding, setOnboarding] = useState(null);
|
|
124
|
+
const [experimentalOpen, setExperimentalOpen] = useState(false);
|
|
125
|
+
const [experimentalIndex, setExperimentalIndex] = useState(0);
|
|
126
|
+
const [experimentalFlags, setExperimentalFlags] = useState({
|
|
127
|
+
fileAnalysis: readBooleanEnv(process.env.PATCHPILOT_EXPERIMENTAL_FILE_ANALYSIS, false),
|
|
128
|
+
memory: readBooleanEnv(process.env.PATCHPILOT_EXPERIMENTAL_MEMORY, false),
|
|
129
|
+
subagents: props.subagents,
|
|
130
|
+
shellMetacharacters: readBooleanEnv(process.env.PATCHPILOT_EXPERIMENTAL_SHELL_METACHARACTERS, false)
|
|
131
|
+
});
|
|
132
|
+
const [uiTheme, setUiTheme] = useState(() => readUiTheme());
|
|
133
|
+
const [themePickerOpen, setThemePickerOpen] = useState(false);
|
|
134
|
+
const [themePickerIndex, setThemePickerIndex] = useState(0);
|
|
135
|
+
const [ultramaxxRun, setUltramaxxRun] = useState(false);
|
|
136
|
+
const [reauthPrompt, setReauthPrompt] = useState(null);
|
|
137
|
+
const [reauthBusy, setReauthBusy] = useState(false);
|
|
138
|
+
const [artifacts, setArtifacts] = useState([]);
|
|
139
|
+
const artifactsRef = useRef([]);
|
|
140
|
+
const pendingAttachmentsRef = useRef([]);
|
|
141
|
+
// Tracks whether the "image in clipboard" hint was already shown for the
|
|
142
|
+
// current clipboard contents, so the poll does not repeat it every tick.
|
|
143
|
+
const clipboardHintShownRef = useRef(false);
|
|
68
144
|
const [onboardingIndex, setOnboardingIndex] = useState(0);
|
|
69
145
|
const [onboardingInput, setOnboardingInput] = useState("");
|
|
70
146
|
const [onboardingBusyMessage, setOnboardingBusyMessage] = useState(null);
|
|
@@ -88,7 +164,11 @@ export function App(props) {
|
|
|
88
164
|
const draftTokens = estimateTokens(input);
|
|
89
165
|
const terminalRows = stdout.rows ?? 40;
|
|
90
166
|
const terminalColumns = stdout.columns ?? 120;
|
|
91
|
-
const
|
|
167
|
+
const reauthPromptActive = Boolean(reauthPrompt || reauthBusy);
|
|
168
|
+
const updatePromptActive = !reauthPromptActive && Boolean(updatePrompt || updateBusy);
|
|
169
|
+
const approvalPromptActive = !reauthPromptActive && !updatePromptActive && Boolean(pendingApproval || bypassConfirmation);
|
|
170
|
+
const blockingPromptActive = reauthPromptActive || updatePromptActive || approvalPromptActive;
|
|
171
|
+
const paletteItems = !isRunning && !onboarding && !experimentalOpen && !blockingPromptActive
|
|
92
172
|
? buildCommandSuggestionItems({
|
|
93
173
|
input,
|
|
94
174
|
provider: settings.provider,
|
|
@@ -101,44 +181,239 @@ export function App(props) {
|
|
|
101
181
|
: [];
|
|
102
182
|
const rootHeight = Math.max(24, terminalRows);
|
|
103
183
|
const headerReservedHeight = 5;
|
|
104
|
-
const paletteReservedHeight = !onboarding && paletteItems.length > 0 ? Math.min(8, paletteItems.length) + 4 : 0;
|
|
105
|
-
const composerReservedHeight = onboarding ? 0 : 2;
|
|
106
|
-
const footerReservedHeight = onboarding ? 0 : 1;
|
|
107
|
-
const approvalReservedHeight = !onboarding && (pendingApproval || bypassConfirmation) ? 6 : 0;
|
|
108
|
-
const panelHeight = Math.max(8, rootHeight - headerReservedHeight - composerReservedHeight - paletteReservedHeight - footerReservedHeight - approvalReservedHeight);
|
|
109
184
|
const transcriptWidth = Math.max(42, terminalColumns - 38);
|
|
110
|
-
const
|
|
185
|
+
const paletteReservedHeight = !onboarding && paletteItems.length > 0 ? Math.min(8, paletteItems.length) + 7 : 0;
|
|
186
|
+
const composerReservedHeight = onboarding || experimentalOpen ? 0 : computeComposerLayout({ input, width: transcriptWidth, promptWidth: 8 }).height;
|
|
187
|
+
const footerReservedHeight = onboarding || experimentalOpen ? 0 : 1;
|
|
188
|
+
const approvalReservedHeight = !onboarding && !experimentalOpen && blockingPromptActive ? 7 : 0;
|
|
189
|
+
const bodyHeight = Math.max(8, rootHeight - headerReservedHeight);
|
|
190
|
+
const transcriptHeight = Math.max(4, bodyHeight - composerReservedHeight - paletteReservedHeight - footerReservedHeight - approvalReservedHeight);
|
|
191
|
+
const panelHeight = onboarding || experimentalOpen ? bodyHeight : transcriptHeight;
|
|
192
|
+
const scrollStep = Math.max(4, Math.floor(transcriptHeight * 0.8));
|
|
111
193
|
const appendLine = useCallback((line) => {
|
|
112
194
|
setLines((currentLines) => [
|
|
113
|
-
...currentLines
|
|
195
|
+
...currentLines,
|
|
114
196
|
{
|
|
115
197
|
...line,
|
|
116
198
|
kind: line.kind ?? defaultLogKind(line),
|
|
117
199
|
id: Date.now() + Math.random()
|
|
118
200
|
}
|
|
119
|
-
]);
|
|
201
|
+
].slice(-maxTranscriptLines));
|
|
202
|
+
}, []);
|
|
203
|
+
const pushArtifact = useCallback((artifact) => {
|
|
204
|
+
artifactsRef.current = [...artifactsRef.current, artifact].slice(-40);
|
|
205
|
+
setArtifacts(artifactsRef.current);
|
|
206
|
+
void contextStoreRef.current.append({
|
|
207
|
+
kind: artifact.origin === "attached" ? "attachment" : "artifact",
|
|
208
|
+
source: artifact.origin === "attached" ? "user" : "tool",
|
|
209
|
+
label: artifact.label,
|
|
210
|
+
path: artifact.path,
|
|
211
|
+
priority: artifact.origin === "attached" ? 85 : 75,
|
|
212
|
+
meta: {
|
|
213
|
+
artifactKind: artifact.kind,
|
|
214
|
+
origin: artifact.origin
|
|
215
|
+
}
|
|
216
|
+
}).catch(() => undefined);
|
|
120
217
|
}, []);
|
|
218
|
+
// Registers a pasted document as an attachment chip; returns the chip label
|
|
219
|
+
// ("[PNG #1]") for the composer to insert inline.
|
|
220
|
+
const attachFile = useCallback((path) => {
|
|
221
|
+
const kind = attachmentKindForPath(path) ?? "file";
|
|
222
|
+
const type = attachmentTypeForPath(path);
|
|
223
|
+
const sameType = artifactsRef.current.filter((item) => item.origin === "attached" && attachmentTypeForPath(item.path) === type).length;
|
|
224
|
+
const label = attachmentLabel(kind, sameType + 1, path);
|
|
225
|
+
pushArtifact({ id: Date.now() + Math.random(), kind, path, label, origin: "attached" });
|
|
226
|
+
const nextPendingAttachments = [...pendingAttachmentsRef.current, path];
|
|
227
|
+
pendingAttachmentsRef.current = nextPendingAttachments;
|
|
228
|
+
appendLine({
|
|
229
|
+
tone: "accent",
|
|
230
|
+
label: "attach",
|
|
231
|
+
text: `${label} attached`,
|
|
232
|
+
detail: path
|
|
233
|
+
});
|
|
234
|
+
const warning = attachmentLimitWarning(nextPendingAttachments, settings.provider);
|
|
235
|
+
if (warning && warning !== lastAttachmentWarningRef.current) {
|
|
236
|
+
lastAttachmentWarningRef.current = warning;
|
|
237
|
+
appendLine({
|
|
238
|
+
tone: "warning",
|
|
239
|
+
label: "attach",
|
|
240
|
+
text: warning,
|
|
241
|
+
detail: "For Gemini/Gemini-Wrapper, send large batches in smaller prompts or ask PatchPilot to inspect the files in separate calls."
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return label;
|
|
245
|
+
}, [appendLine, pushArtifact, settings.provider]);
|
|
246
|
+
// Ctrl+V: pull an image straight out of the OS clipboard, save it to a temp
|
|
247
|
+
// file, and attach it — no need to save the screenshot to disk first.
|
|
248
|
+
const handleClipboardImagePaste = useCallback(async () => {
|
|
249
|
+
appendLine({ kind: "status", tone: "muted", label: "clipboard", text: "Zwischenablage wird gelesen…" });
|
|
250
|
+
const imagePath = await readClipboardImage();
|
|
251
|
+
if (!imagePath) {
|
|
252
|
+
appendLine({
|
|
253
|
+
kind: "status",
|
|
254
|
+
tone: "warning",
|
|
255
|
+
label: "clipboard",
|
|
256
|
+
text: "Kein Bild in der Zwischenablage gefunden.",
|
|
257
|
+
detail: "Kopiere ein Bild (z. B. einen Screenshot) und drücke erneut Ctrl+V."
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const label = attachFile(imagePath);
|
|
262
|
+
setInput((current) => {
|
|
263
|
+
if (current.length === 0) {
|
|
264
|
+
return `${label} `;
|
|
265
|
+
}
|
|
266
|
+
return `${current}${current.endsWith(" ") ? "" : " "}${label} `;
|
|
267
|
+
});
|
|
268
|
+
clipboardHintShownRef.current = true;
|
|
269
|
+
}, [appendLine, attachFile]);
|
|
270
|
+
// Watch the OS clipboard while idle: when an image appears, tell the user
|
|
271
|
+
// once that they can attach it with Ctrl+V (reset when the image is gone).
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (isRunning) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
let cancelled = false;
|
|
277
|
+
const poll = async () => {
|
|
278
|
+
const present = await clipboardHasImage();
|
|
279
|
+
if (cancelled) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (present && !clipboardHintShownRef.current) {
|
|
283
|
+
clipboardHintShownRef.current = true;
|
|
284
|
+
appendLine({ kind: "status", tone: "accent", label: "clipboard", text: clipboardImageHint() });
|
|
285
|
+
}
|
|
286
|
+
else if (!present) {
|
|
287
|
+
clipboardHintShownRef.current = false;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
void poll();
|
|
291
|
+
const timer = setInterval(() => void poll(), 7000);
|
|
292
|
+
return () => {
|
|
293
|
+
cancelled = true;
|
|
294
|
+
clearInterval(timer);
|
|
295
|
+
};
|
|
296
|
+
}, [isRunning, appendLine]);
|
|
297
|
+
// Best-effort: record a document PatchPilot wrote during a run.
|
|
298
|
+
const registerCreatedArtifact = useCallback((path) => {
|
|
299
|
+
const kind = attachmentKindForPath(path);
|
|
300
|
+
if (!kind || artifactsRef.current.some((item) => item.path === path)) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const type = attachmentTypeForPath(path);
|
|
304
|
+
const sameType = artifactsRef.current.filter((item) => item.origin === "created" && attachmentTypeForPath(item.path) === type).length;
|
|
305
|
+
pushArtifact({
|
|
306
|
+
id: Date.now() + Math.random(),
|
|
307
|
+
kind,
|
|
308
|
+
path,
|
|
309
|
+
label: attachmentLabel(kind, sameType + 1, path),
|
|
310
|
+
origin: "created"
|
|
311
|
+
});
|
|
312
|
+
}, [pushArtifact]);
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (!isRunning || todos.every((todo) => todo.status !== "in_progress")) {
|
|
315
|
+
setTodoFrame(0);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const timer = setInterval(() => {
|
|
319
|
+
setTodoFrame((currentFrame) => (currentFrame + 1) % 4);
|
|
320
|
+
}, 180);
|
|
321
|
+
return () => {
|
|
322
|
+
clearInterval(timer);
|
|
323
|
+
};
|
|
324
|
+
}, [isRunning, todos]);
|
|
325
|
+
// Slow run-status verb tick: the verb only advances every 10s while the fast
|
|
326
|
+
// spinner glyph keeps animating, so the status line never flickers.
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
if (!isRunning) {
|
|
329
|
+
setVerbTick(randomLegacyVerbIndex());
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
setVerbTick(randomLegacyVerbIndex());
|
|
333
|
+
const timer = setInterval(() => {
|
|
334
|
+
setVerbTick((currentTick) => {
|
|
335
|
+
let nextTick = randomLegacyVerbIndex();
|
|
336
|
+
if (nextTick === currentTick) {
|
|
337
|
+
nextTick += 1;
|
|
338
|
+
}
|
|
339
|
+
return nextTick;
|
|
340
|
+
});
|
|
341
|
+
}, 10_000);
|
|
342
|
+
return () => {
|
|
343
|
+
clearInterval(timer);
|
|
344
|
+
};
|
|
345
|
+
}, [isRunning]);
|
|
121
346
|
const resolveApproval = useCallback((decision) => {
|
|
122
347
|
if (!pendingApproval || !approvalResolverRef.current) {
|
|
123
348
|
return;
|
|
124
349
|
}
|
|
125
350
|
approvalResolverRef.current(decision);
|
|
126
351
|
approvalResolverRef.current = null;
|
|
352
|
+
const nextWorkState = decision === "deny" ? "error" : workStateForApprovalTool(pendingApproval.tool);
|
|
353
|
+
setInput("");
|
|
354
|
+
setStatus(decision === "deny" ? `${pendingApproval.tool} denied` : `${pendingApproval.tool} approved; running`);
|
|
355
|
+
setWorkState(nextWorkState);
|
|
127
356
|
appendLine({
|
|
128
357
|
kind: "approval",
|
|
129
358
|
tone: decision === "deny" ? "warning" : "success",
|
|
130
359
|
label: "approval",
|
|
131
360
|
text: `${pendingApproval.tool} ${decision.replace("_", " ")}`,
|
|
132
361
|
detail: pendingApproval.preview,
|
|
133
|
-
workState:
|
|
362
|
+
workState: nextWorkState,
|
|
134
363
|
tool: pendingApproval.tool
|
|
135
364
|
});
|
|
136
365
|
setPendingApproval(null);
|
|
137
366
|
}, [appendLine, pendingApproval]);
|
|
367
|
+
const resolveUpdatePrompt = useCallback(async (accept) => {
|
|
368
|
+
const pending = updatePrompt;
|
|
369
|
+
if (!pending || updateBusy) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!accept) {
|
|
373
|
+
setUpdatePrompt(null);
|
|
374
|
+
setStatus("idle");
|
|
375
|
+
appendLine({
|
|
376
|
+
tone: "muted",
|
|
377
|
+
label: "update",
|
|
378
|
+
text: `Skipped PatchPilot ${pending.latestVersion}.`,
|
|
379
|
+
detail: `Manual command: ${pending.command}`
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
setUpdateBusy(true);
|
|
384
|
+
setStatus(`updating to ${pending.latestVersion}`);
|
|
385
|
+
appendLine({
|
|
386
|
+
tone: "accent",
|
|
387
|
+
label: "update",
|
|
388
|
+
text: `Running ${pending.command}`
|
|
389
|
+
});
|
|
390
|
+
try {
|
|
391
|
+
const result = await installPatchPilotUpdate(pending.latestVersion);
|
|
392
|
+
setUpdatePrompt(null);
|
|
393
|
+
appendLine({
|
|
394
|
+
tone: "success",
|
|
395
|
+
label: "update",
|
|
396
|
+
text: `⚡ Successfully updated to v${result.version}. Please restart PatchPilot.`,
|
|
397
|
+
detail: result.command
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
appendLine({
|
|
402
|
+
tone: "danger",
|
|
403
|
+
label: "update",
|
|
404
|
+
text: error instanceof Error ? error.message : String(error),
|
|
405
|
+
detail: `Automatic update failed. Manual command: ${pending.command}`
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
setUpdateBusy(false);
|
|
410
|
+
}
|
|
411
|
+
}, [appendLine, updateBusy, updatePrompt]);
|
|
138
412
|
const applyMode = useCallback((nextMode, announce = true) => {
|
|
139
413
|
const permissions = permissionsForMode(nextMode);
|
|
140
414
|
setAgentMode(nextMode);
|
|
141
415
|
setBypassConfirmation(false);
|
|
416
|
+
grantedPermissionsRef.current = permissions;
|
|
142
417
|
setSettings((currentSettings) => ({
|
|
143
418
|
...currentSettings,
|
|
144
419
|
allowWrite: permissions.allowWrite,
|
|
@@ -166,9 +441,24 @@ export function App(props) {
|
|
|
166
441
|
setWorkState("waiting_approval");
|
|
167
442
|
}, [bypassConfirmation]);
|
|
168
443
|
const confirmBypassMode = useCallback(() => {
|
|
444
|
+
setInput("");
|
|
169
445
|
applyMode("bypass");
|
|
170
446
|
}, [applyMode]);
|
|
447
|
+
const setExplicitPermission = useCallback((permission, enabled) => {
|
|
448
|
+
const nextPermissions = {
|
|
449
|
+
allowWrite: permission === "write" ? enabled : settings.allowWrite,
|
|
450
|
+
allowShell: permission === "shell" ? enabled : settings.allowShell
|
|
451
|
+
};
|
|
452
|
+
grantedPermissionsRef.current = nextPermissions;
|
|
453
|
+
setBypassConfirmation(false);
|
|
454
|
+
setAgentMode(nextPermissions.allowWrite && nextPermissions.allowShell ? "bypass" : nextPermissions.allowWrite || nextPermissions.allowShell ? "build" : "plan");
|
|
455
|
+
setSettings((currentSettings) => ({
|
|
456
|
+
...currentSettings,
|
|
457
|
+
...nextPermissions
|
|
458
|
+
}));
|
|
459
|
+
}, [settings.allowShell, settings.allowWrite]);
|
|
171
460
|
const cancelBypassMode = useCallback(() => {
|
|
461
|
+
setInput("");
|
|
172
462
|
setBypassConfirmation(false);
|
|
173
463
|
setStatus("idle");
|
|
174
464
|
setWorkState("idle");
|
|
@@ -258,6 +548,7 @@ export function App(props) {
|
|
|
258
548
|
setModelOptions(details.models);
|
|
259
549
|
modelCache.set(`ollama:${verifiedHost.url}`, {
|
|
260
550
|
models: details.models,
|
|
551
|
+
descriptors: details.models.map((model) => ({ id: model, displayName: model })),
|
|
261
552
|
expiresAt: Date.now() + modelCacheTtlMs
|
|
262
553
|
});
|
|
263
554
|
setSettings((currentSettings) => ({
|
|
@@ -291,13 +582,14 @@ export function App(props) {
|
|
|
291
582
|
setTelemetry(null);
|
|
292
583
|
setOnboardingInput("");
|
|
293
584
|
setOnboardingNotice(null);
|
|
294
|
-
setOnboardingBusyMessage(
|
|
585
|
+
setOnboardingBusyMessage(null);
|
|
295
586
|
const nextModel = defaultModelForProvider(provider, options.currentModel ?? settings.model);
|
|
296
587
|
setSettings((currentSettings) => ({
|
|
297
588
|
...currentSettings,
|
|
298
589
|
provider,
|
|
299
590
|
model: nextModel
|
|
300
591
|
}));
|
|
592
|
+
setOnboardingBusyMessage(`Loading ${provider} models...`);
|
|
301
593
|
try {
|
|
302
594
|
const models = await loadAvailableModels(provider, options.ollamaUrl ?? settings.ollamaUrl, setModelOptions, true);
|
|
303
595
|
if (models.length === 0) {
|
|
@@ -307,9 +599,13 @@ export function App(props) {
|
|
|
307
599
|
? "No Ollama models found on that host."
|
|
308
600
|
: provider === "gemini"
|
|
309
601
|
? "No Gemini models listed. Check the API key."
|
|
310
|
-
: provider === "
|
|
311
|
-
? "No
|
|
312
|
-
:
|
|
602
|
+
: provider === "gemini-wrapper"
|
|
603
|
+
? "No Gemini-Wrapper models listed. Check the bridge install and cookie setup."
|
|
604
|
+
: provider === "openrouter"
|
|
605
|
+
? "No OpenRouter models listed. Check the API key."
|
|
606
|
+
: provider === "nvidia"
|
|
607
|
+
? "No NVIDIA models listed. Check the API key."
|
|
608
|
+
: "No Codex OAuth models listed.",
|
|
313
609
|
detail: "Use the back key to choose another provider or retry after fixing the provider setup."
|
|
314
610
|
});
|
|
315
611
|
return;
|
|
@@ -349,12 +645,25 @@ export function App(props) {
|
|
|
349
645
|
setOnboardingNotice(null);
|
|
350
646
|
setOnboardingIndex(0);
|
|
351
647
|
switch (onboarding.step) {
|
|
648
|
+
case "welcome":
|
|
649
|
+
setOnboarding(null);
|
|
650
|
+
return;
|
|
651
|
+
case "disclaimer":
|
|
652
|
+
setOnboarding({
|
|
653
|
+
step: "welcome"
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
352
656
|
case "entry":
|
|
353
657
|
setOnboarding(null);
|
|
354
658
|
return;
|
|
355
659
|
case "host":
|
|
356
660
|
case "api-key-choice":
|
|
357
661
|
case "gemini-key":
|
|
662
|
+
case "gemini-wrapper-url":
|
|
663
|
+
case "gemini-wrapper-psid":
|
|
664
|
+
case "gemini-wrapper-psidts":
|
|
665
|
+
case "gemini-wrapper-model-mode":
|
|
666
|
+
case "gemini-wrapper-key":
|
|
358
667
|
case "openrouter-key":
|
|
359
668
|
case "nvidia-key":
|
|
360
669
|
case "codex-login":
|
|
@@ -368,6 +677,15 @@ export function App(props) {
|
|
|
368
677
|
hosts: hostOptions
|
|
369
678
|
});
|
|
370
679
|
return;
|
|
680
|
+
case "preferences":
|
|
681
|
+
if (onboarding.provider === "gemini-wrapper") {
|
|
682
|
+
setOnboarding({
|
|
683
|
+
step: "gemini-wrapper-model-mode"
|
|
684
|
+
});
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
void openModelSelection(onboarding.provider, { currentModel: onboarding.model });
|
|
688
|
+
return;
|
|
371
689
|
case "model":
|
|
372
690
|
if (onboarding.provider === "ollama" && activeHost?.host.kind !== "local") {
|
|
373
691
|
setOnboarding({
|
|
@@ -380,6 +698,12 @@ export function App(props) {
|
|
|
380
698
|
openApiKeyChoice("gemini", setOnboarding, setOnboardingIndex);
|
|
381
699
|
return;
|
|
382
700
|
}
|
|
701
|
+
if (onboarding.provider === "gemini-wrapper") {
|
|
702
|
+
setOnboarding({
|
|
703
|
+
step: "gemini-wrapper-model-mode"
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
383
707
|
if (onboarding.provider === "nvidia") {
|
|
384
708
|
openApiKeyChoice("nvidia", setOnboarding, setOnboardingIndex);
|
|
385
709
|
return;
|
|
@@ -398,7 +722,7 @@ export function App(props) {
|
|
|
398
722
|
step: "entry"
|
|
399
723
|
});
|
|
400
724
|
}
|
|
401
|
-
}, [activeHost?.host.kind, hostOptions, onboarding]);
|
|
725
|
+
}, [activeHost?.host.kind, hostOptions, onboarding, openModelSelection]);
|
|
402
726
|
const handleOnboardingSubmit = useCallback(async (value) => {
|
|
403
727
|
if (!onboarding) {
|
|
404
728
|
return;
|
|
@@ -407,6 +731,33 @@ export function App(props) {
|
|
|
407
731
|
return;
|
|
408
732
|
}
|
|
409
733
|
setOnboardingNotice(null);
|
|
734
|
+
if (onboarding.step === "welcome") {
|
|
735
|
+
setOnboarding({
|
|
736
|
+
step: "disclaimer"
|
|
737
|
+
});
|
|
738
|
+
setOnboardingIndex(0);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (onboarding.step === "disclaimer") {
|
|
742
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
743
|
+
if (normalizedValue !== "y" && normalizedValue !== "yes" && normalizedValue !== "1") {
|
|
744
|
+
setOnboardingNotice({
|
|
745
|
+
tone: "warning",
|
|
746
|
+
text: "Accept the use-at-your-own-risk notice to continue.",
|
|
747
|
+
detail: "Press y to continue, or Escape to go back."
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
savePatchPilotEnvValues({
|
|
752
|
+
PATCHPILOT_DISCLAIMER_ACCEPTED: "2026-05-22"
|
|
753
|
+
});
|
|
754
|
+
process.env.PATCHPILOT_DISCLAIMER_ACCEPTED = "2026-05-22";
|
|
755
|
+
setOnboarding({
|
|
756
|
+
step: "entry"
|
|
757
|
+
});
|
|
758
|
+
setOnboardingIndex(0);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
410
761
|
if (onboarding.step === "entry") {
|
|
411
762
|
const selection = readEntrySelection(value, onboardingIndex);
|
|
412
763
|
if (!selection) {
|
|
@@ -452,7 +803,7 @@ export function App(props) {
|
|
|
452
803
|
}
|
|
453
804
|
return;
|
|
454
805
|
}
|
|
455
|
-
if (selection === "gemini" || selection === "openrouter" || selection === "nvidia") {
|
|
806
|
+
if (selection === "gemini" || selection === "gemini-wrapper" || selection === "openrouter" || selection === "nvidia") {
|
|
456
807
|
openApiKeyChoice(selection, setOnboarding, setOnboardingIndex);
|
|
457
808
|
return;
|
|
458
809
|
}
|
|
@@ -537,6 +888,58 @@ export function App(props) {
|
|
|
537
888
|
if (choice === null) {
|
|
538
889
|
return;
|
|
539
890
|
}
|
|
891
|
+
if (onboarding.provider === "gemini-wrapper") {
|
|
892
|
+
if (choice === 0 && onboarding.hasExistingKey) {
|
|
893
|
+
setOnboarding({
|
|
894
|
+
step: "gemini-wrapper-model-mode"
|
|
895
|
+
});
|
|
896
|
+
setOnboardingInput("");
|
|
897
|
+
setOnboardingIndex(0);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const importChoice = onboarding.hasExistingKey ? 1 : 0;
|
|
901
|
+
if (choice === importChoice) {
|
|
902
|
+
setOnboardingBusyMessage("Importing Gemini browser cookies...");
|
|
903
|
+
try {
|
|
904
|
+
const result = await importGeminiWrapperBrowserCookies();
|
|
905
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_MODE = "python";
|
|
906
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON = result.cookiesPath;
|
|
907
|
+
savePatchPilotEnvValues({
|
|
908
|
+
PATCHPILOT_PROVIDER: "gemini-wrapper",
|
|
909
|
+
PATCHPILOT_MODEL: defaultGeminiWrapperModel,
|
|
910
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "python",
|
|
911
|
+
PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON: result.cookiesPath
|
|
912
|
+
});
|
|
913
|
+
setOnboardingNotice({
|
|
914
|
+
tone: "success",
|
|
915
|
+
text: `Imported ${result.cookieCount} Gemini browser cookies from ${result.source}.`,
|
|
916
|
+
detail: `${result.cookiesPath} was written with owner-only permissions. Secret values were not printed.`
|
|
917
|
+
});
|
|
918
|
+
setOnboarding({
|
|
919
|
+
step: "gemini-wrapper-model-mode"
|
|
920
|
+
});
|
|
921
|
+
setOnboardingInput("");
|
|
922
|
+
setOnboardingIndex(0);
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
setOnboardingNotice({
|
|
926
|
+
tone: "warning",
|
|
927
|
+
text: "Gemini browser cookie import failed.",
|
|
928
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
finally {
|
|
932
|
+
setOnboardingBusyMessage(null);
|
|
933
|
+
}
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
setOnboarding({
|
|
937
|
+
step: "gemini-wrapper-psid"
|
|
938
|
+
});
|
|
939
|
+
setOnboardingInput("");
|
|
940
|
+
setOnboardingIndex(0);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
540
943
|
if (choice === 0 && onboarding.hasExistingKey) {
|
|
541
944
|
await openModelSelection(onboarding.provider, {
|
|
542
945
|
currentModel: defaultModelForProvider(onboarding.provider, settings.model)
|
|
@@ -574,6 +977,159 @@ export function App(props) {
|
|
|
574
977
|
});
|
|
575
978
|
return;
|
|
576
979
|
}
|
|
980
|
+
if (onboarding.step === "gemini-wrapper-psid") {
|
|
981
|
+
const secure1psid = value.trim();
|
|
982
|
+
if (!secure1psid) {
|
|
983
|
+
setOnboardingNotice({
|
|
984
|
+
tone: "warning",
|
|
985
|
+
text: "__Secure-1PSID cannot be empty.",
|
|
986
|
+
detail: "Paste the cookie value manually. PatchPilot will not scan browser profiles."
|
|
987
|
+
});
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
setOnboarding({
|
|
991
|
+
step: "gemini-wrapper-psidts",
|
|
992
|
+
secure1psid
|
|
993
|
+
});
|
|
994
|
+
setOnboardingInput("");
|
|
995
|
+
setOnboardingIndex(0);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (onboarding.step === "gemini-wrapper-psidts") {
|
|
999
|
+
const secure1psidts = value.trim();
|
|
1000
|
+
const cookiesPath = saveGeminiWrapperCookieFile({
|
|
1001
|
+
secure1psid: onboarding.secure1psid,
|
|
1002
|
+
secure1psidts
|
|
1003
|
+
});
|
|
1004
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_MODE = "python";
|
|
1005
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON = cookiesPath;
|
|
1006
|
+
savePatchPilotEnvValues({
|
|
1007
|
+
PATCHPILOT_PROVIDER: "gemini-wrapper",
|
|
1008
|
+
PATCHPILOT_MODEL: defaultGeminiWrapperModel,
|
|
1009
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "python",
|
|
1010
|
+
PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON: cookiesPath
|
|
1011
|
+
});
|
|
1012
|
+
setOnboardingNotice({
|
|
1013
|
+
tone: "success",
|
|
1014
|
+
text: "Gemini-API bridge cookies saved to PatchPilot config.",
|
|
1015
|
+
detail: `${cookiesPath} was written with owner-only permissions. PatchPilot will run gemini_webapi through python3.`
|
|
1016
|
+
});
|
|
1017
|
+
setOnboarding({
|
|
1018
|
+
step: "gemini-wrapper-model-mode"
|
|
1019
|
+
});
|
|
1020
|
+
setOnboardingInput("");
|
|
1021
|
+
setOnboardingIndex(0);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (onboarding.step === "gemini-wrapper-model-mode") {
|
|
1025
|
+
const choice = readIndexedSelection(value, onboardingIndex);
|
|
1026
|
+
if (choice === null) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const curatedModel = geminiWrapperShortcutModels[choice];
|
|
1030
|
+
if (curatedModel) {
|
|
1031
|
+
setTelemetry(null);
|
|
1032
|
+
const shortcutDescriptors = geminiWrapperShortcutModels.map((model) => ({ id: model, displayName: model }));
|
|
1033
|
+
rememberModelDescriptors(shortcutDescriptors);
|
|
1034
|
+
setModelOptions([...geminiWrapperShortcutModels]);
|
|
1035
|
+
setSettings((currentSettings) => ({
|
|
1036
|
+
...currentSettings,
|
|
1037
|
+
provider: "gemini-wrapper",
|
|
1038
|
+
model: curatedModel
|
|
1039
|
+
}));
|
|
1040
|
+
savePatchPilotEnvValues({
|
|
1041
|
+
PATCHPILOT_PROVIDER: "gemini-wrapper",
|
|
1042
|
+
PATCHPILOT_MODEL: curatedModel,
|
|
1043
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "python"
|
|
1044
|
+
});
|
|
1045
|
+
setOnboarding({
|
|
1046
|
+
step: "preferences",
|
|
1047
|
+
provider: "gemini-wrapper",
|
|
1048
|
+
model: curatedModel,
|
|
1049
|
+
preferences: readOnboardingPreferences()
|
|
1050
|
+
});
|
|
1051
|
+
setOnboardingInput("");
|
|
1052
|
+
setOnboardingIndex(preferenceRows.length);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
await openModelSelection("gemini-wrapper", {
|
|
1056
|
+
currentModel: settings.model
|
|
1057
|
+
});
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (onboarding.step === "gemini-wrapper-url") {
|
|
1061
|
+
const baseUrl = value.trim().replace(/\/$/, "");
|
|
1062
|
+
if (!baseUrl) {
|
|
1063
|
+
setOnboardingNotice({
|
|
1064
|
+
tone: "warning",
|
|
1065
|
+
text: "Gemini-Wrapper URL cannot be empty."
|
|
1066
|
+
});
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
try {
|
|
1070
|
+
new URL(baseUrl);
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
setOnboardingNotice({
|
|
1074
|
+
tone: "warning",
|
|
1075
|
+
text: "Gemini-Wrapper URL must be a valid URL.",
|
|
1076
|
+
detail: "Example: http://localhost:8787/v1"
|
|
1077
|
+
});
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_BASE_URL = baseUrl;
|
|
1081
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_MODE = "http";
|
|
1082
|
+
savePatchPilotEnvValues({
|
|
1083
|
+
PATCHPILOT_PROVIDER: "gemini-wrapper",
|
|
1084
|
+
PATCHPILOT_MODEL: defaultGeminiWrapperModel,
|
|
1085
|
+
PATCHPILOT_GEMINI_WRAPPER_BASE_URL: baseUrl,
|
|
1086
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "http"
|
|
1087
|
+
});
|
|
1088
|
+
setOnboardingNotice({
|
|
1089
|
+
tone: "success",
|
|
1090
|
+
text: "Gemini-Wrapper URL saved to PatchPilot config.",
|
|
1091
|
+
detail: "PatchPilot uses only this explicit URL and never reads browser cookies."
|
|
1092
|
+
});
|
|
1093
|
+
if (geminiWrapperRequiresApiKey(baseUrl) && !readGeminiWrapperApiKey()) {
|
|
1094
|
+
setOnboarding({
|
|
1095
|
+
step: "gemini-wrapper-key",
|
|
1096
|
+
baseUrl
|
|
1097
|
+
});
|
|
1098
|
+
setOnboardingInput("");
|
|
1099
|
+
setOnboardingIndex(0);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
await openModelSelection("gemini-wrapper", {
|
|
1103
|
+
currentModel: defaultGeminiWrapperModel
|
|
1104
|
+
});
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (onboarding.step === "gemini-wrapper-key") {
|
|
1108
|
+
const apiKey = value.trim();
|
|
1109
|
+
if (geminiWrapperRequiresApiKey(onboarding.baseUrl) && !apiKey) {
|
|
1110
|
+
setOnboardingNotice({
|
|
1111
|
+
tone: "warning",
|
|
1112
|
+
text: "Gemini-Wrapper API key cannot be empty for remote wrapper URLs."
|
|
1113
|
+
});
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_API_KEY = apiKey;
|
|
1117
|
+
savePatchPilotEnvValues({
|
|
1118
|
+
PATCHPILOT_PROVIDER: "gemini-wrapper",
|
|
1119
|
+
PATCHPILOT_MODEL: defaultGeminiWrapperModel,
|
|
1120
|
+
PATCHPILOT_GEMINI_WRAPPER_BASE_URL: onboarding.baseUrl,
|
|
1121
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "http",
|
|
1122
|
+
...(apiKey ? { PATCHPILOT_GEMINI_WRAPPER_API_KEY: apiKey } : {})
|
|
1123
|
+
});
|
|
1124
|
+
setOnboardingNotice({
|
|
1125
|
+
tone: "success",
|
|
1126
|
+
text: apiKey ? "Gemini-Wrapper API key saved to PatchPilot config." : "Gemini-Wrapper local URL saved without an API key."
|
|
1127
|
+
});
|
|
1128
|
+
await openModelSelection("gemini-wrapper", {
|
|
1129
|
+
currentModel: defaultGeminiWrapperModel
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
577
1133
|
if (onboarding.step === "openrouter-key") {
|
|
578
1134
|
const apiKey = value.trim();
|
|
579
1135
|
if (!apiKey) {
|
|
@@ -636,9 +1192,56 @@ export function App(props) {
|
|
|
636
1192
|
});
|
|
637
1193
|
return;
|
|
638
1194
|
}
|
|
639
|
-
|
|
1195
|
+
if (onboarding.step === "preferences") {
|
|
1196
|
+
const confirmIndex = preferenceRows.length;
|
|
1197
|
+
const selection = readIndexedSelection(value, onboardingIndex);
|
|
1198
|
+
if (selection !== confirmIndex) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const prefs = onboarding.preferences;
|
|
1202
|
+
const permissions = preferencesModePermissions(prefs.mode);
|
|
1203
|
+
setTelemetry(null);
|
|
1204
|
+
setAgentMode(prefs.mode);
|
|
1205
|
+
grantedPermissionsRef.current = permissions;
|
|
1206
|
+
setExperimentalFlags((currentFlags) => ({ ...currentFlags, subagents: prefs.subagents }));
|
|
1207
|
+
setSettings((currentSettings) => ({
|
|
1208
|
+
...currentSettings,
|
|
1209
|
+
provider: onboarding.provider,
|
|
1210
|
+
model: onboarding.model,
|
|
1211
|
+
allowWrite: permissions.allowWrite,
|
|
1212
|
+
allowShell: permissions.allowShell,
|
|
1213
|
+
thinkingMode: prefs.thinking,
|
|
1214
|
+
reasoningEffort: prefs.reasoning,
|
|
1215
|
+
subagents: prefs.subagents
|
|
1216
|
+
}));
|
|
1217
|
+
savePatchPilotEnvValues({
|
|
1218
|
+
PATCHPILOT_PROVIDER: onboarding.provider,
|
|
1219
|
+
PATCHPILOT_MODEL: onboarding.model,
|
|
1220
|
+
PATCHPILOT_ONBOARDING_COMPLETE: "1",
|
|
1221
|
+
...preferencesEnvValues(prefs),
|
|
1222
|
+
...(onboarding.provider === "ollama" ? { PATCHPILOT_OLLAMA_URL: activeHost?.host.url ?? settings.ollamaUrl } : {})
|
|
1223
|
+
});
|
|
1224
|
+
process.env.PATCHPILOT_ONBOARDING_COMPLETE = "1";
|
|
1225
|
+
appendLine({
|
|
1226
|
+
tone: "success",
|
|
1227
|
+
label: "onboarding",
|
|
1228
|
+
text: `ready: ${onboarding.provider} using ${onboarding.model}`,
|
|
1229
|
+
detail: `mode ${prefs.mode} · reasoning ${prefs.reasoning} · thinking ${prefs.thinking} · subagents ${prefs.subagents ? "on" : "off"}`
|
|
1230
|
+
});
|
|
1231
|
+
if (onboarding.provider === "openrouter" && isOpenRouterFreeModel(onboarding.model)) {
|
|
1232
|
+
appendLine({
|
|
1233
|
+
tone: "warning",
|
|
1234
|
+
label: "openrouter",
|
|
1235
|
+
text: "Free OpenRouter models are rate-limited.",
|
|
1236
|
+
detail: "OpenRouter documents 20 requests/minute for :free models, plus daily limits depending on account credits."
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
closeOnboarding();
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const visibleModels = selectableModels(onboardingInput, onboarding.models, formatModelLabel);
|
|
640
1243
|
const selectedModel = visibleModels[onboardingIndex] ?? selectModelFromInput(value, visibleModels, onboardingIndex, {
|
|
641
|
-
allowManual: onboarding.provider !== "ollama"
|
|
1244
|
+
allowManual: onboarding.provider !== "ollama" && onboarding.provider !== "gemini-wrapper"
|
|
642
1245
|
});
|
|
643
1246
|
if (!selectedModel) {
|
|
644
1247
|
setOnboardingNotice({
|
|
@@ -647,39 +1250,65 @@ export function App(props) {
|
|
|
647
1250
|
});
|
|
648
1251
|
return;
|
|
649
1252
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
...currentSettings,
|
|
1253
|
+
setOnboarding({
|
|
1254
|
+
step: "preferences",
|
|
653
1255
|
provider: onboarding.provider,
|
|
654
|
-
model: selectedModel
|
|
655
|
-
|
|
656
|
-
savePatchPilotEnvValues({
|
|
657
|
-
PATCHPILOT_PROVIDER: onboarding.provider,
|
|
658
|
-
PATCHPILOT_MODEL: selectedModel,
|
|
659
|
-
PATCHPILOT_ONBOARDING_COMPLETE: "1",
|
|
660
|
-
...(onboarding.provider === "ollama" ? { PATCHPILOT_OLLAMA_URL: activeHost?.host.url ?? settings.ollamaUrl } : {})
|
|
661
|
-
});
|
|
662
|
-
appendLine({
|
|
663
|
-
tone: "success",
|
|
664
|
-
label: "onboarding",
|
|
665
|
-
text: `ready: ${onboarding.provider} using ${selectedModel}`
|
|
1256
|
+
model: selectedModel,
|
|
1257
|
+
preferences: readOnboardingPreferences()
|
|
666
1258
|
});
|
|
667
|
-
|
|
1259
|
+
setOnboardingInput("");
|
|
1260
|
+
setOnboardingIndex(preferenceRows.length);
|
|
1261
|
+
}, [activeHost?.host.url, appendLine, closeOnboarding, connectToHost, loadHostSuggestions, onboarding, onboardingBusyMessage, onboardingIndex, openModelSelection, settings.ollamaUrl]);
|
|
1262
|
+
const runTask = useCallback(async (task, overrides = {}) => {
|
|
1263
|
+
if (!task.trim() || isRunning) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
// Ultra-modes: power-mode keywords (ultramaxx / ultracheap / ultrafocus /
|
|
1267
|
+
// ultraloop) found anywhere in the prompt. Several may combine; an
|
|
1268
|
+
// incompatible pair blocks the send so a contradictory run never starts.
|
|
1269
|
+
const ultra = parseUltraModes(task);
|
|
1270
|
+
if (ultra.conflict) {
|
|
1271
|
+
appendLine({
|
|
1272
|
+
kind: "status",
|
|
1273
|
+
tone: "danger",
|
|
1274
|
+
label: "ultra",
|
|
1275
|
+
text: ultra.conflict,
|
|
1276
|
+
detail: "Remove one of the conflicting keywords, then send again."
|
|
1277
|
+
});
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const ultramaxx = ultra.modes.includes("maxx");
|
|
1281
|
+
const ultracheap = ultra.modes.includes("cheap");
|
|
1282
|
+
const ultrafast = ultra.modes.includes("fast");
|
|
1283
|
+
const ultrafocus = ultra.modes.includes("focus");
|
|
1284
|
+
const ultraloop = ultra.modes.includes("loop");
|
|
1285
|
+
// ultracheap and ultrafast both run the lean pipeline (low reasoning,
|
|
1286
|
+
// fixed short thinking, no advisors, capped steps).
|
|
1287
|
+
const ultraLean = ultracheap || ultrafast;
|
|
1288
|
+
const effectiveTask = ultra.modes.length > 0 ? ultra.cleaned : task;
|
|
1289
|
+
if (ultra.modes.length > 0 && !effectiveTask) {
|
|
668
1290
|
appendLine({
|
|
669
1291
|
tone: "warning",
|
|
670
|
-
label: "
|
|
671
|
-
text:
|
|
672
|
-
detail: "OpenRouter documents 20 requests/minute for :free models, plus daily limits depending on account credits."
|
|
1292
|
+
label: "ultra",
|
|
1293
|
+
text: `${describeUltraModes(ultra.modes)} needs an actual task after the keyword.`
|
|
673
1294
|
});
|
|
1295
|
+
return;
|
|
674
1296
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1297
|
+
if (ultrafocus && !ultra.focusPath) {
|
|
1298
|
+
appendLine({
|
|
1299
|
+
tone: "warning",
|
|
1300
|
+
label: "ultra",
|
|
1301
|
+
text: "ultrafocus needs a path — write ultrafocus:src/file.ts or ultrafocus \"my dir\"."
|
|
1302
|
+
});
|
|
679
1303
|
return;
|
|
680
1304
|
}
|
|
1305
|
+
const runStartedAt = Date.now();
|
|
1306
|
+
softStopRequestedRef.current = false;
|
|
1307
|
+
lastEscapeStopAtRef.current = 0;
|
|
681
1308
|
setInput("");
|
|
682
1309
|
setTranscriptScrollOffset(0);
|
|
1310
|
+
setTodos([]);
|
|
1311
|
+
setUltramaxxRun(ultramaxx);
|
|
683
1312
|
setIsRunning(true);
|
|
684
1313
|
appendLine({
|
|
685
1314
|
kind: "user",
|
|
@@ -687,20 +1316,100 @@ export function App(props) {
|
|
|
687
1316
|
label: "you",
|
|
688
1317
|
text: task
|
|
689
1318
|
});
|
|
1319
|
+
if (ultra.modes.length > 0) {
|
|
1320
|
+
const engagedDetail = [];
|
|
1321
|
+
if (ultramaxx) {
|
|
1322
|
+
engagedDetail.push("ultramaxx: xhigh reasoning, expanded step budget, advisors on.");
|
|
1323
|
+
}
|
|
1324
|
+
if (ultracheap) {
|
|
1325
|
+
engagedDetail.push("ultracheap: low reasoning, terse output, advisors off.");
|
|
1326
|
+
}
|
|
1327
|
+
if (ultrafast) {
|
|
1328
|
+
engagedDetail.push("ultrafast: lowest-latency pipeline — low reasoning, fixed short thinking, advisors off.");
|
|
1329
|
+
}
|
|
1330
|
+
if (ultrafocus) {
|
|
1331
|
+
engagedDetail.push(`ultrafocus: the agent stays inside ${ultra.focusPath}.`);
|
|
1332
|
+
}
|
|
1333
|
+
if (ultraloop) {
|
|
1334
|
+
engagedDetail.push("ultraloop: expanded budget with explicit final self-check before finishing.");
|
|
1335
|
+
}
|
|
1336
|
+
appendLine({
|
|
1337
|
+
tone: "accent",
|
|
1338
|
+
label: "ultra",
|
|
1339
|
+
text: `✻ ${describeUltraModes(ultra.modes).toUpperCase()} engaged`,
|
|
1340
|
+
detail: engagedDetail.join("\n")
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
let finalMessage = "";
|
|
1344
|
+
let turnAttachmentPaths = [];
|
|
690
1345
|
try {
|
|
691
|
-
const runnableSettings = await resolveRunnableSettings(settings, modelOptions, appendLine, setModelOptions)
|
|
1346
|
+
const runnableSettings = await resolveRunnableSettings(settings, modelOptions, appendLine, setModelOptions, (message) => {
|
|
1347
|
+
if (settings.provider === "gemini-wrapper" && isGeminiCookieError(message)) {
|
|
1348
|
+
setReauthPrompt({ task });
|
|
1349
|
+
setStatus("gemini cookies expired");
|
|
1350
|
+
setWorkState("waiting_approval");
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
692
1353
|
if (!runnableSettings) {
|
|
693
1354
|
return;
|
|
694
1355
|
}
|
|
695
1356
|
const abortController = new AbortController();
|
|
696
1357
|
abortControllerRef.current = abortController;
|
|
1358
|
+
const effectiveMode = overrides.mode ?? agentMode;
|
|
1359
|
+
// Carry earlier-turn context forward so a follow-up prompt still knows
|
|
1360
|
+
// what the user was doing and where. Advisory only — it never changes
|
|
1361
|
+
// the workspace root or restricts the agent.
|
|
1362
|
+
const sessionMemory = conversationTurnsRef.current.length > 0
|
|
1363
|
+
? `Earlier in this PatchPilot session (most recent last), for continuity only — the request below still takes priority and is not restricted to these paths:\n${conversationTurnsRef.current.join("\n")}`
|
|
1364
|
+
: "";
|
|
1365
|
+
const artifactContext = formatSessionArtifactContext(artifactsRef.current);
|
|
1366
|
+
const persistedContext = await contextStoreRef.current
|
|
1367
|
+
.buildContextBlock({
|
|
1368
|
+
maxItems: 12,
|
|
1369
|
+
title: "Known session context"
|
|
1370
|
+
})
|
|
1371
|
+
.catch(() => "");
|
|
1372
|
+
// Ultra-mode run instructions — injected as advisory context so each
|
|
1373
|
+
// mode shapes the run without changing the workspace root.
|
|
1374
|
+
const ultraInstructions = [];
|
|
1375
|
+
if (ultrafocus && ultra.focusPath) {
|
|
1376
|
+
ultraInstructions.push(`ULTRAFOCUS is active. Restrict every read, edit, and command to \`${ultra.focusPath}\` and the files it directly depends on. Do not modify anything outside that path; if the task genuinely needs other files, stop and say so instead.`);
|
|
1377
|
+
}
|
|
1378
|
+
if (ultraloop) {
|
|
1379
|
+
ultraInstructions.push("ULTRALOOP is active. Do not finish until the user's actual goal is fully achieved and verified — not merely attempted. Before any final answer, restate the goal, list what is done, list any remaining gap, and keep working if something is still missing.");
|
|
1380
|
+
}
|
|
1381
|
+
if (ultracheap) {
|
|
1382
|
+
ultraInstructions.push("ULTRACHEAP is active. Keep output terse, avoid unnecessary tool calls, and take the most direct path to a correct result.");
|
|
1383
|
+
}
|
|
1384
|
+
if (ultrafast) {
|
|
1385
|
+
ultraInstructions.push("ULTRAFAST is active. Optimise for speed: minimal reasoning, the fewest tool calls that still get it right, no exploratory detours. Answer as directly as possible.");
|
|
1386
|
+
}
|
|
1387
|
+
const effectiveResumeContext = [resumeContext, sessionMemory, artifactContext, persistedContext, ultraInstructions.join("\n\n")]
|
|
1388
|
+
.filter(Boolean)
|
|
1389
|
+
.join("\n\n");
|
|
697
1390
|
const taskRunner = new AgentRunner({
|
|
698
1391
|
...runnableSettings,
|
|
699
|
-
|
|
1392
|
+
maxSteps: ultraloop
|
|
1393
|
+
? Math.max(runnableSettings.maxSteps, 60)
|
|
1394
|
+
: ultramaxx
|
|
1395
|
+
? Math.max(runnableSettings.maxSteps, 40)
|
|
1396
|
+
: ultraLean
|
|
1397
|
+
? Math.min(runnableSettings.maxSteps, 12)
|
|
1398
|
+
: runnableSettings.maxSteps,
|
|
1399
|
+
reasoningEffort: ultramaxx ? "xhigh" : ultraLean ? "low" : runnableSettings.reasoningEffort,
|
|
1400
|
+
thinkingMode: ultramaxx || ultraloop ? "adaptive" : ultraLean ? "fixed" : runnableSettings.thinkingMode,
|
|
1401
|
+
subagents: ultramaxx || ultraloop ? true : ultraLean ? false : runnableSettings.subagents,
|
|
1402
|
+
ultramaxx,
|
|
1403
|
+
allowExternalFileAnalysis: experimentalFlags.fileAnalysis,
|
|
1404
|
+
allowShellMetacharacters: experimentalFlags.shellMetacharacters,
|
|
1405
|
+
memoryEnabled: experimentalFlags.memory,
|
|
1406
|
+
mode: effectiveMode,
|
|
700
1407
|
signal: abortController.signal,
|
|
1408
|
+
shouldStopAfterStep: () => softStopRequestedRef.current,
|
|
701
1409
|
sessionStore: sessionStoreRef.current,
|
|
1410
|
+
resumeContext: effectiveResumeContext,
|
|
702
1411
|
approvalHandler: (request) => new Promise((resolve) => {
|
|
703
|
-
if (
|
|
1412
|
+
if (effectiveMode === "plan") {
|
|
704
1413
|
appendLine({
|
|
705
1414
|
kind: "approval",
|
|
706
1415
|
tone: "warning",
|
|
@@ -714,7 +1423,13 @@ export function App(props) {
|
|
|
714
1423
|
resolve("deny");
|
|
715
1424
|
return;
|
|
716
1425
|
}
|
|
717
|
-
if (
|
|
1426
|
+
if (request.bypassable !== false &&
|
|
1427
|
+
shouldBypassApproval({
|
|
1428
|
+
mode: effectiveMode,
|
|
1429
|
+
permission: request.permission,
|
|
1430
|
+
permissions: runnableSettings,
|
|
1431
|
+
allowExternalFileAnalysis: experimentalFlags.fileAnalysis
|
|
1432
|
+
})) {
|
|
718
1433
|
resolve("allow_session");
|
|
719
1434
|
return;
|
|
720
1435
|
}
|
|
@@ -735,7 +1450,15 @@ export function App(props) {
|
|
|
735
1450
|
approvalResolverRef.current = resolve;
|
|
736
1451
|
})
|
|
737
1452
|
});
|
|
738
|
-
|
|
1453
|
+
// Hand any documents the user attached this turn to the agent so it
|
|
1454
|
+
// can read/analyse them with its file tools.
|
|
1455
|
+
const pendingAttachments = pendingAttachmentsRef.current;
|
|
1456
|
+
turnAttachmentPaths = pendingAttachments;
|
|
1457
|
+
pendingAttachmentsRef.current = [];
|
|
1458
|
+
const taskWithAttachments = pendingAttachments.length > 0
|
|
1459
|
+
? `${effectiveTask}\n\n[Attached documents for this task — read or analyse them as needed with inspect_document. Paths are JSON-escaped and may contain spaces:\n${formatAttachedDocuments(pendingAttachments)}\n]`
|
|
1460
|
+
: effectiveTask;
|
|
1461
|
+
for await (const event of taskRunner.run(taskWithAttachments)) {
|
|
739
1462
|
setWorkState(event.workState);
|
|
740
1463
|
if (event.type === "metrics") {
|
|
741
1464
|
if (runnableSettings.provider === "ollama") {
|
|
@@ -748,31 +1471,154 @@ export function App(props) {
|
|
|
748
1471
|
if (event.type === "subagent") {
|
|
749
1472
|
setTelemetry(event.metrics);
|
|
750
1473
|
setSessionTelemetry((currentSession) => addTelemetryToSession(currentSession, event.metrics));
|
|
1474
|
+
setToolTelemetry((currentTools) => addToolTelemetry(currentTools, "subagent", true));
|
|
751
1475
|
setAdvisorNotes((currentNotes) => upsertAdvisorNote(currentNotes, {
|
|
752
1476
|
role: event.role,
|
|
753
1477
|
message: event.message
|
|
754
1478
|
}));
|
|
755
1479
|
}
|
|
1480
|
+
if (event.type === "todo") {
|
|
1481
|
+
setTodos(event.items);
|
|
1482
|
+
setStatus(event.summary);
|
|
1483
|
+
setToolTelemetry((currentTools) => addToolTelemetry(currentTools, "update_todo", true));
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (event.type === "final") {
|
|
1487
|
+
finalMessage = event.message;
|
|
1488
|
+
}
|
|
1489
|
+
// Best-effort: list documents PatchPilot wrote in the artifacts bar.
|
|
1490
|
+
if (event.type === "tool" && event.ok && /write|create|pdf|save|export/i.test(event.name)) {
|
|
1491
|
+
const created = /((?:\/|~|\.\/|[\w.-]+\/)[\w./-]+\.(?:pdf|docx?|md|txt|jsonl?|csv|ya?ml|toml|xml|html?|css|tsx?|jsx?|mjs|cjs|py|sh|zsh|bash|sql|log|diff|patch|png|jpe?g|gif|webp|bmp|heic|svg))/i.exec(`${event.summary ?? ""} ${event.preview ?? ""}`);
|
|
1492
|
+
if (created?.[1]) {
|
|
1493
|
+
registerCreatedArtifact(created[1]);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
if (event.type === "tool") {
|
|
1497
|
+
setToolTelemetry((currentTools) => addToolTelemetry(currentTools, event.name, event.ok));
|
|
1498
|
+
}
|
|
1499
|
+
if (event.type === "approval") {
|
|
1500
|
+
setToolTelemetry((currentTools) => addApprovalTelemetry(currentTools, event.decision));
|
|
1501
|
+
}
|
|
1502
|
+
// Expired Gemini-Wrapper cookies arrive as an error event (the run
|
|
1503
|
+
// does not throw) — offer the y/n re-auth prompt here too.
|
|
1504
|
+
if (event.type === "error" &&
|
|
1505
|
+
settings.provider === "gemini-wrapper" &&
|
|
1506
|
+
isGeminiCookieError(event.message)) {
|
|
1507
|
+
setReauthPrompt({ task });
|
|
1508
|
+
setWorkState("waiting_approval");
|
|
1509
|
+
}
|
|
756
1510
|
setStatus(eventToStatus(event));
|
|
757
1511
|
appendLine(eventToLine(event));
|
|
758
1512
|
}
|
|
759
1513
|
}
|
|
760
1514
|
catch (error) {
|
|
1515
|
+
if (abortControllerRef.current?.signal.aborted) {
|
|
1516
|
+
appendLine({
|
|
1517
|
+
kind: "status",
|
|
1518
|
+
tone: "warning",
|
|
1519
|
+
label: "stop",
|
|
1520
|
+
text: "Stopped by user.",
|
|
1521
|
+
workState: "done"
|
|
1522
|
+
});
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
761
1526
|
appendLine({
|
|
762
1527
|
kind: "error",
|
|
763
1528
|
tone: "danger",
|
|
764
1529
|
label: "error",
|
|
765
|
-
text:
|
|
1530
|
+
text: message,
|
|
766
1531
|
workState: "error"
|
|
767
1532
|
});
|
|
1533
|
+
// Expired Gemini-Wrapper cookies: offer a one-key re-auth + retry
|
|
1534
|
+
// instead of making the user restart and re-type the prompt.
|
|
1535
|
+
if (settings.provider === "gemini-wrapper" && isGeminiCookieError(message)) {
|
|
1536
|
+
setReauthPrompt({ task });
|
|
1537
|
+
setStatus("gemini cookies expired");
|
|
1538
|
+
setWorkState("waiting_approval");
|
|
1539
|
+
}
|
|
768
1540
|
}
|
|
769
1541
|
finally {
|
|
770
1542
|
abortControllerRef.current = null;
|
|
1543
|
+
setIsRunning(false);
|
|
1544
|
+
setUltramaxxRun(false);
|
|
1545
|
+
// Record a short digest of this turn for cross-run continuity.
|
|
1546
|
+
conversationTurnsRef.current = [
|
|
1547
|
+
...conversationTurnsRef.current,
|
|
1548
|
+
`- Asked: "${task.replace(/\s+/g, " ").trim().slice(0, 220)}"${turnAttachmentPaths.length > 0 ? ` attachments: ${turnAttachmentPaths.map(formatAttachmentDigestPath).join(", ")}` : ""}${finalMessage ? ` → outcome: ${finalMessage.replace(/\s+/g, " ").trim().slice(0, 220)}` : ""}`
|
|
1549
|
+
].slice(-6);
|
|
1550
|
+
void contextStoreRef.current.append({
|
|
1551
|
+
kind: "turn",
|
|
1552
|
+
source: "user",
|
|
1553
|
+
label: task.replace(/\s+/g, " ").trim().slice(0, 120) || "PatchPilot turn",
|
|
1554
|
+
text: [
|
|
1555
|
+
`Asked: ${task.replace(/\s+/g, " ").trim()}`,
|
|
1556
|
+
turnAttachmentPaths.length > 0 ? `Attachments: ${turnAttachmentPaths.join(", ")}` : "",
|
|
1557
|
+
finalMessage ? `Outcome: ${finalMessage.replace(/\s+/g, " ").trim().slice(0, 500)}` : ""
|
|
1558
|
+
]
|
|
1559
|
+
.filter(Boolean)
|
|
1560
|
+
.join("\n"),
|
|
1561
|
+
priority: turnAttachmentPaths.length > 0 ? 65 : 35
|
|
1562
|
+
}).catch(() => undefined);
|
|
1563
|
+
appendLine({
|
|
1564
|
+
kind: "status",
|
|
1565
|
+
tone: "muted",
|
|
1566
|
+
label: "done",
|
|
1567
|
+
text: `✻ ${formatCompletionSummary(Date.now() - runStartedAt, runStartedAt)}`,
|
|
1568
|
+
workState: "done"
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}, [agentMode, appendLine, experimentalFlags, isRunning, modelOptions, registerCreatedArtifact, resumeContext, settings]);
|
|
1572
|
+
const resolveReauthPrompt = useCallback(async (accept) => {
|
|
1573
|
+
const pending = reauthPrompt;
|
|
1574
|
+
if (!pending || reauthBusy) {
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (!accept) {
|
|
1578
|
+
setReauthPrompt(null);
|
|
771
1579
|
setStatus("idle");
|
|
772
1580
|
setWorkState("idle");
|
|
773
|
-
|
|
1581
|
+
appendLine({
|
|
1582
|
+
tone: "warning",
|
|
1583
|
+
label: "gemini",
|
|
1584
|
+
text: "Cookie refresh declined.",
|
|
1585
|
+
detail: "Run /onboarding to re-authenticate Gemini-Wrapper when you are ready."
|
|
1586
|
+
});
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
// Keep the panel on screen and show the busy animation while the
|
|
1590
|
+
// browser cookies are imported.
|
|
1591
|
+
setReauthBusy(true);
|
|
1592
|
+
try {
|
|
1593
|
+
const result = await importGeminiWrapperBrowserCookies();
|
|
1594
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_MODE = "python";
|
|
1595
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON = result.cookiesPath;
|
|
1596
|
+
savePatchPilotEnvValues({
|
|
1597
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "python",
|
|
1598
|
+
PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON: result.cookiesPath
|
|
1599
|
+
});
|
|
1600
|
+
setReauthBusy(false);
|
|
1601
|
+
setReauthPrompt(null);
|
|
1602
|
+
appendLine({
|
|
1603
|
+
tone: "success",
|
|
1604
|
+
label: "gemini",
|
|
1605
|
+
text: `Imported ${result.cookieCount} fresh cookies from ${result.source}. Retrying your task...`
|
|
1606
|
+
});
|
|
1607
|
+
await runTask(pending.task);
|
|
1608
|
+
}
|
|
1609
|
+
catch (error) {
|
|
1610
|
+
setReauthBusy(false);
|
|
1611
|
+
setReauthPrompt(null);
|
|
1612
|
+
setStatus("idle");
|
|
1613
|
+
setWorkState("idle");
|
|
1614
|
+
appendLine({
|
|
1615
|
+
tone: "danger",
|
|
1616
|
+
label: "gemini",
|
|
1617
|
+
text: error instanceof Error ? error.message : String(error),
|
|
1618
|
+
detail: "Cookie refresh failed. Sign in to Gemini in your browser, then retry the prompt."
|
|
1619
|
+
});
|
|
774
1620
|
}
|
|
775
|
-
}, [
|
|
1621
|
+
}, [appendLine, reauthBusy, reauthPrompt, runTask]);
|
|
776
1622
|
const handleSlashCommand = useCallback(async (rawCommand) => {
|
|
777
1623
|
const [commandName = "", ...args] = rawCommand.slice(1).trim().split(/\s+/);
|
|
778
1624
|
const command = commandName.toLowerCase();
|
|
@@ -815,17 +1661,17 @@ export function App(props) {
|
|
|
815
1661
|
appendLine({
|
|
816
1662
|
tone: "accent",
|
|
817
1663
|
label: "permissions",
|
|
818
|
-
text: `mode ${agentMode} | write ${modePermissionLabel(agentMode, "write")} | shell ${modePermissionLabel(agentMode, "shell")} | subagents ${settings.subagents ? "on" : "off"}`,
|
|
1664
|
+
text: `mode ${agentMode} | write ${modePermissionLabel(agentMode, "write", settings)} | shell ${modePermissionLabel(agentMode, "shell", settings)} | subagents ${settings.subagents ? "on" : "off"}`,
|
|
819
1665
|
detail: modeDescription(agentMode)
|
|
820
1666
|
});
|
|
821
1667
|
return;
|
|
822
1668
|
case "provider": {
|
|
823
1669
|
const nextProvider = args[0]?.toLowerCase();
|
|
824
|
-
if (nextProvider !== "ollama" && nextProvider !== "gemini" && nextProvider !== "codex" && nextProvider !== "openrouter" && nextProvider !== "nvidia") {
|
|
1670
|
+
if (nextProvider !== "ollama" && nextProvider !== "gemini" && nextProvider !== "gemini-wrapper" && nextProvider !== "codex" && nextProvider !== "openrouter" && nextProvider !== "nvidia") {
|
|
825
1671
|
appendLine({
|
|
826
1672
|
tone: "accent",
|
|
827
1673
|
label: "provider",
|
|
828
|
-
text: `current ${settings.provider}. Use /provider ollama, gemini, openrouter, nvidia, or codex.`
|
|
1674
|
+
text: `current ${settings.provider}. Use /provider ollama, gemini, gemini-wrapper, openrouter, nvidia, or codex.`
|
|
829
1675
|
});
|
|
830
1676
|
return;
|
|
831
1677
|
}
|
|
@@ -848,14 +1694,14 @@ export function App(props) {
|
|
|
848
1694
|
tone: needsApiKey(nextProvider) && !hasApiKey(nextProvider) ? "warning" : "success",
|
|
849
1695
|
label: "provider",
|
|
850
1696
|
text: needsApiKey(nextProvider) && !hasApiKey(nextProvider)
|
|
851
|
-
? `${nextProvider} needs
|
|
1697
|
+
? `${nextProvider} needs setup. Setup opened.`
|
|
852
1698
|
: `switched to ${nextProvider} using ${nextModel}`
|
|
853
1699
|
});
|
|
854
1700
|
return;
|
|
855
1701
|
}
|
|
856
1702
|
case "onboarding":
|
|
857
1703
|
setOnboarding({
|
|
858
|
-
step: "
|
|
1704
|
+
step: "welcome"
|
|
859
1705
|
});
|
|
860
1706
|
setOnboardingIndex(0);
|
|
861
1707
|
setOnboardingInput("");
|
|
@@ -868,6 +1714,10 @@ export function App(props) {
|
|
|
868
1714
|
...currentSettings,
|
|
869
1715
|
subagents: subagentsEnabled
|
|
870
1716
|
}));
|
|
1717
|
+
setExperimentalFlags((currentFlags) => ({
|
|
1718
|
+
...currentFlags,
|
|
1719
|
+
subagents: subagentsEnabled
|
|
1720
|
+
}));
|
|
871
1721
|
appendLine({
|
|
872
1722
|
tone: "success",
|
|
873
1723
|
label: "agents",
|
|
@@ -926,14 +1776,18 @@ export function App(props) {
|
|
|
926
1776
|
const writeEnabled = readToggle(args[0], !settings.allowWrite);
|
|
927
1777
|
if (writeEnabled) {
|
|
928
1778
|
requestBypassMode();
|
|
1779
|
+
appendLine({
|
|
1780
|
+
tone: "warning",
|
|
1781
|
+
label: "write",
|
|
1782
|
+
text: "write bypass needs trusted-workspace confirmation"
|
|
1783
|
+
});
|
|
929
1784
|
return;
|
|
930
1785
|
}
|
|
931
|
-
|
|
932
|
-
applyMode("build", false);
|
|
1786
|
+
setExplicitPermission("write", writeEnabled);
|
|
933
1787
|
appendLine({
|
|
934
1788
|
tone: "success",
|
|
935
1789
|
label: "write",
|
|
936
|
-
text: "workspace writes
|
|
1790
|
+
text: writeEnabled ? "workspace writes are allowed; shell remains separately controlled" : "workspace writes disabled"
|
|
937
1791
|
});
|
|
938
1792
|
return;
|
|
939
1793
|
}
|
|
@@ -941,21 +1795,27 @@ export function App(props) {
|
|
|
941
1795
|
const shellEnabled = readToggle(args[0], !settings.allowShell);
|
|
942
1796
|
if (shellEnabled) {
|
|
943
1797
|
requestBypassMode();
|
|
1798
|
+
appendLine({
|
|
1799
|
+
tone: "warning",
|
|
1800
|
+
label: "shell",
|
|
1801
|
+
text: "shell bypass needs trusted-workspace confirmation"
|
|
1802
|
+
});
|
|
944
1803
|
return;
|
|
945
1804
|
}
|
|
946
|
-
|
|
947
|
-
applyMode("build", false);
|
|
1805
|
+
setExplicitPermission("shell", shellEnabled);
|
|
948
1806
|
appendLine({
|
|
949
1807
|
tone: "success",
|
|
950
1808
|
label: "shell",
|
|
951
|
-
text: "shell commands
|
|
1809
|
+
text: shellEnabled ? "shell commands are allowed; writes remain separately controlled" : "shell commands disabled"
|
|
952
1810
|
});
|
|
953
1811
|
return;
|
|
954
1812
|
}
|
|
955
1813
|
case "model": {
|
|
956
1814
|
const requestedModel = normalizeModelAlias(args.join(" ").trim());
|
|
957
1815
|
if (!requestedModel) {
|
|
958
|
-
const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine
|
|
1816
|
+
const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine, {
|
|
1817
|
+
refresh: settings.provider === "gemini-wrapper"
|
|
1818
|
+
});
|
|
959
1819
|
if (!models) {
|
|
960
1820
|
return;
|
|
961
1821
|
}
|
|
@@ -968,7 +1828,9 @@ export function App(props) {
|
|
|
968
1828
|
return;
|
|
969
1829
|
}
|
|
970
1830
|
{
|
|
971
|
-
const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine
|
|
1831
|
+
const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine, {
|
|
1832
|
+
refresh: settings.provider === "gemini-wrapper"
|
|
1833
|
+
});
|
|
972
1834
|
if (!models) {
|
|
973
1835
|
return;
|
|
974
1836
|
}
|
|
@@ -980,7 +1842,7 @@ export function App(props) {
|
|
|
980
1842
|
tone: "warning",
|
|
981
1843
|
label: "model",
|
|
982
1844
|
text: `No unique model match for "${requestedModel}".`,
|
|
983
|
-
detail: formatModelOptions(selectableModels(requestedModel, models).slice(0, 12), settings.model)
|
|
1845
|
+
detail: formatModelOptions(selectableModels(requestedModel, models, formatModelLabel).slice(0, 12), settings.model)
|
|
984
1846
|
});
|
|
985
1847
|
return;
|
|
986
1848
|
}
|
|
@@ -991,7 +1853,9 @@ export function App(props) {
|
|
|
991
1853
|
case "models": {
|
|
992
1854
|
const requestedModel = args.join(" ").trim();
|
|
993
1855
|
if (requestedModel) {
|
|
994
|
-
const installedModels = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine
|
|
1856
|
+
const installedModels = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine, {
|
|
1857
|
+
refresh: settings.provider === "gemini-wrapper"
|
|
1858
|
+
});
|
|
995
1859
|
if (!installedModels) {
|
|
996
1860
|
return;
|
|
997
1861
|
}
|
|
@@ -1025,7 +1889,13 @@ export function App(props) {
|
|
|
1025
1889
|
? "Pull a model on the selected host first."
|
|
1026
1890
|
: settings.provider === "gemini"
|
|
1027
1891
|
? "Check GEMINI_API_KEY in PatchPilot config."
|
|
1028
|
-
:
|
|
1892
|
+
: settings.provider === "gemini-wrapper"
|
|
1893
|
+
? "Check gemini_webapi install and PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON in PatchPilot config."
|
|
1894
|
+
: settings.provider === "openrouter"
|
|
1895
|
+
? "Check OPENROUTER_API_KEY in PatchPilot config."
|
|
1896
|
+
: settings.provider === "nvidia"
|
|
1897
|
+
? "Check NVIDIA_API_KEY in PatchPilot config."
|
|
1898
|
+
: "Run codex login first."
|
|
1029
1899
|
});
|
|
1030
1900
|
return;
|
|
1031
1901
|
}
|
|
@@ -1049,13 +1919,59 @@ export function App(props) {
|
|
|
1049
1919
|
}
|
|
1050
1920
|
case "status":
|
|
1051
1921
|
appendLine({
|
|
1922
|
+
kind: "status",
|
|
1052
1923
|
tone: "accent",
|
|
1053
1924
|
label: "status",
|
|
1054
|
-
text: settings.provider
|
|
1055
|
-
|
|
1056
|
-
|
|
1925
|
+
text: `mode ${agentMode} · write ${modePermissionLabel(agentMode, "write", settings)} · shell ${modePermissionLabel(agentMode, "shell", settings)} · ${settings.provider}/${settings.model} · subagents ${settings.subagents ? "on" : "off"}`,
|
|
1926
|
+
detail: formatStatusDock({
|
|
1927
|
+
provider: settings.provider,
|
|
1928
|
+
model: settings.model,
|
|
1929
|
+
agentMode,
|
|
1930
|
+
subagents: settings.subagents,
|
|
1931
|
+
thinkingMode: settings.thinkingMode,
|
|
1932
|
+
reasoningEffort: settings.reasoningEffort,
|
|
1933
|
+
workspace: settings.workspace,
|
|
1934
|
+
ollamaUrl: settings.ollamaUrl,
|
|
1935
|
+
sessionId: sessionStoreRef.current.sessionId,
|
|
1936
|
+
activeHost,
|
|
1937
|
+
advisorNotes,
|
|
1938
|
+
toolTelemetry,
|
|
1939
|
+
sessionTelemetry,
|
|
1940
|
+
telemetry,
|
|
1941
|
+
draftTokens
|
|
1942
|
+
})
|
|
1943
|
+
});
|
|
1944
|
+
return;
|
|
1945
|
+
case "usage":
|
|
1946
|
+
appendLine({
|
|
1947
|
+
tone: "accent",
|
|
1948
|
+
label: "usage",
|
|
1949
|
+
text: formatUsageSummary({
|
|
1950
|
+
provider: settings.provider,
|
|
1951
|
+
model: settings.model,
|
|
1952
|
+
telemetry,
|
|
1953
|
+
sessionTelemetry,
|
|
1954
|
+
toolTelemetry
|
|
1955
|
+
}),
|
|
1956
|
+
detail: formatUsageDetail({
|
|
1957
|
+
provider: settings.provider,
|
|
1958
|
+
model: settings.model,
|
|
1959
|
+
sessionTelemetry,
|
|
1960
|
+
toolTelemetry
|
|
1961
|
+
})
|
|
1057
1962
|
});
|
|
1058
1963
|
return;
|
|
1964
|
+
case "context":
|
|
1965
|
+
case "ctx":
|
|
1966
|
+
case "compact":
|
|
1967
|
+
case "compress":
|
|
1968
|
+
appendLine(await runContextSlashCommand({
|
|
1969
|
+
workspace: settings.workspace,
|
|
1970
|
+
sessionId: sessionStoreRef.current.sessionId,
|
|
1971
|
+
command: command === "compact" || command === "compress" ? "compact" : "context",
|
|
1972
|
+
args
|
|
1973
|
+
}));
|
|
1974
|
+
return;
|
|
1059
1975
|
case "sessions": {
|
|
1060
1976
|
const sessions = await listWorkspaceSessions(settings.workspace);
|
|
1061
1977
|
appendLine({
|
|
@@ -1074,11 +1990,31 @@ export function App(props) {
|
|
|
1074
1990
|
const sessionId = args[0] ?? "";
|
|
1075
1991
|
const sessions = await listWorkspaceSessions(settings.workspace);
|
|
1076
1992
|
const selectedSession = sessionId ? await loadSessionSummary(settings.workspace, sessionId) : sessions[0] ?? null;
|
|
1993
|
+
if (selectedSession) {
|
|
1994
|
+
sessionStoreRef.current = new SessionStore({
|
|
1995
|
+
workspace: settings.workspace,
|
|
1996
|
+
sessionId: selectedSession.sessionId
|
|
1997
|
+
});
|
|
1998
|
+
contextStoreRef.current = new ContextStore({
|
|
1999
|
+
workspace: settings.workspace,
|
|
2000
|
+
sessionId: selectedSession.sessionId
|
|
2001
|
+
});
|
|
2002
|
+
await contextStoreRef.current.bootstrapFromSession(await sessionStoreRef.current.loadEvents());
|
|
2003
|
+
await sessionStoreRef.current.append({
|
|
2004
|
+
type: "session.resumed",
|
|
2005
|
+
sessionId: selectedSession.sessionId,
|
|
2006
|
+
workspace: settings.workspace,
|
|
2007
|
+
resumedAt: new Date().toISOString()
|
|
2008
|
+
});
|
|
2009
|
+
setResumeContext(await buildSessionResumeContext(settings.workspace, selectedSession.sessionId));
|
|
2010
|
+
setSessionTelemetry(emptySessionTelemetry());
|
|
2011
|
+
setTelemetry(null);
|
|
2012
|
+
}
|
|
1077
2013
|
appendLine({
|
|
1078
2014
|
kind: "status",
|
|
1079
2015
|
tone: selectedSession ? "accent" : "warning",
|
|
1080
2016
|
label: "resume",
|
|
1081
|
-
text: selectedSession ? `Loaded session ${selectedSession.sessionId}
|
|
2017
|
+
text: selectedSession ? `Loaded session ${selectedSession.sessionId} and will inject its summary into the next run.` : "No session available to resume.",
|
|
1082
2018
|
detail: selectedSession
|
|
1083
2019
|
? `workspace ${selectedSession.workspace}\nupdated ${selectedSession.updatedAt}\nmodel ${selectedSession.provider ?? "-"} ${selectedSession.model ?? "-"}\nlast task ${selectedSession.lastTask ?? "-"}`
|
|
1084
2020
|
: "Run /sessions after at least one PatchPilot run."
|
|
@@ -1179,45 +2115,208 @@ export function App(props) {
|
|
|
1179
2115
|
if (ejectedModels.length === 0) {
|
|
1180
2116
|
appendLine({
|
|
1181
2117
|
tone: "warning",
|
|
1182
|
-
label: "eject",
|
|
1183
|
-
text: "No Ollama model was ejected."
|
|
2118
|
+
label: "eject",
|
|
2119
|
+
text: "No Ollama model was ejected."
|
|
2120
|
+
});
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
appendLine({
|
|
2124
|
+
tone: "success",
|
|
2125
|
+
label: "eject",
|
|
2126
|
+
text: `ejected ${ejectedModels.join(", ")}`
|
|
2127
|
+
});
|
|
2128
|
+
if (activeHost) {
|
|
2129
|
+
const details = await readOllamaHostDetails(activeHost.host, true).catch(() => activeHost);
|
|
2130
|
+
setActiveHost(details);
|
|
2131
|
+
}
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
case "doctor": {
|
|
2135
|
+
const shouldFix = args.some((arg) => arg.toLowerCase() === "fix" || arg.toLowerCase() === "--fix");
|
|
2136
|
+
appendLine({
|
|
2137
|
+
tone: "muted",
|
|
2138
|
+
label: "doctor",
|
|
2139
|
+
text: shouldFix ? "checking local requirements and applying safe fixes..." : "checking local requirements..."
|
|
2140
|
+
});
|
|
2141
|
+
const doctorResults = await runDoctor(settings.provider, settings.ollamaUrl, settings.model, {
|
|
2142
|
+
fix: shouldFix
|
|
2143
|
+
});
|
|
2144
|
+
for (const result of doctorResults) {
|
|
2145
|
+
appendLine({
|
|
2146
|
+
tone: result.ok ? "success" : "danger",
|
|
2147
|
+
label: result.name,
|
|
2148
|
+
text: result.action && result.action !== "check" ? `${result.action}: ${result.details}` : result.details
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
if (!shouldFix && doctorResults.some((result) => result.action === "skipped")) {
|
|
2152
|
+
appendLine({
|
|
2153
|
+
tone: "accent",
|
|
2154
|
+
label: "doctor",
|
|
2155
|
+
text: "Some safe fixes are available. Run /doctor fix to approve them."
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
case "cleanup": {
|
|
2161
|
+
const target = readCleanupTarget(args[0]);
|
|
2162
|
+
if (!target) {
|
|
2163
|
+
appendLine({
|
|
2164
|
+
tone: "accent",
|
|
2165
|
+
label: "cleanup",
|
|
2166
|
+
text: "Choose what to clean: /cleanup cache, /cleanup sessions, /cleanup temp, or /cleanup all.",
|
|
2167
|
+
detail: "Sessions deletes saved workspace transcripts. Cache/temp are safe first choices."
|
|
2168
|
+
});
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const removed = await cleanupPatchPilot(settings.workspace, target);
|
|
2172
|
+
if (target === "sessions" || target === "all") {
|
|
2173
|
+
sessionStoreRef.current = new SessionStore({
|
|
2174
|
+
workspace: settings.workspace
|
|
2175
|
+
});
|
|
2176
|
+
await sessionStoreRef.current.create();
|
|
2177
|
+
setResumeContext("");
|
|
2178
|
+
setLines([]);
|
|
2179
|
+
setAdvisorNotes([]);
|
|
2180
|
+
setTelemetry(null);
|
|
2181
|
+
setSessionTelemetry(emptySessionTelemetry());
|
|
2182
|
+
setToolTelemetry(emptyToolTelemetry());
|
|
2183
|
+
}
|
|
2184
|
+
appendLine({
|
|
2185
|
+
tone: "success",
|
|
2186
|
+
label: "cleanup",
|
|
2187
|
+
text: `cleaned ${removed.join(", ") || target}`
|
|
2188
|
+
});
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
case "experimental": {
|
|
2192
|
+
const requestedFlag = args[0]?.toLowerCase();
|
|
2193
|
+
const requestedValue = args[1]?.toLowerCase();
|
|
2194
|
+
if (!requestedFlag) {
|
|
2195
|
+
setExperimentalOpen(true);
|
|
2196
|
+
setExperimentalIndex(0);
|
|
2197
|
+
setInput("");
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
const normalizedFlag = normalizeExperimentalFlag(requestedFlag);
|
|
2201
|
+
if (!normalizedFlag) {
|
|
2202
|
+
appendLine({
|
|
2203
|
+
tone: "warning",
|
|
2204
|
+
label: "experimental",
|
|
2205
|
+
text: `unknown flag ${requestedFlag}`,
|
|
2206
|
+
detail: "Use file-analysis, memory, subagents, or shell-metacharacters."
|
|
1184
2207
|
});
|
|
1185
2208
|
return;
|
|
1186
2209
|
}
|
|
2210
|
+
const enabled = readToggle(requestedValue, true);
|
|
2211
|
+
if (normalizedFlag === "subagents") {
|
|
2212
|
+
setSettings((currentSettings) => ({
|
|
2213
|
+
...currentSettings,
|
|
2214
|
+
subagents: enabled
|
|
2215
|
+
}));
|
|
2216
|
+
}
|
|
2217
|
+
savePatchPilotEnvValues({
|
|
2218
|
+
[experimentalFlagEnvName(normalizedFlag)]: enabled ? "1" : "0"
|
|
2219
|
+
});
|
|
2220
|
+
setExperimentalFlags((currentFlags) => ({
|
|
2221
|
+
...currentFlags,
|
|
2222
|
+
...(normalizedFlag === "fileAnalysis"
|
|
2223
|
+
? { fileAnalysis: enabled }
|
|
2224
|
+
: normalizedFlag === "memory"
|
|
2225
|
+
? { memory: enabled }
|
|
2226
|
+
: normalizedFlag === "subagents"
|
|
2227
|
+
? { subagents: enabled }
|
|
2228
|
+
: { shellMetacharacters: enabled })
|
|
2229
|
+
}));
|
|
1187
2230
|
appendLine({
|
|
1188
2231
|
tone: "success",
|
|
1189
|
-
label: "
|
|
1190
|
-
text:
|
|
2232
|
+
label: "experimental",
|
|
2233
|
+
text: `${experimentalFlagCommandName(normalizedFlag)} ${enabled ? "enabled" : "disabled"}`
|
|
1191
2234
|
});
|
|
1192
|
-
if (activeHost) {
|
|
1193
|
-
const details = await readOllamaHostDetails(activeHost.host, true).catch(() => activeHost);
|
|
1194
|
-
setActiveHost(details);
|
|
1195
|
-
}
|
|
1196
2235
|
return;
|
|
1197
2236
|
}
|
|
1198
|
-
case "
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
});
|
|
1204
|
-
const doctorResults = await runDoctor(settings.provider, settings.ollamaUrl, settings.model);
|
|
1205
|
-
for (const result of doctorResults) {
|
|
2237
|
+
case "theme": {
|
|
2238
|
+
const requested = args[0]?.toLowerCase();
|
|
2239
|
+
if (requested === "new" || requested === "legacy") {
|
|
2240
|
+
setUiTheme(requested);
|
|
2241
|
+
savePatchPilotEnvValues({ PATCHPILOT_UI_THEME: requested });
|
|
1206
2242
|
appendLine({
|
|
1207
|
-
tone:
|
|
1208
|
-
label:
|
|
1209
|
-
text:
|
|
2243
|
+
tone: "success",
|
|
2244
|
+
label: "theme",
|
|
2245
|
+
text: `switched to the ${requested} UI`
|
|
1210
2246
|
});
|
|
2247
|
+
return;
|
|
1211
2248
|
}
|
|
2249
|
+
setThemePickerOpen(true);
|
|
2250
|
+
setThemePickerIndex(themeOptions.findIndex((option) => option.value === uiTheme));
|
|
2251
|
+
setInput("");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
case "init": {
|
|
2255
|
+
await ensurePatchPilotGitignore(settings.workspace);
|
|
2256
|
+
appendLine({
|
|
2257
|
+
tone: "accent",
|
|
2258
|
+
label: "init",
|
|
2259
|
+
text: "starting model-driven project init",
|
|
2260
|
+
detail: "PatchPilot will inspect the repository and create or update PATCHPILOT.md with approval-gated writes."
|
|
2261
|
+
});
|
|
2262
|
+
await runTask(patchPilotInitPrompt, {
|
|
2263
|
+
mode: "build"
|
|
2264
|
+
});
|
|
1212
2265
|
return;
|
|
1213
2266
|
}
|
|
1214
2267
|
case "clear":
|
|
1215
2268
|
setLines([]);
|
|
1216
2269
|
setAdvisorNotes([]);
|
|
2270
|
+
setTodos([]);
|
|
2271
|
+
setTelemetry(null);
|
|
2272
|
+
setResumeContext("");
|
|
2273
|
+
setSessionTelemetry(emptySessionTelemetry());
|
|
2274
|
+
setToolTelemetry(emptyToolTelemetry());
|
|
2275
|
+
setTranscriptScrollOffset(0);
|
|
2276
|
+
setSessionScrollOffset(0);
|
|
2277
|
+
conversationTurnsRef.current = [];
|
|
2278
|
+
artifactsRef.current = [];
|
|
2279
|
+
pendingAttachmentsRef.current = [];
|
|
2280
|
+
setArtifacts([]);
|
|
2281
|
+
return;
|
|
2282
|
+
case "new":
|
|
2283
|
+
if (isRunning) {
|
|
2284
|
+
appendLine({
|
|
2285
|
+
tone: "warning",
|
|
2286
|
+
label: "new",
|
|
2287
|
+
text: "Cannot start a new session while a run is active.",
|
|
2288
|
+
detail: "Stop the current run first, then use /new again."
|
|
2289
|
+
});
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
sessionStoreRef.current = new SessionStore({
|
|
2293
|
+
workspace: settings.workspace
|
|
2294
|
+
});
|
|
2295
|
+
contextStoreRef.current = new ContextStore({
|
|
2296
|
+
workspace: settings.workspace,
|
|
2297
|
+
sessionId: sessionStoreRef.current.sessionId
|
|
2298
|
+
});
|
|
2299
|
+
await sessionStoreRef.current.create();
|
|
2300
|
+
setLines([]);
|
|
2301
|
+
setAdvisorNotes([]);
|
|
2302
|
+
setTodos([]);
|
|
1217
2303
|
setTelemetry(null);
|
|
1218
2304
|
setSessionTelemetry(emptySessionTelemetry());
|
|
2305
|
+
setToolTelemetry(emptyToolTelemetry());
|
|
2306
|
+
setPendingApproval(null);
|
|
2307
|
+
approvalResolverRef.current = null;
|
|
2308
|
+
setBypassConfirmation(false);
|
|
2309
|
+
setInput("");
|
|
1219
2310
|
setTranscriptScrollOffset(0);
|
|
1220
2311
|
setSessionScrollOffset(0);
|
|
2312
|
+
setStatus("idle");
|
|
2313
|
+
setWorkState("idle");
|
|
2314
|
+
conversationTurnsRef.current = [];
|
|
2315
|
+
artifactsRef.current = [];
|
|
2316
|
+
pendingAttachmentsRef.current = [];
|
|
2317
|
+
setArtifacts([]);
|
|
2318
|
+
// Leave the transcript empty so the startup banner shows again,
|
|
2319
|
+
// exactly like a fresh launch.
|
|
1221
2320
|
return;
|
|
1222
2321
|
case "exit":
|
|
1223
2322
|
case "quit":
|
|
@@ -1245,6 +2344,7 @@ export function App(props) {
|
|
|
1245
2344
|
loadHostSuggestions,
|
|
1246
2345
|
loadProviderModels,
|
|
1247
2346
|
modelOptions,
|
|
2347
|
+
isRunning,
|
|
1248
2348
|
resolveApproval,
|
|
1249
2349
|
sessionTelemetry,
|
|
1250
2350
|
settings,
|
|
@@ -1286,6 +2386,39 @@ export function App(props) {
|
|
|
1286
2386
|
useEffect(() => {
|
|
1287
2387
|
void sessionStoreRef.current.create();
|
|
1288
2388
|
}, []);
|
|
2389
|
+
useEffect(() => {
|
|
2390
|
+
if (didCheckForUpdates.current || process.env.PATCHPILOT_UPDATE_CHECK === "0") {
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
didCheckForUpdates.current = true;
|
|
2394
|
+
const controller = new AbortController();
|
|
2395
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
2396
|
+
setStatus("checking for updates");
|
|
2397
|
+
void checkForPatchPilotUpdate(props.packageVersion ?? "0.0.0", controller.signal)
|
|
2398
|
+
.then((result) => {
|
|
2399
|
+
if (result.available) {
|
|
2400
|
+
setUpdatePrompt(result);
|
|
2401
|
+
setStatus(`update available ${result.currentVersion} -> ${result.latestVersion}`);
|
|
2402
|
+
}
|
|
2403
|
+
else {
|
|
2404
|
+
setStatus((current) => (current === "checking for updates" ? "idle" : current));
|
|
2405
|
+
}
|
|
2406
|
+
})
|
|
2407
|
+
.catch(() => {
|
|
2408
|
+
setStatus((current) => (current === "checking for updates" ? "idle" : current));
|
|
2409
|
+
})
|
|
2410
|
+
.finally(() => {
|
|
2411
|
+
clearTimeout(timer);
|
|
2412
|
+
});
|
|
2413
|
+
return () => {
|
|
2414
|
+
clearTimeout(timer);
|
|
2415
|
+
controller.abort();
|
|
2416
|
+
};
|
|
2417
|
+
}, [props.packageVersion]);
|
|
2418
|
+
useEffect(() => {
|
|
2419
|
+
runtimeStateRef.current.isRunning = isRunning;
|
|
2420
|
+
runtimeStateRef.current.hasPendingApproval = Boolean(pendingApproval || bypassConfirmation || updatePrompt || updateBusy);
|
|
2421
|
+
}, [bypassConfirmation, isRunning, pendingApproval, updateBusy, updatePrompt]);
|
|
1289
2422
|
useEffect(() => {
|
|
1290
2423
|
if (!props.initialTask || didRunInitialTask.current || onboarding || process.env.PATCHPILOT_ONBOARDING_COMPLETE !== "1") {
|
|
1291
2424
|
return;
|
|
@@ -1302,7 +2435,7 @@ export function App(props) {
|
|
|
1302
2435
|
}
|
|
1303
2436
|
didOpenDefaultOnboarding.current = true;
|
|
1304
2437
|
setOnboarding({
|
|
1305
|
-
step: "
|
|
2438
|
+
step: "welcome"
|
|
1306
2439
|
});
|
|
1307
2440
|
setOnboardingIndex(0);
|
|
1308
2441
|
setOnboardingInput("");
|
|
@@ -1374,10 +2507,81 @@ export function App(props) {
|
|
|
1374
2507
|
}
|
|
1375
2508
|
}, [hostOptions.length, input, isLoadingHosts, isLoadingModels, isRunning, loadHostSuggestions, loadProviderModels, modelOptions.length, onboarding, settings.provider]);
|
|
1376
2509
|
useInput((inputValue, key) => {
|
|
2510
|
+
if (themePickerOpen) {
|
|
2511
|
+
if (key.upArrow) {
|
|
2512
|
+
setThemePickerIndex((currentIndex) => (currentIndex - 1 + themeOptions.length) % themeOptions.length);
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
if (key.downArrow) {
|
|
2516
|
+
setThemePickerIndex((currentIndex) => (currentIndex + 1) % themeOptions.length);
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
if (key.escape || key.leftArrow) {
|
|
2520
|
+
setThemePickerOpen(false);
|
|
2521
|
+
setInput("");
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
if (key.return) {
|
|
2525
|
+
const chosen = themeOptions[themePickerIndex]?.value ?? "new";
|
|
2526
|
+
setUiTheme(chosen);
|
|
2527
|
+
savePatchPilotEnvValues({ PATCHPILOT_UI_THEME: chosen });
|
|
2528
|
+
setThemePickerOpen(false);
|
|
2529
|
+
setInput("");
|
|
2530
|
+
appendLine({
|
|
2531
|
+
tone: "success",
|
|
2532
|
+
label: "theme",
|
|
2533
|
+
text: `switched to the ${chosen} UI`
|
|
2534
|
+
});
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (experimentalOpen) {
|
|
2540
|
+
if (key.upArrow) {
|
|
2541
|
+
setExperimentalIndex((currentIndex) => (currentIndex - 1 + experimentalFlagCount()) % experimentalFlagCount());
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
if (key.downArrow) {
|
|
2545
|
+
setExperimentalIndex((currentIndex) => (currentIndex + 1) % experimentalFlagCount());
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
if (inputValue === " ") {
|
|
2549
|
+
const flag = experimentalFlagAt(experimentalIndex);
|
|
2550
|
+
setExperimentalFlags((currentFlags) => {
|
|
2551
|
+
const nextFlags = {
|
|
2552
|
+
...currentFlags,
|
|
2553
|
+
[flag]: !currentFlags[flag]
|
|
2554
|
+
};
|
|
2555
|
+
if (flag === "subagents") {
|
|
2556
|
+
setSettings((currentSettings) => ({
|
|
2557
|
+
...currentSettings,
|
|
2558
|
+
subagents: nextFlags.subagents
|
|
2559
|
+
}));
|
|
2560
|
+
}
|
|
2561
|
+
savePatchPilotEnvValues({
|
|
2562
|
+
PATCHPILOT_EXPERIMENTAL_FILE_ANALYSIS: nextFlags.fileAnalysis ? "1" : "0",
|
|
2563
|
+
PATCHPILOT_EXPERIMENTAL_MEMORY: nextFlags.memory ? "1" : "0",
|
|
2564
|
+
PATCHPILOT_EXPERIMENTAL_SUBAGENTS: nextFlags.subagents ? "1" : "0",
|
|
2565
|
+
PATCHPILOT_EXPERIMENTAL_SHELL_METACHARACTERS: nextFlags.shellMetacharacters ? "1" : "0"
|
|
2566
|
+
});
|
|
2567
|
+
return nextFlags;
|
|
2568
|
+
});
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
if (key.return || key.escape || key.leftArrow) {
|
|
2572
|
+
setExperimentalOpen(false);
|
|
2573
|
+
setInput("");
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
1377
2578
|
if (bypassConfirmation) {
|
|
1378
2579
|
const normalizedInput = inputValue.toLowerCase();
|
|
2580
|
+
// While the bypass confirmation is pending, tab continues the mode
|
|
2581
|
+
// cycle straight back to plan — no need to confirm bypass first.
|
|
1379
2582
|
if (key.tab) {
|
|
1380
|
-
|
|
2583
|
+
setInput("");
|
|
2584
|
+
applyMode("plan");
|
|
1381
2585
|
return;
|
|
1382
2586
|
}
|
|
1383
2587
|
if (normalizedInput === "y") {
|
|
@@ -1389,6 +2593,36 @@ export function App(props) {
|
|
|
1389
2593
|
return;
|
|
1390
2594
|
}
|
|
1391
2595
|
}
|
|
2596
|
+
if (reauthBusy) {
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
if (updateBusy) {
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (updatePrompt) {
|
|
2603
|
+
const normalizedInput = inputValue.toLowerCase();
|
|
2604
|
+
if (normalizedInput === "y") {
|
|
2605
|
+
void resolveUpdatePrompt(true);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (normalizedInput === "n" || key.escape) {
|
|
2609
|
+
void resolveUpdatePrompt(false);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
if (reauthPrompt) {
|
|
2615
|
+
const normalizedInput = inputValue.toLowerCase();
|
|
2616
|
+
if (normalizedInput === "y") {
|
|
2617
|
+
void resolveReauthPrompt(true);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (normalizedInput === "n" || key.escape) {
|
|
2621
|
+
void resolveReauthPrompt(false);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
1392
2626
|
if (pendingApproval) {
|
|
1393
2627
|
const normalizedInput = inputValue.toLowerCase();
|
|
1394
2628
|
if (normalizedInput === "y") {
|
|
@@ -1405,25 +2639,77 @@ export function App(props) {
|
|
|
1405
2639
|
}
|
|
1406
2640
|
}
|
|
1407
2641
|
if (isRunning && key.escape) {
|
|
1408
|
-
|
|
2642
|
+
const now = Date.now();
|
|
2643
|
+
const isDoubleEscape = now - lastEscapeStopAtRef.current <= 700;
|
|
2644
|
+
lastEscapeStopAtRef.current = now;
|
|
2645
|
+
if (isDoubleEscape) {
|
|
2646
|
+
abortControllerRef.current?.abort();
|
|
2647
|
+
appendLine({
|
|
2648
|
+
kind: "status",
|
|
2649
|
+
tone: "warning",
|
|
2650
|
+
label: "stop",
|
|
2651
|
+
text: "Force stopping current task now..."
|
|
2652
|
+
});
|
|
2653
|
+
setStatus("force stopping");
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
softStopRequestedRef.current = true;
|
|
1409
2657
|
appendLine({
|
|
1410
2658
|
kind: "status",
|
|
1411
2659
|
tone: "warning",
|
|
1412
2660
|
label: "stop",
|
|
1413
|
-
text: "
|
|
2661
|
+
text: "Will stop after the current step. Press esc again quickly to force stop now."
|
|
1414
2662
|
});
|
|
1415
|
-
setStatus("stopping");
|
|
2663
|
+
setStatus("stopping after current step");
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
// Ctrl+V — paste an image from the OS clipboard as an attachment. Bound to
|
|
2667
|
+
// Ctrl+V on every platform because terminals capture ⌘V / the native paste
|
|
2668
|
+
// shortcut for their own text paste.
|
|
2669
|
+
if (key.ctrl && (inputValue === "v" || inputValue === "V") && !onboarding && !isRunning) {
|
|
2670
|
+
void handleClipboardImagePaste();
|
|
1416
2671
|
return;
|
|
1417
2672
|
}
|
|
1418
2673
|
if (onboarding) {
|
|
1419
|
-
if (key.escape
|
|
2674
|
+
if (key.escape) {
|
|
1420
2675
|
goBackOnboarding();
|
|
1421
2676
|
return;
|
|
1422
2677
|
}
|
|
1423
2678
|
if (onboardingBusyMessage) {
|
|
1424
2679
|
return;
|
|
1425
2680
|
}
|
|
1426
|
-
|
|
2681
|
+
// Preferences step: left/right cycle the selected row's value in place;
|
|
2682
|
+
// left only goes back when no row is highlighted (the confirm row).
|
|
2683
|
+
if (onboarding.step === "preferences") {
|
|
2684
|
+
const confirmIndex = preferenceRows.length;
|
|
2685
|
+
if ((key.leftArrow || key.rightArrow) && onboardingIndex < confirmIndex) {
|
|
2686
|
+
const row = preferenceRows[onboardingIndex];
|
|
2687
|
+
if (row) {
|
|
2688
|
+
setOnboarding((current) => current && current.step === "preferences"
|
|
2689
|
+
? { ...current, preferences: cyclePreference(current.preferences, row.key, key.leftArrow ? -1 : 1) }
|
|
2690
|
+
: current);
|
|
2691
|
+
}
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
if (key.leftArrow) {
|
|
2695
|
+
goBackOnboarding();
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
else if (key.leftArrow) {
|
|
2700
|
+
goBackOnboarding();
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
if (onboarding.step === "disclaimer") {
|
|
2704
|
+
if (inputValue.toLowerCase() === "y") {
|
|
2705
|
+
void handleOnboardingSubmit("y");
|
|
2706
|
+
}
|
|
2707
|
+
else if (inputValue.toLowerCase() === "n") {
|
|
2708
|
+
goBackOnboarding();
|
|
2709
|
+
}
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
const optionCount = onboarding.step === "model" ? selectableModels(onboardingInput, onboarding.models, formatModelLabel).length : getOnboardingOptionCount(onboarding);
|
|
1427
2713
|
if (optionCount > 0 && key.upArrow) {
|
|
1428
2714
|
setOnboardingIndex((currentIndex) => (currentIndex - 1 + optionCount) % optionCount);
|
|
1429
2715
|
return;
|
|
@@ -1432,7 +2718,7 @@ export function App(props) {
|
|
|
1432
2718
|
setOnboardingIndex((currentIndex) => (currentIndex + 1) % optionCount);
|
|
1433
2719
|
return;
|
|
1434
2720
|
}
|
|
1435
|
-
if (optionCount > 0 && key.return
|
|
2721
|
+
if (optionCount > 0 && key.return) {
|
|
1436
2722
|
void handleOnboardingSubmit(String(onboardingIndex + 1));
|
|
1437
2723
|
return;
|
|
1438
2724
|
}
|
|
@@ -1457,6 +2743,16 @@ export function App(props) {
|
|
|
1457
2743
|
}
|
|
1458
2744
|
}
|
|
1459
2745
|
const canUsePanelKeys = input.length === 0 || isRunning;
|
|
2746
|
+
if (canUsePanelKeys && key.upArrow && paletteItems.length === 0) {
|
|
2747
|
+
const setOffset = activeScrollPane === "session" ? setSessionScrollOffset : setTranscriptScrollOffset;
|
|
2748
|
+
setOffset((currentOffset) => currentOffset + 1);
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
if (canUsePanelKeys && key.downArrow && paletteItems.length === 0) {
|
|
2752
|
+
const setOffset = activeScrollPane === "session" ? setSessionScrollOffset : setTranscriptScrollOffset;
|
|
2753
|
+
setOffset((currentOffset) => Math.max(0, currentOffset - 1));
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
1460
2756
|
if (canUsePanelKeys && key.leftArrow) {
|
|
1461
2757
|
setActiveScrollPane("session");
|
|
1462
2758
|
return;
|
|
@@ -1485,24 +2781,46 @@ export function App(props) {
|
|
|
1485
2781
|
toggleMode();
|
|
1486
2782
|
return;
|
|
1487
2783
|
}
|
|
1488
|
-
if (!isRunning && input.length === 0 && inputValue === "q") {
|
|
1489
|
-
void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(exit);
|
|
1490
|
-
}
|
|
1491
2784
|
});
|
|
1492
2785
|
useEffect(() => {
|
|
2786
|
+
const gracefulStopOrExit = () => {
|
|
2787
|
+
const now = Date.now();
|
|
2788
|
+
const state = runtimeStateRef.current;
|
|
2789
|
+
if ((state.isRunning || state.hasPendingApproval) && now - state.lastSigintAt > 1500) {
|
|
2790
|
+
state.lastSigintAt = now;
|
|
2791
|
+
abortControllerRef.current?.abort();
|
|
2792
|
+
approvalResolverRef.current?.("deny");
|
|
2793
|
+
approvalResolverRef.current = null;
|
|
2794
|
+
setPendingApproval(null);
|
|
2795
|
+
setBypassConfirmation(false);
|
|
2796
|
+
setInput("");
|
|
2797
|
+
setStatus("stopping");
|
|
2798
|
+
setWorkState("idle");
|
|
2799
|
+
appendLine({
|
|
2800
|
+
kind: "status",
|
|
2801
|
+
tone: "warning",
|
|
2802
|
+
label: "stop",
|
|
2803
|
+
text: "Stopping current task. Press Ctrl-C again to quit."
|
|
2804
|
+
});
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(() => {
|
|
2808
|
+
process.exit(0);
|
|
2809
|
+
});
|
|
2810
|
+
};
|
|
1493
2811
|
const unloadAndExit = () => {
|
|
1494
2812
|
void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(() => {
|
|
1495
2813
|
process.exit(0);
|
|
1496
2814
|
});
|
|
1497
2815
|
};
|
|
1498
|
-
process.
|
|
1499
|
-
process.
|
|
2816
|
+
process.on("SIGINT", gracefulStopOrExit);
|
|
2817
|
+
process.on("SIGTERM", unloadAndExit);
|
|
1500
2818
|
return () => {
|
|
1501
|
-
process.off("SIGINT",
|
|
2819
|
+
process.off("SIGINT", gracefulStopOrExit);
|
|
1502
2820
|
process.off("SIGTERM", unloadAndExit);
|
|
1503
2821
|
void unloadUsedOllamaModels(usedOllamaModelsRef.current);
|
|
1504
2822
|
};
|
|
1505
|
-
}, []);
|
|
2823
|
+
}, [appendLine]);
|
|
1506
2824
|
useEffect(() => {
|
|
1507
2825
|
let previousSnapshot = readSystemStats().snapshot;
|
|
1508
2826
|
const timer = setInterval(() => {
|
|
@@ -1531,29 +2849,71 @@ export function App(props) {
|
|
|
1531
2849
|
clearInterval(timer);
|
|
1532
2850
|
};
|
|
1533
2851
|
}, []);
|
|
1534
|
-
|
|
2852
|
+
if (themePickerOpen) {
|
|
2853
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: 1, height: rootHeight, overflowY: "hidden", children: _jsx(ThemePicker, { options: themeOptions, selectedIndex: themePickerIndex, currentValue: uiTheme, height: rootHeight - 2 }) }));
|
|
2854
|
+
}
|
|
2855
|
+
if (uiTheme === "new" && !experimentalOpen) {
|
|
2856
|
+
if (onboarding) {
|
|
2857
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: rootHeight, overflowY: "hidden", children: [_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "\u25C6 PatchPilot" }), _jsx(Text, { color: "gray", children: " \u00B7 guided setup \u00B7 the new shell starts once setup is done" })] }), _jsx(OnboardingPanel, { state: onboarding, height: rootHeight - 3, selectedIndex: onboardingIndex, input: onboardingInput, busyMessage: onboardingBusyMessage, notice: onboardingNotice, formatModelLabel: formatModelLabel, formatModelDescription: formatModelDescription, onInputChange: setOnboardingInput, onInputSubmit: (value) => void handleOnboardingSubmit(value) })] }));
|
|
2858
|
+
}
|
|
2859
|
+
return (_jsx(ExperimentalShell, { provider: settings.provider, model: settings.model, workspace: settings.workspace, sessionId: sessionStoreRef.current.sessionId, agentMode: agentMode, allowWrite: settings.allowWrite, allowShell: settings.allowShell, subagents: settings.subagents, workState: workState, status: status, isRunning: isRunning, ultramaxxRun: ultramaxxRun, telemetry: telemetry, sessionTelemetry: sessionTelemetry, draftTokens: draftTokens, lines: lines, todos: todos, todoFrame: todoFrame, pendingApproval: pendingApproval, bypassConfirmation: bypassConfirmation, updatePrompt: updatePrompt, updateBusy: updateBusy, reauthActive: Boolean(reauthPrompt) || reauthBusy, reauthBusy: reauthBusy, transcriptScrollOffset: transcriptScrollOffset, input: input, paletteItems: paletteItems, paletteIndex: paletteIndex, rows: terminalRows, columns: terminalColumns, activeHost: activeHost, artifacts: artifacts, onChange: setInput, onSubmit: (value) => void handleSubmit(value), onAttach: attachFile }));
|
|
2860
|
+
}
|
|
2861
|
+
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, workState: workState, 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 }), experimentalOpen ? (_jsx(ExperimentalPanel, { flags: experimentalFlags, selectedIndex: experimentalIndex, height: bodyHeight })) : onboarding ? (_jsx(OnboardingPanel, { state: onboarding, height: bodyHeight, selectedIndex: onboardingIndex, input: onboardingInput, busyMessage: onboardingBusyMessage, notice: onboardingNotice, formatModelLabel: formatModelLabel, formatModelDescription: formatModelDescription, onInputChange: setOnboardingInput, onInputSubmit: (value) => void handleOnboardingSubmit(value) })) : (_jsxs(Box, { flexDirection: "row", height: bodyHeight, 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, workState: workState, sessionId: sessionStoreRef.current.sessionId, systemStats: systemStats, gpuStats: gpuStats, telemetry: telemetry, sessionTelemetry: sessionTelemetry, draftTokens: draftTokens, height: bodyHeight, scrollOffset: sessionScrollOffset, advisors: advisorNotes, isActive: activeScrollPane === "session", activeHost: activeHost }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, height: bodyHeight, overflowY: "hidden", children: [_jsx(Transcript, { lines: lines, isRunning: isRunning, isActive: activeScrollPane === "transcript", height: transcriptHeight, width: transcriptWidth, scrollOffset: transcriptScrollOffset, todos: todos, todoFrame: todoFrame, verbIndex: verbTick, status: status, workState: workState, isApprovalWaiting: blockingPromptActive }), _jsx(ReauthPromptPanel, { active: reauthPromptActive, busy: reauthBusy }), _jsx(UpdatePromptPanel, { prompt: updatePromptActive ? updatePrompt : null, busy: updatePromptActive && updateBusy }), _jsx(ApprovalPanel, { request: approvalPromptActive ? pendingApproval : null, bypassConfirmation: approvalPromptActive && bypassConfirmation }), _jsx(Composer, { input: input, isRunning: isRunning, status: status, workState: workState, draftTokens: draftTokens, width: transcriptWidth, isApprovalWaiting: blockingPromptActive, onChange: setInput, onSubmit: (value) => void handleSubmit(value) }), paletteItems.length > 0 ? _jsx(CommandSuggestions, { items: paletteItems, selectedIndex: paletteIndex }) : null, _jsx(FooterHints, { activePane: activeScrollPane })] })] }))] }));
|
|
1535
2862
|
}
|
|
1536
2863
|
async function loadAvailableModels(provider, ollamaUrl, setModelOptions, refresh = false) {
|
|
1537
|
-
const cacheKey =
|
|
2864
|
+
const cacheKey = modelCacheKey(provider, ollamaUrl);
|
|
1538
2865
|
const cachedModels = modelCache.get(cacheKey);
|
|
1539
2866
|
if (!refresh && cachedModels && cachedModels.expiresAt > Date.now()) {
|
|
2867
|
+
rememberModelDescriptors(cachedModels.descriptors);
|
|
1540
2868
|
setModelOptions(cachedModels.models);
|
|
1541
2869
|
return cachedModels.models;
|
|
1542
2870
|
}
|
|
1543
|
-
const
|
|
2871
|
+
const client = createModelClient({
|
|
1544
2872
|
provider,
|
|
1545
2873
|
ollamaUrl
|
|
1546
|
-
})
|
|
2874
|
+
});
|
|
2875
|
+
const descriptors = client.listModelDescriptors
|
|
2876
|
+
? await client.listModelDescriptors()
|
|
2877
|
+
: (await client.listModels()).map((model) => ({ id: model, displayName: model }));
|
|
2878
|
+
const models = descriptors.map((model) => model.id);
|
|
2879
|
+
rememberModelDescriptors(descriptors);
|
|
1547
2880
|
modelCache.set(cacheKey, {
|
|
1548
2881
|
models,
|
|
2882
|
+
descriptors,
|
|
1549
2883
|
expiresAt: Date.now() + modelCacheTtlMs
|
|
1550
2884
|
});
|
|
1551
2885
|
setModelOptions(models);
|
|
1552
2886
|
return models;
|
|
1553
2887
|
}
|
|
1554
|
-
|
|
2888
|
+
function modelCacheKey(provider, ollamaUrl) {
|
|
2889
|
+
if (provider === "ollama") {
|
|
2890
|
+
return `${provider}:${ollamaUrl}`;
|
|
2891
|
+
}
|
|
2892
|
+
if (provider === "gemini-wrapper") {
|
|
2893
|
+
return [
|
|
2894
|
+
provider,
|
|
2895
|
+
readGeminiWrapperMode(),
|
|
2896
|
+
readGeminiWrapperBaseUrl() || "python",
|
|
2897
|
+
readGeminiWrapperPythonCommand(),
|
|
2898
|
+
readGeminiWrapperCookiesJson()
|
|
2899
|
+
].join(":");
|
|
2900
|
+
}
|
|
2901
|
+
return `${provider}:default`;
|
|
2902
|
+
}
|
|
2903
|
+
function rememberModelDescriptors(descriptors) {
|
|
2904
|
+
for (const descriptor of descriptors) {
|
|
2905
|
+
modelDescriptorIndex.set(descriptor.id, descriptor);
|
|
2906
|
+
if (descriptor.modelName) {
|
|
2907
|
+
modelDescriptorIndex.set(descriptor.modelName, descriptor);
|
|
2908
|
+
}
|
|
2909
|
+
if (descriptor.displayName) {
|
|
2910
|
+
modelDescriptorIndex.set(descriptor.displayName, descriptor);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
async function loadKnownOrAvailableModels(provider, ollamaUrl, modelOptions, setModelOptions, appendLine, options = {}) {
|
|
1555
2915
|
try {
|
|
1556
|
-
return modelOptions.length > 0 ? modelOptions : await loadAvailableModels(provider, ollamaUrl, setModelOptions);
|
|
2916
|
+
return !options.refresh && modelOptions.length > 0 ? modelOptions : await loadAvailableModels(provider, ollamaUrl, setModelOptions, options.refresh);
|
|
1557
2917
|
}
|
|
1558
2918
|
catch (error) {
|
|
1559
2919
|
appendLine({
|
|
@@ -1577,7 +2937,7 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
|
|
|
1577
2937
|
if (!installedModels) {
|
|
1578
2938
|
return;
|
|
1579
2939
|
}
|
|
1580
|
-
if (!installedModels.includes(nextModel) && !(provider
|
|
2940
|
+
if (!installedModels.includes(nextModel) && !canUseUnverifiedCloudModel(provider, nextModel)) {
|
|
1581
2941
|
appendLine({
|
|
1582
2942
|
tone: "warning",
|
|
1583
2943
|
label: "model",
|
|
@@ -1588,9 +2948,11 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
|
|
|
1588
2948
|
? "No models installed on the selected host."
|
|
1589
2949
|
: provider === "gemini"
|
|
1590
2950
|
? "Check GEMINI_API_KEY in PatchPilot config."
|
|
1591
|
-
: provider === "
|
|
1592
|
-
? "Check
|
|
1593
|
-
:
|
|
2951
|
+
: provider === "gemini-wrapper"
|
|
2952
|
+
? "Check PATCHPILOT_GEMINI_WRAPPER_BASE_URL in PatchPilot config."
|
|
2953
|
+
: provider === "openrouter"
|
|
2954
|
+
? "Check OPENROUTER_API_KEY in PatchPilot config."
|
|
2955
|
+
: "Run codex login first."
|
|
1594
2956
|
});
|
|
1595
2957
|
return;
|
|
1596
2958
|
}
|
|
@@ -1606,7 +2968,7 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
|
|
|
1606
2968
|
appendLine({
|
|
1607
2969
|
tone: installedModels.includes(nextModel) ? "success" : "warning",
|
|
1608
2970
|
label: "model",
|
|
1609
|
-
text: installedModels.includes(nextModel) ? `switched to ${nextModel}` : `switched to unverified ${provider} model ${nextModel}`,
|
|
2971
|
+
text: installedModels.includes(nextModel) ? `switched to ${formatModelLabel(nextModel)}` : `switched to unverified ${provider} model ${nextModel}`,
|
|
1610
2972
|
detail: installedModels.includes(nextModel) ? undefined : "The provider did not list this model in discovery. PatchPilot will try it and surface the provider error if it is unavailable."
|
|
1611
2973
|
});
|
|
1612
2974
|
if (provider === "openrouter" && isOpenRouterFreeModel(nextModel)) {
|
|
@@ -1618,7 +2980,7 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
|
|
|
1618
2980
|
});
|
|
1619
2981
|
}
|
|
1620
2982
|
}
|
|
1621
|
-
async function resolveRunnableSettings(settings, modelOptions, appendLine, setModelOptions) {
|
|
2983
|
+
async function resolveRunnableSettings(settings, modelOptions, appendLine, setModelOptions, onProviderError) {
|
|
1622
2984
|
let installedModels;
|
|
1623
2985
|
try {
|
|
1624
2986
|
installedModels = modelOptions.includes(settings.model)
|
|
@@ -1626,14 +2988,16 @@ async function resolveRunnableSettings(settings, modelOptions, appendLine, setMo
|
|
|
1626
2988
|
: await loadAvailableModels(settings.provider, settings.ollamaUrl, setModelOptions);
|
|
1627
2989
|
}
|
|
1628
2990
|
catch (error) {
|
|
2991
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1629
2992
|
appendLine({
|
|
1630
2993
|
tone: "danger",
|
|
1631
2994
|
label: settings.provider,
|
|
1632
|
-
text:
|
|
2995
|
+
text: message
|
|
1633
2996
|
});
|
|
2997
|
+
onProviderError?.(message);
|
|
1634
2998
|
return null;
|
|
1635
2999
|
}
|
|
1636
|
-
if (installedModels.includes(settings.model) || (settings.provider
|
|
3000
|
+
if (installedModels.includes(settings.model) || canUseUnverifiedCloudModel(settings.provider, settings.model)) {
|
|
1637
3001
|
if (!installedModels.includes(settings.model)) {
|
|
1638
3002
|
appendLine({
|
|
1639
3003
|
tone: "warning",
|
|
@@ -1654,9 +3018,11 @@ async function resolveRunnableSettings(settings, modelOptions, appendLine, setMo
|
|
|
1654
3018
|
? "No models installed on the selected host."
|
|
1655
3019
|
: settings.provider === "gemini"
|
|
1656
3020
|
? "No Gemini models listed. Check GEMINI_API_KEY in PatchPilot config."
|
|
1657
|
-
: settings.provider === "
|
|
1658
|
-
? "No
|
|
1659
|
-
:
|
|
3021
|
+
: settings.provider === "gemini-wrapper"
|
|
3022
|
+
? "No Gemini-Wrapper models listed. Check gemini_webapi install and PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON in PatchPilot config."
|
|
3023
|
+
: settings.provider === "openrouter"
|
|
3024
|
+
? "No OpenRouter models listed. Check OPENROUTER_API_KEY in PatchPilot config."
|
|
3025
|
+
: "Codex OAuth is not ready. Run codex login."
|
|
1660
3026
|
});
|
|
1661
3027
|
return null;
|
|
1662
3028
|
}
|
|
@@ -1666,7 +3032,6 @@ function buildCommandSuggestionItems(options) {
|
|
|
1666
3032
|
}
|
|
1667
3033
|
const trimmedInput = options.input.trimStart().toLowerCase();
|
|
1668
3034
|
const items = filterSlashCommands(options.input)
|
|
1669
|
-
.slice(0, 6)
|
|
1670
3035
|
.map((command) => {
|
|
1671
3036
|
const baseCommand = `/${command.name}`;
|
|
1672
3037
|
return {
|
|
@@ -1701,7 +3066,7 @@ function buildCommandSuggestionItems(options) {
|
|
|
1701
3066
|
})));
|
|
1702
3067
|
}
|
|
1703
3068
|
}
|
|
1704
|
-
if (trimmedInput
|
|
3069
|
+
if (trimmedInput.startsWith("/models ") || trimmedInput.startsWith("/model ")) {
|
|
1705
3070
|
const modelQuery = trimmedInput.replace(/^\/models?/, "").trim();
|
|
1706
3071
|
if (options.isLoadingModels) {
|
|
1707
3072
|
items.unshift({
|
|
@@ -1714,28 +3079,39 @@ function buildCommandSuggestionItems(options) {
|
|
|
1714
3079
|
});
|
|
1715
3080
|
}
|
|
1716
3081
|
else {
|
|
1717
|
-
items.unshift(...selectableModels(modelQuery, options.modelOptions).slice(0, 8).map((model) => ({
|
|
3082
|
+
items.unshift(...selectableModels(modelQuery, options.modelOptions, formatModelLabel).slice(0, 8).map((model) => ({
|
|
1718
3083
|
key: `model-${model}`,
|
|
1719
3084
|
category: "model",
|
|
1720
|
-
label: model,
|
|
1721
|
-
detail: `${model === options.currentModel ? "current" : "available"} ${options.provider}`,
|
|
3085
|
+
label: formatModelLabel(model),
|
|
3086
|
+
detail: `${model === options.currentModel ? "current" : "available"} ${options.provider}${formatModelDescription(model)}`,
|
|
1722
3087
|
command: `/model ${model}`,
|
|
1723
3088
|
execute: true
|
|
1724
3089
|
})));
|
|
1725
3090
|
}
|
|
1726
3091
|
}
|
|
1727
|
-
return items
|
|
3092
|
+
return items;
|
|
1728
3093
|
}
|
|
1729
3094
|
function getOnboardingOptionCount(onboarding) {
|
|
1730
3095
|
switch (onboarding.step) {
|
|
3096
|
+
case "welcome":
|
|
3097
|
+
return 1;
|
|
3098
|
+
case "disclaimer":
|
|
3099
|
+
return 0;
|
|
1731
3100
|
case "entry":
|
|
1732
|
-
return
|
|
3101
|
+
return 7;
|
|
1733
3102
|
case "host":
|
|
1734
3103
|
return onboarding.hosts.length + 1;
|
|
1735
3104
|
case "api-key-choice":
|
|
3105
|
+
if (onboarding.provider === "gemini-wrapper") {
|
|
3106
|
+
return onboarding.hasExistingKey ? 3 : 2;
|
|
3107
|
+
}
|
|
1736
3108
|
return onboarding.hasExistingKey ? 2 : 1;
|
|
3109
|
+
case "gemini-wrapper-model-mode":
|
|
3110
|
+
return geminiWrapperShortcutModels.length + 1;
|
|
1737
3111
|
case "model":
|
|
1738
3112
|
return onboarding.models.length;
|
|
3113
|
+
case "preferences":
|
|
3114
|
+
return preferenceRows.length + 1;
|
|
1739
3115
|
default:
|
|
1740
3116
|
return 0;
|
|
1741
3117
|
}
|
|
@@ -1743,7 +3119,7 @@ function getOnboardingOptionCount(onboarding) {
|
|
|
1743
3119
|
function readEntrySelection(value, selectedIndex) {
|
|
1744
3120
|
const normalizedValue = value.trim().toLowerCase();
|
|
1745
3121
|
if (!normalizedValue) {
|
|
1746
|
-
return ["local", "host", "gemini", "openrouter", "nvidia", "codex"][selectedIndex];
|
|
3122
|
+
return ["local", "host", "gemini", "gemini-wrapper", "openrouter", "nvidia", "codex"][selectedIndex];
|
|
1747
3123
|
}
|
|
1748
3124
|
if (normalizedValue === "1" || normalizedValue === "local" || normalizedValue === "this device") {
|
|
1749
3125
|
return "local";
|
|
@@ -1754,17 +3130,64 @@ function readEntrySelection(value, selectedIndex) {
|
|
|
1754
3130
|
if (normalizedValue === "3" || normalizedValue === "gemini" || normalizedValue === "google") {
|
|
1755
3131
|
return "gemini";
|
|
1756
3132
|
}
|
|
1757
|
-
if (normalizedValue === "4" || normalizedValue === "
|
|
3133
|
+
if (normalizedValue === "4" || normalizedValue === "gemini-wrapper" || normalizedValue === "geminiwrapper" || normalizedValue === "google-wrapper") {
|
|
3134
|
+
return "gemini-wrapper";
|
|
3135
|
+
}
|
|
3136
|
+
if (normalizedValue === "5" || normalizedValue === "openrouter" || normalizedValue === "open-router") {
|
|
1758
3137
|
return "openrouter";
|
|
1759
3138
|
}
|
|
1760
|
-
if (normalizedValue === "
|
|
3139
|
+
if (normalizedValue === "6" || normalizedValue === "nvidia" || normalizedValue === "nim") {
|
|
1761
3140
|
return "nvidia";
|
|
1762
3141
|
}
|
|
1763
|
-
if (normalizedValue === "
|
|
3142
|
+
if (normalizedValue === "7" || normalizedValue === "codex") {
|
|
1764
3143
|
return "codex";
|
|
1765
3144
|
}
|
|
1766
3145
|
return null;
|
|
1767
3146
|
}
|
|
3147
|
+
function readBooleanEnv(value, fallback) {
|
|
3148
|
+
if (!value) {
|
|
3149
|
+
return fallback;
|
|
3150
|
+
}
|
|
3151
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
3152
|
+
if (["1", "true", "yes", "on", "enabled"].includes(normalizedValue)) {
|
|
3153
|
+
return true;
|
|
3154
|
+
}
|
|
3155
|
+
if (["0", "false", "no", "off", "disabled"].includes(normalizedValue)) {
|
|
3156
|
+
return false;
|
|
3157
|
+
}
|
|
3158
|
+
return fallback;
|
|
3159
|
+
}
|
|
3160
|
+
function normalizeExperimentalFlag(value) {
|
|
3161
|
+
switch (value.trim().toLowerCase()) {
|
|
3162
|
+
case "file-analysis":
|
|
3163
|
+
case "fileanalysis":
|
|
3164
|
+
case "files":
|
|
3165
|
+
return "fileAnalysis";
|
|
3166
|
+
case "memory":
|
|
3167
|
+
return "memory";
|
|
3168
|
+
case "subagents":
|
|
3169
|
+
case "agents":
|
|
3170
|
+
return "subagents";
|
|
3171
|
+
case "shell-metacharacters":
|
|
3172
|
+
case "shell-metachars":
|
|
3173
|
+
case "metacharacters":
|
|
3174
|
+
case "metachars":
|
|
3175
|
+
case "shell":
|
|
3176
|
+
return "shellMetacharacters";
|
|
3177
|
+
default:
|
|
3178
|
+
return null;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
function experimentalFlagCommandName(flag) {
|
|
3182
|
+
return flag === "fileAnalysis"
|
|
3183
|
+
? "file-analysis"
|
|
3184
|
+
: flag === "shellMetacharacters"
|
|
3185
|
+
? "shell-metacharacters"
|
|
3186
|
+
: flag;
|
|
3187
|
+
}
|
|
3188
|
+
function experimentalFlagEnvName(flag) {
|
|
3189
|
+
return `PATCHPILOT_EXPERIMENTAL_${experimentalFlagCommandName(flag).replace(/-/g, "_").toUpperCase()}`;
|
|
3190
|
+
}
|
|
1768
3191
|
function readIndexedSelection(value, selectedIndex) {
|
|
1769
3192
|
const normalizedValue = value.trim();
|
|
1770
3193
|
if (!normalizedValue) {
|
|
@@ -1788,7 +3211,11 @@ function selectModelFromInput(value, models, selectedIndex, options = {}) {
|
|
|
1788
3211
|
if (models.includes(normalizedValue)) {
|
|
1789
3212
|
return normalizedValue;
|
|
1790
3213
|
}
|
|
1791
|
-
const
|
|
3214
|
+
const labelMatch = models.find((model) => formatModelLabel(model).toLowerCase() === normalizedValue.toLowerCase());
|
|
3215
|
+
if (labelMatch) {
|
|
3216
|
+
return labelMatch;
|
|
3217
|
+
}
|
|
3218
|
+
const matches = selectableModels(normalizedValue, models, formatModelLabel);
|
|
1792
3219
|
if (matches.length === 1) {
|
|
1793
3220
|
return matches[0] ?? null;
|
|
1794
3221
|
}
|
|
@@ -1797,6 +3224,9 @@ function selectModelFromInput(value, models, selectedIndex, options = {}) {
|
|
|
1797
3224
|
function isPlausibleCloudModelId(value) {
|
|
1798
3225
|
return /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(value) && value.length >= 3;
|
|
1799
3226
|
}
|
|
3227
|
+
function canUseUnverifiedCloudModel(provider, model) {
|
|
3228
|
+
return provider !== "ollama" && isPlausibleCloudModelId(model);
|
|
3229
|
+
}
|
|
1800
3230
|
function defaultModelForProvider(provider, currentModel) {
|
|
1801
3231
|
if (provider === "nvidia") {
|
|
1802
3232
|
return currentModel.includes("/") && !currentModel.startsWith("openrouter/") ? currentModel : defaultNvidiaModel;
|
|
@@ -1804,6 +3234,9 @@ function defaultModelForProvider(provider, currentModel) {
|
|
|
1804
3234
|
if (provider === "openrouter") {
|
|
1805
3235
|
return currentModel.includes("/") ? currentModel : defaultOpenRouterModel;
|
|
1806
3236
|
}
|
|
3237
|
+
if (provider === "gemini-wrapper") {
|
|
3238
|
+
return geminiWrapperCuratedModels.includes(currentModel) || currentModel.startsWith("gemini-") || modelDescriptorIndex.has(currentModel) ? currentModel : defaultGeminiWrapperModel;
|
|
3239
|
+
}
|
|
1807
3240
|
if (provider === "gemini") {
|
|
1808
3241
|
return currentModel.startsWith("gemini-") ? currentModel : defaultGeminiModel;
|
|
1809
3242
|
}
|
|
@@ -1821,12 +3254,19 @@ function openApiKeyChoice(provider, setOnboarding, setOnboardingIndex) {
|
|
|
1821
3254
|
setOnboardingIndex(0);
|
|
1822
3255
|
}
|
|
1823
3256
|
function needsApiKey(provider) {
|
|
1824
|
-
return provider === "gemini" || provider === "openrouter" || provider === "nvidia";
|
|
3257
|
+
return provider === "gemini" || provider === "gemini-wrapper" || provider === "openrouter" || provider === "nvidia";
|
|
1825
3258
|
}
|
|
1826
3259
|
function hasApiKey(provider) {
|
|
1827
3260
|
if (provider === "gemini") {
|
|
1828
3261
|
return Boolean(readGeminiApiKey());
|
|
1829
3262
|
}
|
|
3263
|
+
if (provider === "gemini-wrapper") {
|
|
3264
|
+
const baseUrl = readGeminiWrapperBaseUrl();
|
|
3265
|
+
if (readGeminiWrapperMode() === "http") {
|
|
3266
|
+
return !geminiWrapperRequiresApiKey(baseUrl) || Boolean(readGeminiWrapperApiKey());
|
|
3267
|
+
}
|
|
3268
|
+
return Boolean(readGeminiWrapperCookiesJson());
|
|
3269
|
+
}
|
|
1830
3270
|
if (provider === "openrouter") {
|
|
1831
3271
|
return Boolean(readOpenRouterApiKey());
|
|
1832
3272
|
}
|
|
@@ -1874,6 +3314,219 @@ function upsertAdvisorNote(notes, nextNote) {
|
|
|
1874
3314
|
const nextNotes = notes.filter((note) => note.role !== nextNote.role);
|
|
1875
3315
|
return [...nextNotes, nextNote].slice(-2);
|
|
1876
3316
|
}
|
|
3317
|
+
function UpdatePromptPanel(props) {
|
|
3318
|
+
if (!props.prompt && !props.busy) {
|
|
3319
|
+
return null;
|
|
3320
|
+
}
|
|
3321
|
+
const command = props.prompt?.command ?? "npm update -g @jx-grxf/patchpilot";
|
|
3322
|
+
return (_jsxs(Box, { borderStyle: "double", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "UPDATE AVAILABLE" }), props.busy ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: "Updating PatchPilot..." }), _jsx(Text, { color: "gray", children: command })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "white", children: ["Install PatchPilot ", props.prompt?.latestVersion, " now?"] }), _jsxs(Text, { color: "gray", children: ["Current ", props.prompt?.currentVersion, " \u00B7 source ", props.prompt?.source, " \u00B7 ", command] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "[y]" }), _jsx(Text, { color: "gray", children: " update " }), _jsx(Text, { color: "red", bold: true, children: "[n / esc]" }), _jsx(Text, { color: "gray", children: " skip" })] })] }))] }));
|
|
3323
|
+
}
|
|
3324
|
+
function ReauthPromptPanel(props) {
|
|
3325
|
+
if (!props.active) {
|
|
3326
|
+
return null;
|
|
3327
|
+
}
|
|
3328
|
+
return (_jsxs(Box, { borderStyle: "double", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "GEMINI COOKIES EXPIRED" }), props.busy ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: "Refreshing Gemini browser cookies..." }), _jsx(Text, { color: "gray", children: "PatchPilot will retry the prompt automatically on success." })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "white", children: "Refresh Gemini browser cookies and retry the last prompt?" }), _jsx(Text, { color: "gray", children: "Secret cookie values are imported from your signed-in browser and are not printed." }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "[y]" }), _jsx(Text, { color: "gray", children: " refresh & retry " }), _jsx(Text, { color: "red", bold: true, children: "[n / esc]" }), _jsx(Text, { color: "gray", children: " dismiss" })] })] }))] }));
|
|
3329
|
+
}
|
|
3330
|
+
function emptyToolTelemetry() {
|
|
3331
|
+
return {
|
|
3332
|
+
total: 0,
|
|
3333
|
+
succeeded: 0,
|
|
3334
|
+
failed: 0,
|
|
3335
|
+
approvals: 0,
|
|
3336
|
+
denied: 0,
|
|
3337
|
+
byTool: {}
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
function addToolTelemetry(current, tool, ok) {
|
|
3341
|
+
return {
|
|
3342
|
+
...current,
|
|
3343
|
+
total: current.total + 1,
|
|
3344
|
+
succeeded: current.succeeded + (ok ? 1 : 0),
|
|
3345
|
+
failed: current.failed + (ok ? 0 : 1),
|
|
3346
|
+
byTool: {
|
|
3347
|
+
...current.byTool,
|
|
3348
|
+
[tool]: (current.byTool[tool] ?? 0) + 1
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
function addApprovalTelemetry(current, decision) {
|
|
3353
|
+
return {
|
|
3354
|
+
...current,
|
|
3355
|
+
approvals: current.approvals + (decision === "deny" ? 0 : 1),
|
|
3356
|
+
denied: current.denied + (decision === "deny" ? 1 : 0)
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
/**
|
|
3360
|
+
* Dense operational status dock for `/status` — restores the always-available
|
|
3361
|
+
* "what mode am I in and what can happen" view the legacy sidebar provided,
|
|
3362
|
+
* without spending fixed screen rows in the new shell's header.
|
|
3363
|
+
*/
|
|
3364
|
+
function formatStatusDock(options) {
|
|
3365
|
+
const isOllama = options.provider === "ollama";
|
|
3366
|
+
const hostLine = isOllama
|
|
3367
|
+
? `${options.activeHost?.host.deviceName ?? "ollama"} ${options.activeHost?.host.url ?? options.ollamaUrl}`
|
|
3368
|
+
: `${options.provider} api`;
|
|
3369
|
+
const computeKind = isOllama ? describeComputeTarget(options.ollamaUrl).kind : "cloud";
|
|
3370
|
+
const reasoning = isOllama
|
|
3371
|
+
? `think ${options.thinkingMode}`
|
|
3372
|
+
: `think ${options.thinkingMode} · reasoning ${formatReasoningSupport(options.provider, options.model, options.reasoningEffort === "adaptive" ? undefined : options.reasoningEffort)}`;
|
|
3373
|
+
const toolCounters = Object.entries(options.toolTelemetry.byTool)
|
|
3374
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
3375
|
+
.slice(0, 6)
|
|
3376
|
+
.map(([tool, count]) => `${tool} ${count}`)
|
|
3377
|
+
.join(" · ");
|
|
3378
|
+
const advisors = options.advisorNotes.length > 0
|
|
3379
|
+
? options.advisorNotes.map((note) => ` ${note.role}: ${note.message.replace(/\s+/g, " ").slice(0, 88)}`).join("\n")
|
|
3380
|
+
: " none yet";
|
|
3381
|
+
return [
|
|
3382
|
+
`provider ${options.provider}/${options.model}`,
|
|
3383
|
+
`host ${hostLine} · compute ${computeKind} · tools local`,
|
|
3384
|
+
`mode ${options.agentMode} · write ${modePermissionLabel(options.agentMode, "write")} · shell ${modePermissionLabel(options.agentMode, "shell")}`,
|
|
3385
|
+
`model cfg ${reasoning} · subagents ${options.subagents ? "on" : "off"}`,
|
|
3386
|
+
`workspace ${options.workspace}`,
|
|
3387
|
+
`session ${options.sessionId}`,
|
|
3388
|
+
`tokens draft ${options.draftTokens} · last ${formatTokens(options.telemetry)} · session ${formatSessionTokens(options.sessionTelemetry)} · cost ${formatCost(options.sessionTelemetry.estimatedCostUsd)}`,
|
|
3389
|
+
options.toolTelemetry.total > 0
|
|
3390
|
+
? `tools ${options.toolTelemetry.total} calls · ${options.toolTelemetry.succeeded} ok · ${options.toolTelemetry.failed} failed · ${options.toolTelemetry.approvals} approved · ${options.toolTelemetry.denied} denied`
|
|
3391
|
+
: "tools none yet",
|
|
3392
|
+
toolCounters ? `counters ${toolCounters}` : "",
|
|
3393
|
+
`advisors\n${advisors}`,
|
|
3394
|
+
]
|
|
3395
|
+
.filter(Boolean)
|
|
3396
|
+
.join("\n");
|
|
3397
|
+
}
|
|
3398
|
+
function formatUsageSummary(options) {
|
|
3399
|
+
const session = options.sessionTelemetry;
|
|
3400
|
+
const cost = formatCost(session.estimatedCostUsd);
|
|
3401
|
+
const saved = estimateSessionSavings(options.provider, options.model, session);
|
|
3402
|
+
const pricingNote = pricingSourceLabel(session.costSource, saved.source);
|
|
3403
|
+
return [
|
|
3404
|
+
`${session.requests} request${session.requests === 1 ? "" : "s"}`,
|
|
3405
|
+
`${session.promptTokens} in`,
|
|
3406
|
+
`${session.responseTokens} out`,
|
|
3407
|
+
`${session.cachedPromptTokens} cached`,
|
|
3408
|
+
`${options.toolTelemetry.total} tool call${options.toolTelemetry.total === 1 ? "" : "s"}`,
|
|
3409
|
+
`cost ${cost}`,
|
|
3410
|
+
saved.costUsd !== null ? `saved ${formatCost(saved.costUsd)}` : "saved -",
|
|
3411
|
+
pricingNote
|
|
3412
|
+
].join(" · ");
|
|
3413
|
+
}
|
|
3414
|
+
function formatUsageDetail(options) {
|
|
3415
|
+
const session = options.sessionTelemetry;
|
|
3416
|
+
const saved = estimateSessionSavings(options.provider, options.model, session);
|
|
3417
|
+
const toolRows = Object.entries(options.toolTelemetry.byTool)
|
|
3418
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
3419
|
+
.map(([tool, count]) => `${tool}: ${count}`)
|
|
3420
|
+
.join("\n");
|
|
3421
|
+
return [
|
|
3422
|
+
`model: ${options.provider}/${options.model}`,
|
|
3423
|
+
`tokens: ${session.promptTokens} input, ${session.responseTokens} output, ${session.cachedPromptTokens} cached, ${session.cacheWriteTokens} cache-write, ${session.totalTokens} total`,
|
|
3424
|
+
`cost: ${formatCost(session.estimatedCostUsd)} (${session.costSource})`,
|
|
3425
|
+
saved.costUsd !== null ? `lifetime saved this session: ${formatCost(saved.costUsd)} (${saved.source})` : "lifetime saved this session: -",
|
|
3426
|
+
options.toolTelemetry.total > 0
|
|
3427
|
+
? `tools: ${options.toolTelemetry.total} total, ${options.toolTelemetry.succeeded} ok, ${options.toolTelemetry.failed} failed, ${options.toolTelemetry.approvals} approved, ${options.toolTelemetry.denied} denied`
|
|
3428
|
+
: "tools: none yet",
|
|
3429
|
+
toolRows ? `tool counters:\n${toolRows}` : "",
|
|
3430
|
+
session.costSource === "fallback-pricing" || saved.source === "fallback-pricing"
|
|
3431
|
+
? "pricing note: exact model pricing was not available, so PatchPilot used a conservative general cloud-model estimate."
|
|
3432
|
+
: session.costSource === "unknown"
|
|
3433
|
+
? "pricing note: exact pricing is unavailable for this provider/model."
|
|
3434
|
+
: ""
|
|
3435
|
+
]
|
|
3436
|
+
.filter(Boolean)
|
|
3437
|
+
.join("\n");
|
|
3438
|
+
}
|
|
3439
|
+
function estimateSessionSavings(provider, model, session) {
|
|
3440
|
+
return estimateComparableApiCost(provider, model, session.promptTokens, session.responseTokens, session.cachedPromptTokens);
|
|
3441
|
+
}
|
|
3442
|
+
function pricingSourceLabel(costSource, savedSource) {
|
|
3443
|
+
if (costSource === "fallback-pricing" || savedSource === "fallback-pricing") {
|
|
3444
|
+
return "fallback pricing";
|
|
3445
|
+
}
|
|
3446
|
+
if (costSource === "unknown" && savedSource === "unknown") {
|
|
3447
|
+
return "pricing unknown";
|
|
3448
|
+
}
|
|
3449
|
+
if (costSource === "free-route") {
|
|
3450
|
+
return "free route";
|
|
3451
|
+
}
|
|
3452
|
+
if (costSource === "mixed") {
|
|
3453
|
+
return "mixed pricing";
|
|
3454
|
+
}
|
|
3455
|
+
return "priced";
|
|
3456
|
+
}
|
|
3457
|
+
const bytesPerMiB = 1024 * 1024;
|
|
3458
|
+
const geminiAppsPromptFileLimit = 10;
|
|
3459
|
+
const geminiNonVideoFileLimitBytes = 100 * bytesPerMiB;
|
|
3460
|
+
const geminiApiPdfLimitBytes = 50 * bytesPerMiB;
|
|
3461
|
+
const geminiPdfCautionBytes = 20 * bytesPerMiB;
|
|
3462
|
+
const geminiInlineRequestWarnBytes = 25 * bytesPerMiB;
|
|
3463
|
+
function attachmentLimitWarning(paths, provider) {
|
|
3464
|
+
if (paths.length === 0 || (provider !== "gemini" && provider !== "gemini-wrapper")) {
|
|
3465
|
+
return null;
|
|
3466
|
+
}
|
|
3467
|
+
const files = paths.map((filePath) => ({
|
|
3468
|
+
path: filePath,
|
|
3469
|
+
type: attachmentTypeForPath(filePath),
|
|
3470
|
+
size: readFileSize(filePath)
|
|
3471
|
+
}));
|
|
3472
|
+
const knownTotalBytes = files.reduce((total, file) => total + (file.size ?? 0), 0);
|
|
3473
|
+
const tooLargePdf = files.find((file) => file.type === "PDF" && typeof file.size === "number" && file.size > geminiApiPdfLimitBytes);
|
|
3474
|
+
const largePdf = files.find((file) => file.type === "PDF" && typeof file.size === "number" && file.size > geminiPdfCautionBytes);
|
|
3475
|
+
const tooLargeFile = files.find((file) => typeof file.size === "number" && file.size > geminiNonVideoFileLimitBytes);
|
|
3476
|
+
if (paths.length > geminiAppsPromptFileLimit) {
|
|
3477
|
+
return `Attached ${paths.length} files; Gemini web-style uploads are capped around ${geminiAppsPromptFileLimit} files per prompt. Split this into smaller batches.`;
|
|
3478
|
+
}
|
|
3479
|
+
if (tooLargePdf) {
|
|
3480
|
+
return `${attachmentTypeForPath(tooLargePdf.path)} file ${attachmentBasename(tooLargePdf.path)} is over 50 MiB; Gemini API PDF input can reject it.`;
|
|
3481
|
+
}
|
|
3482
|
+
if (tooLargeFile) {
|
|
3483
|
+
return `${attachmentTypeForPath(tooLargeFile.path)} file ${attachmentBasename(tooLargeFile.path)} is over 100 MiB; Gemini file prompts may reject it.`;
|
|
3484
|
+
}
|
|
3485
|
+
if (largePdf) {
|
|
3486
|
+
return `${attachmentTypeForPath(largePdf.path)} file ${attachmentBasename(largePdf.path)} is over 20 MiB; Gemini PDF analysis can be slow or incomplete.`;
|
|
3487
|
+
}
|
|
3488
|
+
if (knownTotalBytes > geminiInlineRequestWarnBytes) {
|
|
3489
|
+
return `Attached files total about ${formatMiB(knownTotalBytes)}; Gemini analysis is more reliable in smaller batches.`;
|
|
3490
|
+
}
|
|
3491
|
+
if (paths.length > 3) {
|
|
3492
|
+
return `Attached ${paths.length} files; PatchPilot will reference them, but Gemini/Gemini-Wrapper is more reliable if you split large batches.`;
|
|
3493
|
+
}
|
|
3494
|
+
return null;
|
|
3495
|
+
}
|
|
3496
|
+
function readFileSize(filePath) {
|
|
3497
|
+
try {
|
|
3498
|
+
const stats = statSync(filePath);
|
|
3499
|
+
return stats.isFile() ? stats.size : null;
|
|
3500
|
+
}
|
|
3501
|
+
catch {
|
|
3502
|
+
return null;
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
function formatMiB(bytes) {
|
|
3506
|
+
return `${Math.round((bytes / bytesPerMiB) * 10) / 10} MiB`;
|
|
3507
|
+
}
|
|
3508
|
+
function formatAttachedDocuments(paths) {
|
|
3509
|
+
const counts = new Map();
|
|
3510
|
+
return paths
|
|
3511
|
+
.map((filePath) => {
|
|
3512
|
+
const kind = attachmentKindForPath(filePath) ?? "file";
|
|
3513
|
+
const type = attachmentTypeForPath(filePath);
|
|
3514
|
+
const index = (counts.get(type) ?? 0) + 1;
|
|
3515
|
+
counts.set(type, index);
|
|
3516
|
+
return `- ${attachmentLabel(kind, index, filePath)} path=${JSON.stringify(filePath)}`;
|
|
3517
|
+
})
|
|
3518
|
+
.join("\n");
|
|
3519
|
+
}
|
|
3520
|
+
/** Last path segment, splitting on both POSIX and Windows separators. */
|
|
3521
|
+
function attachmentBasename(filePath) {
|
|
3522
|
+
return filePath.split(/[\\/]/).filter(Boolean).at(-1) ?? filePath;
|
|
3523
|
+
}
|
|
3524
|
+
function formatAttachmentDigestPath(filePath) {
|
|
3525
|
+
return JSON.stringify(filePath.split(/[\\/]/).filter(Boolean).at(-1) ?? filePath);
|
|
3526
|
+
}
|
|
3527
|
+
function randomLegacyVerbIndex() {
|
|
3528
|
+
return Math.floor(Math.random() * 1_000_000);
|
|
3529
|
+
}
|
|
1877
3530
|
function eventToLine(event) {
|
|
1878
3531
|
switch (event.type) {
|
|
1879
3532
|
case "status":
|
|
@@ -1907,12 +3560,21 @@ function eventToLine(event) {
|
|
|
1907
3560
|
tone: event.ok ? "success" : "warning",
|
|
1908
3561
|
label: event.name,
|
|
1909
3562
|
text: event.summary,
|
|
3563
|
+
detail: event.ok ? previewToolContent(event.content) : event.content,
|
|
1910
3564
|
workState: event.workState,
|
|
1911
3565
|
tool: event.name,
|
|
1912
3566
|
toolCallId: event.toolCallId,
|
|
1913
3567
|
category: event.category,
|
|
1914
3568
|
preview: event.preview
|
|
1915
3569
|
};
|
|
3570
|
+
case "todo":
|
|
3571
|
+
return {
|
|
3572
|
+
kind: "status",
|
|
3573
|
+
tone: "muted",
|
|
3574
|
+
label: "todo",
|
|
3575
|
+
text: event.summary,
|
|
3576
|
+
workState: event.workState
|
|
3577
|
+
};
|
|
1916
3578
|
case "approval":
|
|
1917
3579
|
return {
|
|
1918
3580
|
kind: "approval",
|
|
@@ -1950,6 +3612,16 @@ function eventToLine(event) {
|
|
|
1950
3612
|
};
|
|
1951
3613
|
}
|
|
1952
3614
|
}
|
|
3615
|
+
function previewToolContent(content) {
|
|
3616
|
+
const value = content?.trim();
|
|
3617
|
+
if (!value) {
|
|
3618
|
+
return undefined;
|
|
3619
|
+
}
|
|
3620
|
+
const lines = value.split(/\r?\n/);
|
|
3621
|
+
const preview = lines.slice(0, 6).join("\n");
|
|
3622
|
+
const suffix = lines.length > 6 ? `\n...[${lines.length - 6} more lines]` : "";
|
|
3623
|
+
return `${preview}${suffix}`;
|
|
3624
|
+
}
|
|
1953
3625
|
function eventToStatus(event) {
|
|
1954
3626
|
if (event.type === "status") {
|
|
1955
3627
|
return event.message;
|
|
@@ -1957,6 +3629,9 @@ function eventToStatus(event) {
|
|
|
1957
3629
|
if (event.type === "tool") {
|
|
1958
3630
|
return `${event.name}: ${event.summary}`;
|
|
1959
3631
|
}
|
|
3632
|
+
if (event.type === "todo") {
|
|
3633
|
+
return event.summary;
|
|
3634
|
+
}
|
|
1960
3635
|
if (event.type === "subagent") {
|
|
1961
3636
|
return `${event.role} subagent`;
|
|
1962
3637
|
}
|
|
@@ -1965,6 +3640,19 @@ function eventToStatus(event) {
|
|
|
1965
3640
|
}
|
|
1966
3641
|
return event.type;
|
|
1967
3642
|
}
|
|
3643
|
+
function workStateForApprovalTool(tool) {
|
|
3644
|
+
const category = getToolSpec(tool).category;
|
|
3645
|
+
if (category === "write") {
|
|
3646
|
+
return "editing";
|
|
3647
|
+
}
|
|
3648
|
+
if (category === "shell" || category === "test") {
|
|
3649
|
+
return "verifying";
|
|
3650
|
+
}
|
|
3651
|
+
if (category === "read" || category === "search" || category === "document" || category === "git") {
|
|
3652
|
+
return "reading";
|
|
3653
|
+
}
|
|
3654
|
+
return "inspecting";
|
|
3655
|
+
}
|
|
1968
3656
|
function defaultLogKind(line) {
|
|
1969
3657
|
if (line.kind) {
|
|
1970
3658
|
return line.kind;
|
|
@@ -1992,8 +3680,21 @@ function formatModelOptions(models, currentModel) {
|
|
|
1992
3680
|
return models
|
|
1993
3681
|
.map((model, index) => {
|
|
1994
3682
|
const currentMarker = model === currentModel ? " current" : "";
|
|
1995
|
-
return `${index + 1}. ${model}${currentMarker}`;
|
|
3683
|
+
return `${index + 1}. ${formatModelLabel(model)}${formatModelDescription(model)}${currentMarker}`;
|
|
1996
3684
|
})
|
|
1997
3685
|
.join("\n");
|
|
1998
3686
|
}
|
|
3687
|
+
function formatModelLabel(model) {
|
|
3688
|
+
const descriptor = modelDescriptorIndex.get(model);
|
|
3689
|
+
const label = descriptor?.displayName || descriptor?.modelName || model;
|
|
3690
|
+
return label === model ? model : `${label} (${model})`;
|
|
3691
|
+
}
|
|
3692
|
+
function formatModelDescription(model) {
|
|
3693
|
+
const descriptor = modelDescriptorIndex.get(model);
|
|
3694
|
+
if (!descriptor?.description) {
|
|
3695
|
+
return "";
|
|
3696
|
+
}
|
|
3697
|
+
const legacySuffix = descriptor.legacy ? " legacy" : "";
|
|
3698
|
+
return ` ${descriptor.description}${legacySuffix}`;
|
|
3699
|
+
}
|
|
1999
3700
|
//# sourceMappingURL=App.js.map
|