@jx-grxf/patchpilot 1.0.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/README.md +51 -16
- package/dist/cli.js +46 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/agent.d.ts +44 -1
- package/dist/core/agent.js +617 -70
- package/dist/core/agent.js.map +1 -1
- 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.js +9 -8
- 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 +43 -2
- package/dist/core/geminiWrapper.js +582 -42
- package/dist/core/geminiWrapper.js.map +1 -1
- 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 +18 -20
- package/dist/core/json.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/reasoning.js +3 -0
- package/dist/core/reasoning.js.map +1 -1
- package/dist/core/session.js +9 -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 +58 -3
- 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 +29 -0
- package/dist/core/workspace.js +1271 -92
- package/dist/core/workspace.js.map +1 -1
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +1346 -112
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/commands.js +109 -6
- 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 +26 -3
- 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 +1 -1
- package/dist/tui/components/ExperimentalPanel.js +5 -0
- package/dist/tui/components/ExperimentalPanel.js.map +1 -1
- package/dist/tui/components/OnboardingPanel.d.ts +12 -0
- package/dist/tui/components/OnboardingPanel.js +69 -21
- package/dist/tui/components/OnboardingPanel.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 +86 -16
- 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 +14 -0
- 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 +7 -0
- package/dist/tui/modes.js +12 -0
- 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 +23 -0
- package/docs/product-context.md +43 -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/package.json +4 -2
package/dist/tui/App.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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";
|
|
5
6
|
import { cleanupPatchPilot, readCleanupTarget } from "../core/cleanup.js";
|
|
6
7
|
import { defaultCodexModel, hasCodexCliOAuth } from "../core/codex.js";
|
|
7
8
|
import { describeComputeTarget } from "../core/compute.js";
|
|
9
|
+
import { ContextStore } from "../core/contextStore.js";
|
|
8
10
|
import { runDoctor } from "../core/doctor.js";
|
|
9
11
|
import { savePatchPilotEnvValues } from "../core/env.js";
|
|
10
12
|
import { defaultGeminiModel, readGeminiApiKey } from "../core/gemini.js";
|
|
11
|
-
import { defaultGeminiWrapperModel, geminiWrapperRequiresApiKey, readGeminiWrapperApiKey, readGeminiWrapperBaseUrl, readGeminiWrapperCookiesJson, readGeminiWrapperMode, readGeminiWrapperPythonCommand, saveGeminiWrapperCookieFile } from "../core/geminiWrapper.js";
|
|
13
|
+
import { defaultGeminiWrapperModel, geminiWrapperCuratedModels, geminiWrapperShortcutModels, geminiWrapperRequiresApiKey, readGeminiWrapperApiKey, readGeminiWrapperBaseUrl, readGeminiWrapperCookiesJson, readGeminiWrapperMode, readGeminiWrapperPythonCommand, importGeminiWrapperBrowserCookies, saveGeminiWrapperCookieFile } from "../core/geminiWrapper.js";
|
|
12
14
|
import { createModelClient } from "../core/modelClient.js";
|
|
13
15
|
import { defaultNvidiaModel, readNvidiaApiKey } from "../core/nvidia.js";
|
|
14
16
|
import { defaultOllamaModel, OllamaClient } from "../core/ollama.js";
|
|
@@ -16,12 +18,20 @@ import { defaultOpenRouterModel, isOpenRouterFreeModel, readOpenRouterApiKey } f
|
|
|
16
18
|
import { ensurePatchPilotGitignore, patchPilotInitPrompt } from "../core/projectInit.js";
|
|
17
19
|
import { formatReasoningSupport } from "../core/reasoning.js";
|
|
18
20
|
import { buildSessionResumeContext, listWorkspaceSessions, loadSessionSummary, SessionStore } from "../core/session.js";
|
|
19
|
-
import { addTelemetryToSession, emptySessionTelemetry, estimateTokens } from "../core/tokenAccounting.js";
|
|
20
|
-
import {
|
|
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";
|
|
21
24
|
import { ApprovalPanel } from "./components/ApprovalPanel.js";
|
|
25
|
+
import { clipboardHasImage, clipboardImageHint, readClipboardImage } from "../core/clipboard.js";
|
|
22
26
|
import { CommandSuggestions } from "./components/CommandSuggestions.js";
|
|
23
27
|
import { Composer, FooterHints } from "./components/Composer.js";
|
|
24
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";
|
|
25
35
|
import { Header } from "./components/Header.js";
|
|
26
36
|
import { OnboardingPanel } from "./components/OnboardingPanel.js";
|
|
27
37
|
import { Sidebar } from "./components/Sidebar.js";
|
|
@@ -29,20 +39,47 @@ import { Transcript } from "./components/Transcript.js";
|
|
|
29
39
|
import { filterSlashCommands, formatCommandDetail, formatCommandHelp } from "./commands.js";
|
|
30
40
|
import { formatCost, formatSessionTokens, formatTokens, normalizeModelAlias, readToggle } from "./format.js";
|
|
31
41
|
import { checkOllamaHost, discoverOllamaHosts, normalizeOllamaUrl, readOllamaHostDetails, startLocalOllamaAppAndWait } from "./hosts.js";
|
|
32
|
-
import {
|
|
42
|
+
import { computeComposerLayout } from "./layout.js";
|
|
43
|
+
import { initialAgentMode, modeDescription, modePermissionLabel, nextAgentMode, permissionsForMode, shouldBypassApproval } from "./modes.js";
|
|
33
44
|
import { selectableModels } from "./modelSelection.js";
|
|
45
|
+
import { cyclePreference, modePermissions as preferencesModePermissions, preferenceRows, preferencesEnvValues, readOnboardingPreferences } from "./onboardingPreferences.js";
|
|
34
46
|
import { readGpuStats, readSystemStats } from "./systemStats.js";
|
|
35
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
|
+
}
|
|
36
67
|
const modelCacheTtlMs = 5 * 60_000;
|
|
37
68
|
const modelCache = new Map();
|
|
69
|
+
const modelDescriptorIndex = new Map();
|
|
38
70
|
export function App(props) {
|
|
39
71
|
const { exit } = useApp();
|
|
40
72
|
const { stdout } = useStdout();
|
|
41
73
|
const [input, setInput] = useState(props.initialTask ?? "");
|
|
42
74
|
const didRunInitialTask = useRef(false);
|
|
43
75
|
const didOpenDefaultOnboarding = useRef(false);
|
|
76
|
+
const didCheckForUpdates = useRef(false);
|
|
44
77
|
const abortControllerRef = useRef(null);
|
|
78
|
+
const softStopRequestedRef = useRef(false);
|
|
79
|
+
const lastEscapeStopAtRef = useRef(0);
|
|
80
|
+
const lastAttachmentWarningRef = useRef("");
|
|
45
81
|
const sessionStoreRef = useRef(new SessionStore({ workspace: props.workspace }));
|
|
82
|
+
const contextStoreRef = useRef(new ContextStore({ workspace: props.workspace, sessionId: sessionStoreRef.current.sessionId }));
|
|
46
83
|
const approvalResolverRef = useRef(null);
|
|
47
84
|
const runtimeStateRef = useRef({
|
|
48
85
|
isRunning: false,
|
|
@@ -56,14 +93,23 @@ export function App(props) {
|
|
|
56
93
|
const activeHostSyncInFlightRef = useRef(false);
|
|
57
94
|
const autoLoadKeysRef = useRef(new Set());
|
|
58
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([]);
|
|
59
99
|
const [lines, setLines] = useState([]);
|
|
60
100
|
const [advisorNotes, setAdvisorNotes] = useState([]);
|
|
101
|
+
const [todos, setTodos] = useState([]);
|
|
102
|
+
const [todoFrame, setTodoFrame] = useState(0);
|
|
103
|
+
const [verbTick, setVerbTick] = useState(0);
|
|
61
104
|
const [isRunning, setIsRunning] = useState(false);
|
|
62
105
|
const [status, setStatus] = useState("idle");
|
|
63
106
|
const [workState, setWorkState] = useState("idle");
|
|
64
107
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
108
|
+
const [updatePrompt, setUpdatePrompt] = useState(null);
|
|
109
|
+
const [updateBusy, setUpdateBusy] = useState(false);
|
|
65
110
|
const [telemetry, setTelemetry] = useState(null);
|
|
66
111
|
const [sessionTelemetry, setSessionTelemetry] = useState(() => emptySessionTelemetry());
|
|
112
|
+
const [toolTelemetry, setToolTelemetry] = useState(() => emptyToolTelemetry());
|
|
67
113
|
const [resumeContext, setResumeContext] = useState("");
|
|
68
114
|
const [systemStats, setSystemStats] = useState(() => readSystemStats().stats);
|
|
69
115
|
const [gpuStats, setGpuStats] = useState(null);
|
|
@@ -80,8 +126,21 @@ export function App(props) {
|
|
|
80
126
|
const [experimentalFlags, setExperimentalFlags] = useState({
|
|
81
127
|
fileAnalysis: readBooleanEnv(process.env.PATCHPILOT_EXPERIMENTAL_FILE_ANALYSIS, false),
|
|
82
128
|
memory: readBooleanEnv(process.env.PATCHPILOT_EXPERIMENTAL_MEMORY, false),
|
|
83
|
-
subagents: props.subagents
|
|
129
|
+
subagents: props.subagents,
|
|
130
|
+
shellMetacharacters: readBooleanEnv(process.env.PATCHPILOT_EXPERIMENTAL_SHELL_METACHARACTERS, false)
|
|
84
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);
|
|
85
144
|
const [onboardingIndex, setOnboardingIndex] = useState(0);
|
|
86
145
|
const [onboardingInput, setOnboardingInput] = useState("");
|
|
87
146
|
const [onboardingBusyMessage, setOnboardingBusyMessage] = useState(null);
|
|
@@ -105,7 +164,11 @@ export function App(props) {
|
|
|
105
164
|
const draftTokens = estimateTokens(input);
|
|
106
165
|
const terminalRows = stdout.rows ?? 40;
|
|
107
166
|
const terminalColumns = stdout.columns ?? 120;
|
|
108
|
-
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
|
|
109
172
|
? buildCommandSuggestionItems({
|
|
110
173
|
input,
|
|
111
174
|
provider: settings.provider,
|
|
@@ -118,41 +181,234 @@ export function App(props) {
|
|
|
118
181
|
: [];
|
|
119
182
|
const rootHeight = Math.max(24, terminalRows);
|
|
120
183
|
const headerReservedHeight = 5;
|
|
121
|
-
const paletteReservedHeight = !onboarding && paletteItems.length > 0 ? Math.min(8, paletteItems.length) + 4 : 0;
|
|
122
|
-
const composerReservedHeight = onboarding || experimentalOpen ? 0 : 2;
|
|
123
|
-
const footerReservedHeight = onboarding || experimentalOpen ? 0 : 1;
|
|
124
|
-
const approvalReservedHeight = !onboarding && !experimentalOpen && (pendingApproval || bypassConfirmation) ? 6 : 0;
|
|
125
|
-
const panelHeight = Math.max(8, rootHeight - headerReservedHeight - composerReservedHeight - paletteReservedHeight - footerReservedHeight - approvalReservedHeight);
|
|
126
184
|
const transcriptWidth = Math.max(42, terminalColumns - 38);
|
|
127
|
-
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));
|
|
128
193
|
const appendLine = useCallback((line) => {
|
|
129
194
|
setLines((currentLines) => [
|
|
130
|
-
...currentLines
|
|
195
|
+
...currentLines,
|
|
131
196
|
{
|
|
132
197
|
...line,
|
|
133
198
|
kind: line.kind ?? defaultLogKind(line),
|
|
134
199
|
id: Date.now() + Math.random()
|
|
135
200
|
}
|
|
136
|
-
]);
|
|
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);
|
|
137
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]);
|
|
138
346
|
const resolveApproval = useCallback((decision) => {
|
|
139
347
|
if (!pendingApproval || !approvalResolverRef.current) {
|
|
140
348
|
return;
|
|
141
349
|
}
|
|
142
350
|
approvalResolverRef.current(decision);
|
|
143
351
|
approvalResolverRef.current = null;
|
|
352
|
+
const nextWorkState = decision === "deny" ? "error" : workStateForApprovalTool(pendingApproval.tool);
|
|
144
353
|
setInput("");
|
|
354
|
+
setStatus(decision === "deny" ? `${pendingApproval.tool} denied` : `${pendingApproval.tool} approved; running`);
|
|
355
|
+
setWorkState(nextWorkState);
|
|
145
356
|
appendLine({
|
|
146
357
|
kind: "approval",
|
|
147
358
|
tone: decision === "deny" ? "warning" : "success",
|
|
148
359
|
label: "approval",
|
|
149
360
|
text: `${pendingApproval.tool} ${decision.replace("_", " ")}`,
|
|
150
361
|
detail: pendingApproval.preview,
|
|
151
|
-
workState:
|
|
362
|
+
workState: nextWorkState,
|
|
152
363
|
tool: pendingApproval.tool
|
|
153
364
|
});
|
|
154
365
|
setPendingApproval(null);
|
|
155
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]);
|
|
156
412
|
const applyMode = useCallback((nextMode, announce = true) => {
|
|
157
413
|
const permissions = permissionsForMode(nextMode);
|
|
158
414
|
setAgentMode(nextMode);
|
|
@@ -292,6 +548,7 @@ export function App(props) {
|
|
|
292
548
|
setModelOptions(details.models);
|
|
293
549
|
modelCache.set(`ollama:${verifiedHost.url}`, {
|
|
294
550
|
models: details.models,
|
|
551
|
+
descriptors: details.models.map((model) => ({ id: model, displayName: model })),
|
|
295
552
|
expiresAt: Date.now() + modelCacheTtlMs
|
|
296
553
|
});
|
|
297
554
|
setSettings((currentSettings) => ({
|
|
@@ -388,6 +645,14 @@ export function App(props) {
|
|
|
388
645
|
setOnboardingNotice(null);
|
|
389
646
|
setOnboardingIndex(0);
|
|
390
647
|
switch (onboarding.step) {
|
|
648
|
+
case "welcome":
|
|
649
|
+
setOnboarding(null);
|
|
650
|
+
return;
|
|
651
|
+
case "disclaimer":
|
|
652
|
+
setOnboarding({
|
|
653
|
+
step: "welcome"
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
391
656
|
case "entry":
|
|
392
657
|
setOnboarding(null);
|
|
393
658
|
return;
|
|
@@ -412,6 +677,15 @@ export function App(props) {
|
|
|
412
677
|
hosts: hostOptions
|
|
413
678
|
});
|
|
414
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;
|
|
415
689
|
case "model":
|
|
416
690
|
if (onboarding.provider === "ollama" && activeHost?.host.kind !== "local") {
|
|
417
691
|
setOnboarding({
|
|
@@ -448,7 +722,7 @@ export function App(props) {
|
|
|
448
722
|
step: "entry"
|
|
449
723
|
});
|
|
450
724
|
}
|
|
451
|
-
}, [activeHost?.host.kind, hostOptions, onboarding]);
|
|
725
|
+
}, [activeHost?.host.kind, hostOptions, onboarding, openModelSelection]);
|
|
452
726
|
const handleOnboardingSubmit = useCallback(async (value) => {
|
|
453
727
|
if (!onboarding) {
|
|
454
728
|
return;
|
|
@@ -457,6 +731,33 @@ export function App(props) {
|
|
|
457
731
|
return;
|
|
458
732
|
}
|
|
459
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
|
+
}
|
|
460
761
|
if (onboarding.step === "entry") {
|
|
461
762
|
const selection = readEntrySelection(value, onboardingIndex);
|
|
462
763
|
if (!selection) {
|
|
@@ -587,8 +888,8 @@ export function App(props) {
|
|
|
587
888
|
if (choice === null) {
|
|
588
889
|
return;
|
|
589
890
|
}
|
|
590
|
-
if (
|
|
591
|
-
if (
|
|
891
|
+
if (onboarding.provider === "gemini-wrapper") {
|
|
892
|
+
if (choice === 0 && onboarding.hasExistingKey) {
|
|
592
893
|
setOnboarding({
|
|
593
894
|
step: "gemini-wrapper-model-mode"
|
|
594
895
|
});
|
|
@@ -596,12 +897,56 @@ export function App(props) {
|
|
|
596
897
|
setOnboardingIndex(0);
|
|
597
898
|
return;
|
|
598
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
|
+
}
|
|
943
|
+
if (choice === 0 && onboarding.hasExistingKey) {
|
|
599
944
|
await openModelSelection(onboarding.provider, {
|
|
600
945
|
currentModel: defaultModelForProvider(onboarding.provider, settings.model)
|
|
601
946
|
});
|
|
602
947
|
return;
|
|
603
948
|
}
|
|
604
|
-
setOnboarding(
|
|
949
|
+
setOnboarding({
|
|
605
950
|
step: `${onboarding.provider}-key`
|
|
606
951
|
});
|
|
607
952
|
setOnboardingInput("");
|
|
@@ -681,25 +1026,30 @@ export function App(props) {
|
|
|
681
1026
|
if (choice === null) {
|
|
682
1027
|
return;
|
|
683
1028
|
}
|
|
684
|
-
|
|
1029
|
+
const curatedModel = geminiWrapperShortcutModels[choice];
|
|
1030
|
+
if (curatedModel) {
|
|
685
1031
|
setTelemetry(null);
|
|
686
|
-
|
|
1032
|
+
const shortcutDescriptors = geminiWrapperShortcutModels.map((model) => ({ id: model, displayName: model }));
|
|
1033
|
+
rememberModelDescriptors(shortcutDescriptors);
|
|
1034
|
+
setModelOptions([...geminiWrapperShortcutModels]);
|
|
687
1035
|
setSettings((currentSettings) => ({
|
|
688
1036
|
...currentSettings,
|
|
689
1037
|
provider: "gemini-wrapper",
|
|
690
|
-
model:
|
|
1038
|
+
model: curatedModel
|
|
691
1039
|
}));
|
|
692
1040
|
savePatchPilotEnvValues({
|
|
693
1041
|
PATCHPILOT_PROVIDER: "gemini-wrapper",
|
|
694
|
-
PATCHPILOT_MODEL:
|
|
695
|
-
|
|
1042
|
+
PATCHPILOT_MODEL: curatedModel,
|
|
1043
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "python"
|
|
696
1044
|
});
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1045
|
+
setOnboarding({
|
|
1046
|
+
step: "preferences",
|
|
1047
|
+
provider: "gemini-wrapper",
|
|
1048
|
+
model: curatedModel,
|
|
1049
|
+
preferences: readOnboardingPreferences()
|
|
701
1050
|
});
|
|
702
|
-
|
|
1051
|
+
setOnboardingInput("");
|
|
1052
|
+
setOnboardingIndex(preferenceRows.length);
|
|
703
1053
|
return;
|
|
704
1054
|
}
|
|
705
1055
|
await openModelSelection("gemini-wrapper", {
|
|
@@ -728,10 +1078,12 @@ export function App(props) {
|
|
|
728
1078
|
return;
|
|
729
1079
|
}
|
|
730
1080
|
process.env.PATCHPILOT_GEMINI_WRAPPER_BASE_URL = baseUrl;
|
|
1081
|
+
process.env.PATCHPILOT_GEMINI_WRAPPER_MODE = "http";
|
|
731
1082
|
savePatchPilotEnvValues({
|
|
732
1083
|
PATCHPILOT_PROVIDER: "gemini-wrapper",
|
|
733
1084
|
PATCHPILOT_MODEL: defaultGeminiWrapperModel,
|
|
734
|
-
PATCHPILOT_GEMINI_WRAPPER_BASE_URL: baseUrl
|
|
1085
|
+
PATCHPILOT_GEMINI_WRAPPER_BASE_URL: baseUrl,
|
|
1086
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "http"
|
|
735
1087
|
});
|
|
736
1088
|
setOnboardingNotice({
|
|
737
1089
|
tone: "success",
|
|
@@ -766,6 +1118,7 @@ export function App(props) {
|
|
|
766
1118
|
PATCHPILOT_PROVIDER: "gemini-wrapper",
|
|
767
1119
|
PATCHPILOT_MODEL: defaultGeminiWrapperModel,
|
|
768
1120
|
PATCHPILOT_GEMINI_WRAPPER_BASE_URL: onboarding.baseUrl,
|
|
1121
|
+
PATCHPILOT_GEMINI_WRAPPER_MODE: "http",
|
|
769
1122
|
...(apiKey ? { PATCHPILOT_GEMINI_WRAPPER_API_KEY: apiKey } : {})
|
|
770
1123
|
});
|
|
771
1124
|
setOnboardingNotice({
|
|
@@ -839,7 +1192,54 @@ export function App(props) {
|
|
|
839
1192
|
});
|
|
840
1193
|
return;
|
|
841
1194
|
}
|
|
842
|
-
|
|
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);
|
|
843
1243
|
const selectedModel = visibleModels[onboardingIndex] ?? selectModelFromInput(value, visibleModels, onboardingIndex, {
|
|
844
1244
|
allowManual: onboarding.provider !== "ollama" && onboarding.provider !== "gemini-wrapper"
|
|
845
1245
|
});
|
|
@@ -850,39 +1250,65 @@ export function App(props) {
|
|
|
850
1250
|
});
|
|
851
1251
|
return;
|
|
852
1252
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
...currentSettings,
|
|
1253
|
+
setOnboarding({
|
|
1254
|
+
step: "preferences",
|
|
856
1255
|
provider: onboarding.provider,
|
|
857
|
-
model: selectedModel
|
|
858
|
-
|
|
859
|
-
savePatchPilotEnvValues({
|
|
860
|
-
PATCHPILOT_PROVIDER: onboarding.provider,
|
|
861
|
-
PATCHPILOT_MODEL: selectedModel,
|
|
862
|
-
PATCHPILOT_ONBOARDING_COMPLETE: "1",
|
|
863
|
-
...(onboarding.provider === "ollama" ? { PATCHPILOT_OLLAMA_URL: activeHost?.host.url ?? settings.ollamaUrl } : {})
|
|
864
|
-
});
|
|
865
|
-
appendLine({
|
|
866
|
-
tone: "success",
|
|
867
|
-
label: "onboarding",
|
|
868
|
-
text: `ready: ${onboarding.provider} using ${selectedModel}`
|
|
1256
|
+
model: selectedModel,
|
|
1257
|
+
preferences: readOnboardingPreferences()
|
|
869
1258
|
});
|
|
870
|
-
|
|
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) {
|
|
871
1290
|
appendLine({
|
|
872
1291
|
tone: "warning",
|
|
873
|
-
label: "
|
|
874
|
-
text:
|
|
875
|
-
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.`
|
|
876
1294
|
});
|
|
1295
|
+
return;
|
|
877
1296
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
+
});
|
|
882
1303
|
return;
|
|
883
1304
|
}
|
|
1305
|
+
const runStartedAt = Date.now();
|
|
1306
|
+
softStopRequestedRef.current = false;
|
|
1307
|
+
lastEscapeStopAtRef.current = 0;
|
|
884
1308
|
setInput("");
|
|
885
1309
|
setTranscriptScrollOffset(0);
|
|
1310
|
+
setTodos([]);
|
|
1311
|
+
setUltramaxxRun(ultramaxx);
|
|
886
1312
|
setIsRunning(true);
|
|
887
1313
|
appendLine({
|
|
888
1314
|
kind: "user",
|
|
@@ -890,22 +1316,98 @@ export function App(props) {
|
|
|
890
1316
|
label: "you",
|
|
891
1317
|
text: task
|
|
892
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 = [];
|
|
893
1345
|
try {
|
|
894
|
-
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
|
+
});
|
|
895
1353
|
if (!runnableSettings) {
|
|
896
1354
|
return;
|
|
897
1355
|
}
|
|
898
1356
|
const abortController = new AbortController();
|
|
899
1357
|
abortControllerRef.current = abortController;
|
|
900
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");
|
|
901
1390
|
const taskRunner = new AgentRunner({
|
|
902
1391
|
...runnableSettings,
|
|
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,
|
|
903
1403
|
allowExternalFileAnalysis: experimentalFlags.fileAnalysis,
|
|
1404
|
+
allowShellMetacharacters: experimentalFlags.shellMetacharacters,
|
|
904
1405
|
memoryEnabled: experimentalFlags.memory,
|
|
905
1406
|
mode: effectiveMode,
|
|
906
1407
|
signal: abortController.signal,
|
|
1408
|
+
shouldStopAfterStep: () => softStopRequestedRef.current,
|
|
907
1409
|
sessionStore: sessionStoreRef.current,
|
|
908
|
-
resumeContext,
|
|
1410
|
+
resumeContext: effectiveResumeContext,
|
|
909
1411
|
approvalHandler: (request) => new Promise((resolve) => {
|
|
910
1412
|
if (effectiveMode === "plan") {
|
|
911
1413
|
appendLine({
|
|
@@ -921,7 +1423,13 @@ export function App(props) {
|
|
|
921
1423
|
resolve("deny");
|
|
922
1424
|
return;
|
|
923
1425
|
}
|
|
924
|
-
if (
|
|
1426
|
+
if (request.bypassable !== false &&
|
|
1427
|
+
shouldBypassApproval({
|
|
1428
|
+
mode: effectiveMode,
|
|
1429
|
+
permission: request.permission,
|
|
1430
|
+
permissions: runnableSettings,
|
|
1431
|
+
allowExternalFileAnalysis: experimentalFlags.fileAnalysis
|
|
1432
|
+
})) {
|
|
925
1433
|
resolve("allow_session");
|
|
926
1434
|
return;
|
|
927
1435
|
}
|
|
@@ -942,7 +1450,15 @@ export function App(props) {
|
|
|
942
1450
|
approvalResolverRef.current = resolve;
|
|
943
1451
|
})
|
|
944
1452
|
});
|
|
945
|
-
|
|
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)) {
|
|
946
1462
|
setWorkState(event.workState);
|
|
947
1463
|
if (event.type === "metrics") {
|
|
948
1464
|
if (runnableSettings.provider === "ollama") {
|
|
@@ -955,31 +1471,154 @@ export function App(props) {
|
|
|
955
1471
|
if (event.type === "subagent") {
|
|
956
1472
|
setTelemetry(event.metrics);
|
|
957
1473
|
setSessionTelemetry((currentSession) => addTelemetryToSession(currentSession, event.metrics));
|
|
1474
|
+
setToolTelemetry((currentTools) => addToolTelemetry(currentTools, "subagent", true));
|
|
958
1475
|
setAdvisorNotes((currentNotes) => upsertAdvisorNote(currentNotes, {
|
|
959
1476
|
role: event.role,
|
|
960
1477
|
message: event.message
|
|
961
1478
|
}));
|
|
962
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
|
+
}
|
|
963
1510
|
setStatus(eventToStatus(event));
|
|
964
1511
|
appendLine(eventToLine(event));
|
|
965
1512
|
}
|
|
966
1513
|
}
|
|
967
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);
|
|
968
1526
|
appendLine({
|
|
969
1527
|
kind: "error",
|
|
970
1528
|
tone: "danger",
|
|
971
1529
|
label: "error",
|
|
972
|
-
text:
|
|
1530
|
+
text: message,
|
|
973
1531
|
workState: "error"
|
|
974
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
|
+
}
|
|
975
1540
|
}
|
|
976
1541
|
finally {
|
|
977
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);
|
|
978
1579
|
setStatus("idle");
|
|
979
1580
|
setWorkState("idle");
|
|
980
|
-
|
|
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);
|
|
981
1608
|
}
|
|
982
|
-
|
|
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
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}, [appendLine, reauthBusy, reauthPrompt, runTask]);
|
|
983
1622
|
const handleSlashCommand = useCallback(async (rawCommand) => {
|
|
984
1623
|
const [commandName = "", ...args] = rawCommand.slice(1).trim().split(/\s+/);
|
|
985
1624
|
const command = commandName.toLowerCase();
|
|
@@ -1022,7 +1661,7 @@ export function App(props) {
|
|
|
1022
1661
|
appendLine({
|
|
1023
1662
|
tone: "accent",
|
|
1024
1663
|
label: "permissions",
|
|
1025
|
-
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"}`,
|
|
1026
1665
|
detail: modeDescription(agentMode)
|
|
1027
1666
|
});
|
|
1028
1667
|
return;
|
|
@@ -1062,7 +1701,7 @@ export function App(props) {
|
|
|
1062
1701
|
}
|
|
1063
1702
|
case "onboarding":
|
|
1064
1703
|
setOnboarding({
|
|
1065
|
-
step: "
|
|
1704
|
+
step: "welcome"
|
|
1066
1705
|
});
|
|
1067
1706
|
setOnboardingIndex(0);
|
|
1068
1707
|
setOnboardingInput("");
|
|
@@ -1135,6 +1774,15 @@ export function App(props) {
|
|
|
1135
1774
|
case "write":
|
|
1136
1775
|
case "apply": {
|
|
1137
1776
|
const writeEnabled = readToggle(args[0], !settings.allowWrite);
|
|
1777
|
+
if (writeEnabled) {
|
|
1778
|
+
requestBypassMode();
|
|
1779
|
+
appendLine({
|
|
1780
|
+
tone: "warning",
|
|
1781
|
+
label: "write",
|
|
1782
|
+
text: "write bypass needs trusted-workspace confirmation"
|
|
1783
|
+
});
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1138
1786
|
setExplicitPermission("write", writeEnabled);
|
|
1139
1787
|
appendLine({
|
|
1140
1788
|
tone: "success",
|
|
@@ -1145,6 +1793,15 @@ export function App(props) {
|
|
|
1145
1793
|
}
|
|
1146
1794
|
case "shell": {
|
|
1147
1795
|
const shellEnabled = readToggle(args[0], !settings.allowShell);
|
|
1796
|
+
if (shellEnabled) {
|
|
1797
|
+
requestBypassMode();
|
|
1798
|
+
appendLine({
|
|
1799
|
+
tone: "warning",
|
|
1800
|
+
label: "shell",
|
|
1801
|
+
text: "shell bypass needs trusted-workspace confirmation"
|
|
1802
|
+
});
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1148
1805
|
setExplicitPermission("shell", shellEnabled);
|
|
1149
1806
|
appendLine({
|
|
1150
1807
|
tone: "success",
|
|
@@ -1178,14 +1835,14 @@ export function App(props) {
|
|
|
1178
1835
|
return;
|
|
1179
1836
|
}
|
|
1180
1837
|
const nextModel = selectModelFromInput(requestedModel, models, undefined, {
|
|
1181
|
-
allowManual: settings.provider !== "ollama"
|
|
1838
|
+
allowManual: settings.provider !== "ollama"
|
|
1182
1839
|
});
|
|
1183
1840
|
if (!nextModel) {
|
|
1184
1841
|
appendLine({
|
|
1185
1842
|
tone: "warning",
|
|
1186
1843
|
label: "model",
|
|
1187
1844
|
text: `No unique model match for "${requestedModel}".`,
|
|
1188
|
-
detail: formatModelOptions(selectableModels(requestedModel, models).slice(0, 12), settings.model)
|
|
1845
|
+
detail: formatModelOptions(selectableModels(requestedModel, models, formatModelLabel).slice(0, 12), settings.model)
|
|
1189
1846
|
});
|
|
1190
1847
|
return;
|
|
1191
1848
|
}
|
|
@@ -1203,7 +1860,7 @@ export function App(props) {
|
|
|
1203
1860
|
return;
|
|
1204
1861
|
}
|
|
1205
1862
|
const nextModel = selectModelFromInput(requestedModel, installedModels, undefined, {
|
|
1206
|
-
allowManual: settings.provider !== "ollama"
|
|
1863
|
+
allowManual: settings.provider !== "ollama"
|
|
1207
1864
|
});
|
|
1208
1865
|
if (!nextModel) {
|
|
1209
1866
|
appendLine({
|
|
@@ -1232,7 +1889,13 @@ export function App(props) {
|
|
|
1232
1889
|
? "Pull a model on the selected host first."
|
|
1233
1890
|
: settings.provider === "gemini"
|
|
1234
1891
|
? "Check GEMINI_API_KEY in PatchPilot config."
|
|
1235
|
-
:
|
|
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."
|
|
1236
1899
|
});
|
|
1237
1900
|
return;
|
|
1238
1901
|
}
|
|
@@ -1256,13 +1919,59 @@ export function App(props) {
|
|
|
1256
1919
|
}
|
|
1257
1920
|
case "status":
|
|
1258
1921
|
appendLine({
|
|
1922
|
+
kind: "status",
|
|
1259
1923
|
tone: "accent",
|
|
1260
1924
|
label: "status",
|
|
1261
|
-
text: settings.provider
|
|
1262
|
-
|
|
1263
|
-
|
|
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
|
+
})
|
|
1264
1962
|
});
|
|
1265
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;
|
|
1266
1975
|
case "sessions": {
|
|
1267
1976
|
const sessions = await listWorkspaceSessions(settings.workspace);
|
|
1268
1977
|
appendLine({
|
|
@@ -1286,6 +1995,11 @@ export function App(props) {
|
|
|
1286
1995
|
workspace: settings.workspace,
|
|
1287
1996
|
sessionId: selectedSession.sessionId
|
|
1288
1997
|
});
|
|
1998
|
+
contextStoreRef.current = new ContextStore({
|
|
1999
|
+
workspace: settings.workspace,
|
|
2000
|
+
sessionId: selectedSession.sessionId
|
|
2001
|
+
});
|
|
2002
|
+
await contextStoreRef.current.bootstrapFromSession(await sessionStoreRef.current.loadEvents());
|
|
1289
2003
|
await sessionStoreRef.current.append({
|
|
1290
2004
|
type: "session.resumed",
|
|
1291
2005
|
sessionId: selectedSession.sessionId,
|
|
@@ -1465,6 +2179,7 @@ export function App(props) {
|
|
|
1465
2179
|
setAdvisorNotes([]);
|
|
1466
2180
|
setTelemetry(null);
|
|
1467
2181
|
setSessionTelemetry(emptySessionTelemetry());
|
|
2182
|
+
setToolTelemetry(emptyToolTelemetry());
|
|
1468
2183
|
}
|
|
1469
2184
|
appendLine({
|
|
1470
2185
|
tone: "success",
|
|
@@ -1482,33 +2197,60 @@ export function App(props) {
|
|
|
1482
2197
|
setInput("");
|
|
1483
2198
|
return;
|
|
1484
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."
|
|
2207
|
+
});
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
1485
2210
|
const enabled = readToggle(requestedValue, true);
|
|
1486
|
-
if (
|
|
2211
|
+
if (normalizedFlag === "subagents") {
|
|
1487
2212
|
setSettings((currentSettings) => ({
|
|
1488
2213
|
...currentSettings,
|
|
1489
2214
|
subagents: enabled
|
|
1490
2215
|
}));
|
|
1491
2216
|
}
|
|
1492
2217
|
savePatchPilotEnvValues({
|
|
1493
|
-
[
|
|
2218
|
+
[experimentalFlagEnvName(normalizedFlag)]: enabled ? "1" : "0"
|
|
1494
2219
|
});
|
|
1495
2220
|
setExperimentalFlags((currentFlags) => ({
|
|
1496
2221
|
...currentFlags,
|
|
1497
|
-
...(
|
|
2222
|
+
...(normalizedFlag === "fileAnalysis"
|
|
1498
2223
|
? { fileAnalysis: enabled }
|
|
1499
|
-
:
|
|
2224
|
+
: normalizedFlag === "memory"
|
|
1500
2225
|
? { memory: enabled }
|
|
1501
|
-
:
|
|
2226
|
+
: normalizedFlag === "subagents"
|
|
1502
2227
|
? { subagents: enabled }
|
|
1503
|
-
: {})
|
|
2228
|
+
: { shellMetacharacters: enabled })
|
|
1504
2229
|
}));
|
|
1505
2230
|
appendLine({
|
|
1506
2231
|
tone: "success",
|
|
1507
2232
|
label: "experimental",
|
|
1508
|
-
text: `${
|
|
2233
|
+
text: `${experimentalFlagCommandName(normalizedFlag)} ${enabled ? "enabled" : "disabled"}`
|
|
1509
2234
|
});
|
|
1510
2235
|
return;
|
|
1511
2236
|
}
|
|
2237
|
+
case "theme": {
|
|
2238
|
+
const requested = args[0]?.toLowerCase();
|
|
2239
|
+
if (requested === "new" || requested === "legacy") {
|
|
2240
|
+
setUiTheme(requested);
|
|
2241
|
+
savePatchPilotEnvValues({ PATCHPILOT_UI_THEME: requested });
|
|
2242
|
+
appendLine({
|
|
2243
|
+
tone: "success",
|
|
2244
|
+
label: "theme",
|
|
2245
|
+
text: `switched to the ${requested} UI`
|
|
2246
|
+
});
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
setThemePickerOpen(true);
|
|
2250
|
+
setThemePickerIndex(themeOptions.findIndex((option) => option.value === uiTheme));
|
|
2251
|
+
setInput("");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
1512
2254
|
case "init": {
|
|
1513
2255
|
await ensurePatchPilotGitignore(settings.workspace);
|
|
1514
2256
|
appendLine({
|
|
@@ -1525,11 +2267,17 @@ export function App(props) {
|
|
|
1525
2267
|
case "clear":
|
|
1526
2268
|
setLines([]);
|
|
1527
2269
|
setAdvisorNotes([]);
|
|
2270
|
+
setTodos([]);
|
|
1528
2271
|
setTelemetry(null);
|
|
1529
2272
|
setResumeContext("");
|
|
1530
2273
|
setSessionTelemetry(emptySessionTelemetry());
|
|
2274
|
+
setToolTelemetry(emptyToolTelemetry());
|
|
1531
2275
|
setTranscriptScrollOffset(0);
|
|
1532
2276
|
setSessionScrollOffset(0);
|
|
2277
|
+
conversationTurnsRef.current = [];
|
|
2278
|
+
artifactsRef.current = [];
|
|
2279
|
+
pendingAttachmentsRef.current = [];
|
|
2280
|
+
setArtifacts([]);
|
|
1533
2281
|
return;
|
|
1534
2282
|
case "new":
|
|
1535
2283
|
if (isRunning) {
|
|
@@ -1544,11 +2292,17 @@ export function App(props) {
|
|
|
1544
2292
|
sessionStoreRef.current = new SessionStore({
|
|
1545
2293
|
workspace: settings.workspace
|
|
1546
2294
|
});
|
|
2295
|
+
contextStoreRef.current = new ContextStore({
|
|
2296
|
+
workspace: settings.workspace,
|
|
2297
|
+
sessionId: sessionStoreRef.current.sessionId
|
|
2298
|
+
});
|
|
1547
2299
|
await sessionStoreRef.current.create();
|
|
1548
2300
|
setLines([]);
|
|
1549
2301
|
setAdvisorNotes([]);
|
|
2302
|
+
setTodos([]);
|
|
1550
2303
|
setTelemetry(null);
|
|
1551
2304
|
setSessionTelemetry(emptySessionTelemetry());
|
|
2305
|
+
setToolTelemetry(emptyToolTelemetry());
|
|
1552
2306
|
setPendingApproval(null);
|
|
1553
2307
|
approvalResolverRef.current = null;
|
|
1554
2308
|
setBypassConfirmation(false);
|
|
@@ -1557,11 +2311,12 @@ export function App(props) {
|
|
|
1557
2311
|
setSessionScrollOffset(0);
|
|
1558
2312
|
setStatus("idle");
|
|
1559
2313
|
setWorkState("idle");
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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.
|
|
1565
2320
|
return;
|
|
1566
2321
|
case "exit":
|
|
1567
2322
|
case "quit":
|
|
@@ -1631,10 +2386,39 @@ export function App(props) {
|
|
|
1631
2386
|
useEffect(() => {
|
|
1632
2387
|
void sessionStoreRef.current.create();
|
|
1633
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]);
|
|
1634
2418
|
useEffect(() => {
|
|
1635
2419
|
runtimeStateRef.current.isRunning = isRunning;
|
|
1636
|
-
runtimeStateRef.current.hasPendingApproval = Boolean(pendingApproval || bypassConfirmation);
|
|
1637
|
-
}, [bypassConfirmation, isRunning, pendingApproval]);
|
|
2420
|
+
runtimeStateRef.current.hasPendingApproval = Boolean(pendingApproval || bypassConfirmation || updatePrompt || updateBusy);
|
|
2421
|
+
}, [bypassConfirmation, isRunning, pendingApproval, updateBusy, updatePrompt]);
|
|
1638
2422
|
useEffect(() => {
|
|
1639
2423
|
if (!props.initialTask || didRunInitialTask.current || onboarding || process.env.PATCHPILOT_ONBOARDING_COMPLETE !== "1") {
|
|
1640
2424
|
return;
|
|
@@ -1651,7 +2435,7 @@ export function App(props) {
|
|
|
1651
2435
|
}
|
|
1652
2436
|
didOpenDefaultOnboarding.current = true;
|
|
1653
2437
|
setOnboarding({
|
|
1654
|
-
step: "
|
|
2438
|
+
step: "welcome"
|
|
1655
2439
|
});
|
|
1656
2440
|
setOnboardingIndex(0);
|
|
1657
2441
|
setOnboardingInput("");
|
|
@@ -1723,6 +2507,35 @@ export function App(props) {
|
|
|
1723
2507
|
}
|
|
1724
2508
|
}, [hostOptions.length, input, isLoadingHosts, isLoadingModels, isRunning, loadHostSuggestions, loadProviderModels, modelOptions.length, onboarding, settings.provider]);
|
|
1725
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
|
+
}
|
|
1726
2539
|
if (experimentalOpen) {
|
|
1727
2540
|
if (key.upArrow) {
|
|
1728
2541
|
setExperimentalIndex((currentIndex) => (currentIndex - 1 + experimentalFlagCount()) % experimentalFlagCount());
|
|
@@ -1748,7 +2561,8 @@ export function App(props) {
|
|
|
1748
2561
|
savePatchPilotEnvValues({
|
|
1749
2562
|
PATCHPILOT_EXPERIMENTAL_FILE_ANALYSIS: nextFlags.fileAnalysis ? "1" : "0",
|
|
1750
2563
|
PATCHPILOT_EXPERIMENTAL_MEMORY: nextFlags.memory ? "1" : "0",
|
|
1751
|
-
PATCHPILOT_EXPERIMENTAL_SUBAGENTS: nextFlags.subagents ? "1" : "0"
|
|
2564
|
+
PATCHPILOT_EXPERIMENTAL_SUBAGENTS: nextFlags.subagents ? "1" : "0",
|
|
2565
|
+
PATCHPILOT_EXPERIMENTAL_SHELL_METACHARACTERS: nextFlags.shellMetacharacters ? "1" : "0"
|
|
1752
2566
|
});
|
|
1753
2567
|
return nextFlags;
|
|
1754
2568
|
});
|
|
@@ -1763,8 +2577,11 @@ export function App(props) {
|
|
|
1763
2577
|
}
|
|
1764
2578
|
if (bypassConfirmation) {
|
|
1765
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.
|
|
1766
2582
|
if (key.tab) {
|
|
1767
|
-
|
|
2583
|
+
setInput("");
|
|
2584
|
+
applyMode("plan");
|
|
1768
2585
|
return;
|
|
1769
2586
|
}
|
|
1770
2587
|
if (normalizedInput === "y") {
|
|
@@ -1776,6 +2593,36 @@ export function App(props) {
|
|
|
1776
2593
|
return;
|
|
1777
2594
|
}
|
|
1778
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
|
+
}
|
|
1779
2626
|
if (pendingApproval) {
|
|
1780
2627
|
const normalizedInput = inputValue.toLowerCase();
|
|
1781
2628
|
if (normalizedInput === "y") {
|
|
@@ -1792,25 +2639,77 @@ export function App(props) {
|
|
|
1792
2639
|
}
|
|
1793
2640
|
}
|
|
1794
2641
|
if (isRunning && key.escape) {
|
|
1795
|
-
|
|
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;
|
|
1796
2657
|
appendLine({
|
|
1797
2658
|
kind: "status",
|
|
1798
2659
|
tone: "warning",
|
|
1799
2660
|
label: "stop",
|
|
1800
|
-
text: "
|
|
2661
|
+
text: "Will stop after the current step. Press esc again quickly to force stop now."
|
|
1801
2662
|
});
|
|
1802
|
-
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();
|
|
1803
2671
|
return;
|
|
1804
2672
|
}
|
|
1805
2673
|
if (onboarding) {
|
|
1806
|
-
if (key.escape
|
|
2674
|
+
if (key.escape) {
|
|
1807
2675
|
goBackOnboarding();
|
|
1808
2676
|
return;
|
|
1809
2677
|
}
|
|
1810
2678
|
if (onboardingBusyMessage) {
|
|
1811
2679
|
return;
|
|
1812
2680
|
}
|
|
1813
|
-
|
|
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);
|
|
1814
2713
|
if (optionCount > 0 && key.upArrow) {
|
|
1815
2714
|
setOnboardingIndex((currentIndex) => (currentIndex - 1 + optionCount) % optionCount);
|
|
1816
2715
|
return;
|
|
@@ -1882,9 +2781,6 @@ export function App(props) {
|
|
|
1882
2781
|
toggleMode();
|
|
1883
2782
|
return;
|
|
1884
2783
|
}
|
|
1885
|
-
if (!isRunning && input.length === 0 && inputValue === "q") {
|
|
1886
|
-
void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(exit);
|
|
1887
|
-
}
|
|
1888
2784
|
});
|
|
1889
2785
|
useEffect(() => {
|
|
1890
2786
|
const gracefulStopOrExit = () => {
|
|
@@ -1953,21 +2849,37 @@ export function App(props) {
|
|
|
1953
2849
|
clearInterval(timer);
|
|
1954
2850
|
};
|
|
1955
2851
|
}, []);
|
|
1956
|
-
|
|
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 })] })] }))] }));
|
|
1957
2862
|
}
|
|
1958
2863
|
async function loadAvailableModels(provider, ollamaUrl, setModelOptions, refresh = false) {
|
|
1959
2864
|
const cacheKey = modelCacheKey(provider, ollamaUrl);
|
|
1960
2865
|
const cachedModels = modelCache.get(cacheKey);
|
|
1961
2866
|
if (!refresh && cachedModels && cachedModels.expiresAt > Date.now()) {
|
|
2867
|
+
rememberModelDescriptors(cachedModels.descriptors);
|
|
1962
2868
|
setModelOptions(cachedModels.models);
|
|
1963
2869
|
return cachedModels.models;
|
|
1964
2870
|
}
|
|
1965
|
-
const
|
|
2871
|
+
const client = createModelClient({
|
|
1966
2872
|
provider,
|
|
1967
2873
|
ollamaUrl
|
|
1968
|
-
})
|
|
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);
|
|
1969
2880
|
modelCache.set(cacheKey, {
|
|
1970
2881
|
models,
|
|
2882
|
+
descriptors,
|
|
1971
2883
|
expiresAt: Date.now() + modelCacheTtlMs
|
|
1972
2884
|
});
|
|
1973
2885
|
setModelOptions(models);
|
|
@@ -1988,6 +2900,17 @@ function modelCacheKey(provider, ollamaUrl) {
|
|
|
1988
2900
|
}
|
|
1989
2901
|
return `${provider}:default`;
|
|
1990
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
|
+
}
|
|
1991
2914
|
async function loadKnownOrAvailableModels(provider, ollamaUrl, modelOptions, setModelOptions, appendLine, options = {}) {
|
|
1992
2915
|
try {
|
|
1993
2916
|
return !options.refresh && modelOptions.length > 0 ? modelOptions : await loadAvailableModels(provider, ollamaUrl, setModelOptions, options.refresh);
|
|
@@ -2045,7 +2968,7 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
|
|
|
2045
2968
|
appendLine({
|
|
2046
2969
|
tone: installedModels.includes(nextModel) ? "success" : "warning",
|
|
2047
2970
|
label: "model",
|
|
2048
|
-
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}`,
|
|
2049
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."
|
|
2050
2973
|
});
|
|
2051
2974
|
if (provider === "openrouter" && isOpenRouterFreeModel(nextModel)) {
|
|
@@ -2057,7 +2980,7 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
|
|
|
2057
2980
|
});
|
|
2058
2981
|
}
|
|
2059
2982
|
}
|
|
2060
|
-
async function resolveRunnableSettings(settings, modelOptions, appendLine, setModelOptions) {
|
|
2983
|
+
async function resolveRunnableSettings(settings, modelOptions, appendLine, setModelOptions, onProviderError) {
|
|
2061
2984
|
let installedModels;
|
|
2062
2985
|
try {
|
|
2063
2986
|
installedModels = modelOptions.includes(settings.model)
|
|
@@ -2065,11 +2988,13 @@ async function resolveRunnableSettings(settings, modelOptions, appendLine, setMo
|
|
|
2065
2988
|
: await loadAvailableModels(settings.provider, settings.ollamaUrl, setModelOptions);
|
|
2066
2989
|
}
|
|
2067
2990
|
catch (error) {
|
|
2991
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2068
2992
|
appendLine({
|
|
2069
2993
|
tone: "danger",
|
|
2070
2994
|
label: settings.provider,
|
|
2071
|
-
text:
|
|
2995
|
+
text: message
|
|
2072
2996
|
});
|
|
2997
|
+
onProviderError?.(message);
|
|
2073
2998
|
return null;
|
|
2074
2999
|
}
|
|
2075
3000
|
if (installedModels.includes(settings.model) || canUseUnverifiedCloudModel(settings.provider, settings.model)) {
|
|
@@ -2154,11 +3079,11 @@ function buildCommandSuggestionItems(options) {
|
|
|
2154
3079
|
});
|
|
2155
3080
|
}
|
|
2156
3081
|
else {
|
|
2157
|
-
items.unshift(...selectableModels(modelQuery, options.modelOptions).slice(0, 8).map((model) => ({
|
|
3082
|
+
items.unshift(...selectableModels(modelQuery, options.modelOptions, formatModelLabel).slice(0, 8).map((model) => ({
|
|
2158
3083
|
key: `model-${model}`,
|
|
2159
3084
|
category: "model",
|
|
2160
|
-
label: model,
|
|
2161
|
-
detail: `${model === options.currentModel ? "current" : "available"} ${options.provider}`,
|
|
3085
|
+
label: formatModelLabel(model),
|
|
3086
|
+
detail: `${model === options.currentModel ? "current" : "available"} ${options.provider}${formatModelDescription(model)}`,
|
|
2162
3087
|
command: `/model ${model}`,
|
|
2163
3088
|
execute: true
|
|
2164
3089
|
})));
|
|
@@ -2168,16 +3093,25 @@ function buildCommandSuggestionItems(options) {
|
|
|
2168
3093
|
}
|
|
2169
3094
|
function getOnboardingOptionCount(onboarding) {
|
|
2170
3095
|
switch (onboarding.step) {
|
|
3096
|
+
case "welcome":
|
|
3097
|
+
return 1;
|
|
3098
|
+
case "disclaimer":
|
|
3099
|
+
return 0;
|
|
2171
3100
|
case "entry":
|
|
2172
3101
|
return 7;
|
|
2173
3102
|
case "host":
|
|
2174
3103
|
return onboarding.hosts.length + 1;
|
|
2175
3104
|
case "api-key-choice":
|
|
3105
|
+
if (onboarding.provider === "gemini-wrapper") {
|
|
3106
|
+
return onboarding.hasExistingKey ? 3 : 2;
|
|
3107
|
+
}
|
|
2176
3108
|
return onboarding.hasExistingKey ? 2 : 1;
|
|
2177
3109
|
case "gemini-wrapper-model-mode":
|
|
2178
|
-
return
|
|
3110
|
+
return geminiWrapperShortcutModels.length + 1;
|
|
2179
3111
|
case "model":
|
|
2180
3112
|
return onboarding.models.length;
|
|
3113
|
+
case "preferences":
|
|
3114
|
+
return preferenceRows.length + 1;
|
|
2181
3115
|
default:
|
|
2182
3116
|
return 0;
|
|
2183
3117
|
}
|
|
@@ -2223,6 +3157,37 @@ function readBooleanEnv(value, fallback) {
|
|
|
2223
3157
|
}
|
|
2224
3158
|
return fallback;
|
|
2225
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
|
+
}
|
|
2226
3191
|
function readIndexedSelection(value, selectedIndex) {
|
|
2227
3192
|
const normalizedValue = value.trim();
|
|
2228
3193
|
if (!normalizedValue) {
|
|
@@ -2246,7 +3211,11 @@ function selectModelFromInput(value, models, selectedIndex, options = {}) {
|
|
|
2246
3211
|
if (models.includes(normalizedValue)) {
|
|
2247
3212
|
return normalizedValue;
|
|
2248
3213
|
}
|
|
2249
|
-
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);
|
|
2250
3219
|
if (matches.length === 1) {
|
|
2251
3220
|
return matches[0] ?? null;
|
|
2252
3221
|
}
|
|
@@ -2256,7 +3225,7 @@ function isPlausibleCloudModelId(value) {
|
|
|
2256
3225
|
return /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(value) && value.length >= 3;
|
|
2257
3226
|
}
|
|
2258
3227
|
function canUseUnverifiedCloudModel(provider, model) {
|
|
2259
|
-
return provider !== "ollama" &&
|
|
3228
|
+
return provider !== "ollama" && isPlausibleCloudModelId(model);
|
|
2260
3229
|
}
|
|
2261
3230
|
function defaultModelForProvider(provider, currentModel) {
|
|
2262
3231
|
if (provider === "nvidia") {
|
|
@@ -2266,7 +3235,7 @@ function defaultModelForProvider(provider, currentModel) {
|
|
|
2266
3235
|
return currentModel.includes("/") ? currentModel : defaultOpenRouterModel;
|
|
2267
3236
|
}
|
|
2268
3237
|
if (provider === "gemini-wrapper") {
|
|
2269
|
-
return currentModel
|
|
3238
|
+
return geminiWrapperCuratedModels.includes(currentModel) || currentModel.startsWith("gemini-") || modelDescriptorIndex.has(currentModel) ? currentModel : defaultGeminiWrapperModel;
|
|
2270
3239
|
}
|
|
2271
3240
|
if (provider === "gemini") {
|
|
2272
3241
|
return currentModel.startsWith("gemini-") ? currentModel : defaultGeminiModel;
|
|
@@ -2292,7 +3261,11 @@ function hasApiKey(provider) {
|
|
|
2292
3261
|
return Boolean(readGeminiApiKey());
|
|
2293
3262
|
}
|
|
2294
3263
|
if (provider === "gemini-wrapper") {
|
|
2295
|
-
|
|
3264
|
+
const baseUrl = readGeminiWrapperBaseUrl();
|
|
3265
|
+
if (readGeminiWrapperMode() === "http") {
|
|
3266
|
+
return !geminiWrapperRequiresApiKey(baseUrl) || Boolean(readGeminiWrapperApiKey());
|
|
3267
|
+
}
|
|
3268
|
+
return Boolean(readGeminiWrapperCookiesJson());
|
|
2296
3269
|
}
|
|
2297
3270
|
if (provider === "openrouter") {
|
|
2298
3271
|
return Boolean(readOpenRouterApiKey());
|
|
@@ -2341,6 +3314,219 @@ function upsertAdvisorNote(notes, nextNote) {
|
|
|
2341
3314
|
const nextNotes = notes.filter((note) => note.role !== nextNote.role);
|
|
2342
3315
|
return [...nextNotes, nextNote].slice(-2);
|
|
2343
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
|
+
}
|
|
2344
3530
|
function eventToLine(event) {
|
|
2345
3531
|
switch (event.type) {
|
|
2346
3532
|
case "status":
|
|
@@ -2374,12 +3560,21 @@ function eventToLine(event) {
|
|
|
2374
3560
|
tone: event.ok ? "success" : "warning",
|
|
2375
3561
|
label: event.name,
|
|
2376
3562
|
text: event.summary,
|
|
3563
|
+
detail: event.ok ? previewToolContent(event.content) : event.content,
|
|
2377
3564
|
workState: event.workState,
|
|
2378
3565
|
tool: event.name,
|
|
2379
3566
|
toolCallId: event.toolCallId,
|
|
2380
3567
|
category: event.category,
|
|
2381
3568
|
preview: event.preview
|
|
2382
3569
|
};
|
|
3570
|
+
case "todo":
|
|
3571
|
+
return {
|
|
3572
|
+
kind: "status",
|
|
3573
|
+
tone: "muted",
|
|
3574
|
+
label: "todo",
|
|
3575
|
+
text: event.summary,
|
|
3576
|
+
workState: event.workState
|
|
3577
|
+
};
|
|
2383
3578
|
case "approval":
|
|
2384
3579
|
return {
|
|
2385
3580
|
kind: "approval",
|
|
@@ -2417,6 +3612,16 @@ function eventToLine(event) {
|
|
|
2417
3612
|
};
|
|
2418
3613
|
}
|
|
2419
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
|
+
}
|
|
2420
3625
|
function eventToStatus(event) {
|
|
2421
3626
|
if (event.type === "status") {
|
|
2422
3627
|
return event.message;
|
|
@@ -2424,6 +3629,9 @@ function eventToStatus(event) {
|
|
|
2424
3629
|
if (event.type === "tool") {
|
|
2425
3630
|
return `${event.name}: ${event.summary}`;
|
|
2426
3631
|
}
|
|
3632
|
+
if (event.type === "todo") {
|
|
3633
|
+
return event.summary;
|
|
3634
|
+
}
|
|
2427
3635
|
if (event.type === "subagent") {
|
|
2428
3636
|
return `${event.role} subagent`;
|
|
2429
3637
|
}
|
|
@@ -2432,6 +3640,19 @@ function eventToStatus(event) {
|
|
|
2432
3640
|
}
|
|
2433
3641
|
return event.type;
|
|
2434
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
|
+
}
|
|
2435
3656
|
function defaultLogKind(line) {
|
|
2436
3657
|
if (line.kind) {
|
|
2437
3658
|
return line.kind;
|
|
@@ -2459,8 +3680,21 @@ function formatModelOptions(models, currentModel) {
|
|
|
2459
3680
|
return models
|
|
2460
3681
|
.map((model, index) => {
|
|
2461
3682
|
const currentMarker = model === currentModel ? " current" : "";
|
|
2462
|
-
return `${index + 1}. ${model}${currentMarker}`;
|
|
3683
|
+
return `${index + 1}. ${formatModelLabel(model)}${formatModelDescription(model)}${currentMarker}`;
|
|
2463
3684
|
})
|
|
2464
3685
|
.join("\n");
|
|
2465
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
|
+
}
|
|
2466
3700
|
//# sourceMappingURL=App.js.map
|