@jx-grxf/patchpilot 0.4.0 → 1.0.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.
Files changed (72) hide show
  1. package/.env.example +17 -1
  2. package/README.md +69 -14
  3. package/SECURITY.md +7 -1
  4. package/dist/cli.js +59 -13
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/agent.d.ts +3 -0
  7. package/dist/core/agent.js +56 -12
  8. package/dist/core/agent.js.map +1 -1
  9. package/dist/core/cleanup.d.ts +3 -0
  10. package/dist/core/cleanup.js +29 -0
  11. package/dist/core/cleanup.js.map +1 -0
  12. package/dist/core/doctor.d.ts +4 -1
  13. package/dist/core/doctor.js +119 -1
  14. package/dist/core/doctor.js.map +1 -1
  15. package/dist/core/geminiWrapper.d.ts +51 -0
  16. package/dist/core/geminiWrapper.js +718 -0
  17. package/dist/core/geminiWrapper.js.map +1 -0
  18. package/dist/core/json.js +65 -1
  19. package/dist/core/json.js.map +1 -1
  20. package/dist/core/memory.d.ts +16 -0
  21. package/dist/core/memory.js +108 -0
  22. package/dist/core/memory.js.map +1 -0
  23. package/dist/core/modelClient.js +7 -0
  24. package/dist/core/modelClient.js.map +1 -1
  25. package/dist/core/nvidia.js +1 -1
  26. package/dist/core/nvidia.js.map +1 -1
  27. package/dist/core/projectInit.d.ts +6 -0
  28. package/dist/core/projectInit.js +44 -0
  29. package/dist/core/projectInit.js.map +1 -0
  30. package/dist/core/reasoning.js +3 -0
  31. package/dist/core/reasoning.js.map +1 -1
  32. package/dist/core/session.d.ts +1 -0
  33. package/dist/core/session.js +46 -0
  34. package/dist/core/session.js.map +1 -1
  35. package/dist/core/types.d.ts +9 -4
  36. package/dist/core/workspace.d.ts +8 -0
  37. package/dist/core/workspace.js +293 -21
  38. package/dist/core/workspace.js.map +1 -1
  39. package/dist/tui/App.js +536 -69
  40. package/dist/tui/App.js.map +1 -1
  41. package/dist/tui/commands.js +35 -6
  42. package/dist/tui/commands.js.map +1 -1
  43. package/dist/tui/components/CommandSuggestions.js +8 -3
  44. package/dist/tui/components/CommandSuggestions.js.map +1 -1
  45. package/dist/tui/components/Composer.js +1 -1
  46. package/dist/tui/components/Composer.js.map +1 -1
  47. package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
  48. package/dist/tui/components/ExperimentalPanel.js +33 -0
  49. package/dist/tui/components/ExperimentalPanel.js.map +1 -0
  50. package/dist/tui/components/Header.js +3 -3
  51. package/dist/tui/components/Header.js.map +1 -1
  52. package/dist/tui/components/OnboardingPanel.d.ts +13 -1
  53. package/dist/tui/components/OnboardingPanel.js +23 -9
  54. package/dist/tui/components/OnboardingPanel.js.map +1 -1
  55. package/dist/tui/components/Sidebar.js +17 -13
  56. package/dist/tui/components/Sidebar.js.map +1 -1
  57. package/dist/tui/components/Transcript.js +2 -2
  58. package/dist/tui/components/Transcript.js.map +1 -1
  59. package/dist/tui/format.js +7 -7
  60. package/dist/tui/format.js.map +1 -1
  61. package/dist/tui/modes.d.ts +1 -1
  62. package/dist/tui/modes.js +8 -2
  63. package/dist/tui/modes.js.map +1 -1
  64. package/docs/gemini-wrapper.md +87 -0
  65. package/docs/releases/v0.1.1-beta.md +18 -0
  66. package/docs/releases/v0.2.1.md +1 -1
  67. package/docs/releases/v0.3.1-beta.md +4 -0
  68. package/docs/releases/v0.4.0.md +1 -1
  69. package/docs/releases/v1.0.0.md +28 -0
  70. package/docs/showcase/patchpilot-banner.png +0 -0
  71. package/docs/showcase/patchpilot-logo.png +0 -0
  72. package/package.json +5 -2
package/dist/tui/App.js CHANGED
@@ -2,22 +2,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { Box, useApp, useInput, useStdout } from "ink";
4
4
  import { AgentRunner } from "../core/agent.js";
5
+ import { cleanupPatchPilot, readCleanupTarget } from "../core/cleanup.js";
5
6
  import { defaultCodexModel, hasCodexCliOAuth } from "../core/codex.js";
6
7
  import { describeComputeTarget } from "../core/compute.js";
7
8
  import { runDoctor } from "../core/doctor.js";
8
9
  import { savePatchPilotEnvValues } from "../core/env.js";
9
10
  import { defaultGeminiModel, readGeminiApiKey } from "../core/gemini.js";
11
+ import { defaultGeminiWrapperModel, geminiWrapperRequiresApiKey, readGeminiWrapperApiKey, readGeminiWrapperBaseUrl, readGeminiWrapperCookiesJson, readGeminiWrapperMode, readGeminiWrapperPythonCommand, saveGeminiWrapperCookieFile } from "../core/geminiWrapper.js";
10
12
  import { createModelClient } from "../core/modelClient.js";
11
13
  import { defaultNvidiaModel, readNvidiaApiKey } from "../core/nvidia.js";
12
14
  import { defaultOllamaModel, OllamaClient } from "../core/ollama.js";
13
15
  import { defaultOpenRouterModel, isOpenRouterFreeModel, readOpenRouterApiKey } from "../core/openrouter.js";
16
+ import { ensurePatchPilotGitignore, patchPilotInitPrompt } from "../core/projectInit.js";
14
17
  import { formatReasoningSupport } from "../core/reasoning.js";
15
- import { listWorkspaceSessions, loadSessionSummary, SessionStore } from "../core/session.js";
18
+ import { buildSessionResumeContext, listWorkspaceSessions, loadSessionSummary, SessionStore } from "../core/session.js";
16
19
  import { addTelemetryToSession, emptySessionTelemetry, estimateTokens } from "../core/tokenAccounting.js";
17
20
  import { WorkspaceTools } from "../core/workspace.js";
18
21
  import { ApprovalPanel } from "./components/ApprovalPanel.js";
19
22
  import { CommandSuggestions } from "./components/CommandSuggestions.js";
20
23
  import { Composer, FooterHints } from "./components/Composer.js";
24
+ import { ExperimentalPanel, experimentalFlagAt, experimentalFlagCount } from "./components/ExperimentalPanel.js";
21
25
  import { Header } from "./components/Header.js";
22
26
  import { OnboardingPanel } from "./components/OnboardingPanel.js";
23
27
  import { Sidebar } from "./components/Sidebar.js";
@@ -40,6 +44,11 @@ export function App(props) {
40
44
  const abortControllerRef = useRef(null);
41
45
  const sessionStoreRef = useRef(new SessionStore({ workspace: props.workspace }));
42
46
  const approvalResolverRef = useRef(null);
47
+ const runtimeStateRef = useRef({
48
+ isRunning: false,
49
+ hasPendingApproval: false,
50
+ lastSigintAt: 0
51
+ });
43
52
  const grantedPermissionsRef = useRef({
44
53
  allowWrite: props.allowWrite,
45
54
  allowShell: props.allowShell
@@ -55,6 +64,7 @@ export function App(props) {
55
64
  const [pendingApproval, setPendingApproval] = useState(null);
56
65
  const [telemetry, setTelemetry] = useState(null);
57
66
  const [sessionTelemetry, setSessionTelemetry] = useState(() => emptySessionTelemetry());
67
+ const [resumeContext, setResumeContext] = useState("");
58
68
  const [systemStats, setSystemStats] = useState(() => readSystemStats().stats);
59
69
  const [gpuStats, setGpuStats] = useState(null);
60
70
  const [agentMode, setAgentMode] = useState(() => initialAgentMode({ allowWrite: props.allowWrite, allowShell: props.allowShell }));
@@ -65,6 +75,13 @@ export function App(props) {
65
75
  const [modelOptions, setModelOptions] = useState([]);
66
76
  const [isLoadingModels, setIsLoadingModels] = useState(false);
67
77
  const [onboarding, setOnboarding] = useState(null);
78
+ const [experimentalOpen, setExperimentalOpen] = useState(false);
79
+ const [experimentalIndex, setExperimentalIndex] = useState(0);
80
+ const [experimentalFlags, setExperimentalFlags] = useState({
81
+ fileAnalysis: readBooleanEnv(process.env.PATCHPILOT_EXPERIMENTAL_FILE_ANALYSIS, false),
82
+ memory: readBooleanEnv(process.env.PATCHPILOT_EXPERIMENTAL_MEMORY, false),
83
+ subagents: props.subagents
84
+ });
68
85
  const [onboardingIndex, setOnboardingIndex] = useState(0);
69
86
  const [onboardingInput, setOnboardingInput] = useState("");
70
87
  const [onboardingBusyMessage, setOnboardingBusyMessage] = useState(null);
@@ -88,7 +105,7 @@ export function App(props) {
88
105
  const draftTokens = estimateTokens(input);
89
106
  const terminalRows = stdout.rows ?? 40;
90
107
  const terminalColumns = stdout.columns ?? 120;
91
- const paletteItems = !isRunning && !onboarding
108
+ const paletteItems = !isRunning && !onboarding && !experimentalOpen
92
109
  ? buildCommandSuggestionItems({
93
110
  input,
94
111
  provider: settings.provider,
@@ -102,9 +119,9 @@ export function App(props) {
102
119
  const rootHeight = Math.max(24, terminalRows);
103
120
  const headerReservedHeight = 5;
104
121
  const paletteReservedHeight = !onboarding && paletteItems.length > 0 ? Math.min(8, paletteItems.length) + 4 : 0;
105
- const composerReservedHeight = onboarding ? 0 : 2;
106
- const footerReservedHeight = onboarding ? 0 : 1;
107
- const approvalReservedHeight = !onboarding && (pendingApproval || bypassConfirmation) ? 6 : 0;
122
+ const composerReservedHeight = onboarding || experimentalOpen ? 0 : 2;
123
+ const footerReservedHeight = onboarding || experimentalOpen ? 0 : 1;
124
+ const approvalReservedHeight = !onboarding && !experimentalOpen && (pendingApproval || bypassConfirmation) ? 6 : 0;
108
125
  const panelHeight = Math.max(8, rootHeight - headerReservedHeight - composerReservedHeight - paletteReservedHeight - footerReservedHeight - approvalReservedHeight);
109
126
  const transcriptWidth = Math.max(42, terminalColumns - 38);
110
127
  const scrollStep = Math.max(4, Math.floor(panelHeight * 0.8));
@@ -124,6 +141,7 @@ export function App(props) {
124
141
  }
125
142
  approvalResolverRef.current(decision);
126
143
  approvalResolverRef.current = null;
144
+ setInput("");
127
145
  appendLine({
128
146
  kind: "approval",
129
147
  tone: decision === "deny" ? "warning" : "success",
@@ -139,6 +157,7 @@ export function App(props) {
139
157
  const permissions = permissionsForMode(nextMode);
140
158
  setAgentMode(nextMode);
141
159
  setBypassConfirmation(false);
160
+ grantedPermissionsRef.current = permissions;
142
161
  setSettings((currentSettings) => ({
143
162
  ...currentSettings,
144
163
  allowWrite: permissions.allowWrite,
@@ -166,9 +185,24 @@ export function App(props) {
166
185
  setWorkState("waiting_approval");
167
186
  }, [bypassConfirmation]);
168
187
  const confirmBypassMode = useCallback(() => {
188
+ setInput("");
169
189
  applyMode("bypass");
170
190
  }, [applyMode]);
191
+ const setExplicitPermission = useCallback((permission, enabled) => {
192
+ const nextPermissions = {
193
+ allowWrite: permission === "write" ? enabled : settings.allowWrite,
194
+ allowShell: permission === "shell" ? enabled : settings.allowShell
195
+ };
196
+ grantedPermissionsRef.current = nextPermissions;
197
+ setBypassConfirmation(false);
198
+ setAgentMode(nextPermissions.allowWrite && nextPermissions.allowShell ? "bypass" : nextPermissions.allowWrite || nextPermissions.allowShell ? "build" : "plan");
199
+ setSettings((currentSettings) => ({
200
+ ...currentSettings,
201
+ ...nextPermissions
202
+ }));
203
+ }, [settings.allowShell, settings.allowWrite]);
171
204
  const cancelBypassMode = useCallback(() => {
205
+ setInput("");
172
206
  setBypassConfirmation(false);
173
207
  setStatus("idle");
174
208
  setWorkState("idle");
@@ -291,13 +325,14 @@ export function App(props) {
291
325
  setTelemetry(null);
292
326
  setOnboardingInput("");
293
327
  setOnboardingNotice(null);
294
- setOnboardingBusyMessage(`Loading ${provider} models...`);
328
+ setOnboardingBusyMessage(null);
295
329
  const nextModel = defaultModelForProvider(provider, options.currentModel ?? settings.model);
296
330
  setSettings((currentSettings) => ({
297
331
  ...currentSettings,
298
332
  provider,
299
333
  model: nextModel
300
334
  }));
335
+ setOnboardingBusyMessage(`Loading ${provider} models...`);
301
336
  try {
302
337
  const models = await loadAvailableModels(provider, options.ollamaUrl ?? settings.ollamaUrl, setModelOptions, true);
303
338
  if (models.length === 0) {
@@ -307,9 +342,13 @@ export function App(props) {
307
342
  ? "No Ollama models found on that host."
308
343
  : provider === "gemini"
309
344
  ? "No Gemini models listed. Check the API key."
310
- : provider === "openrouter"
311
- ? "No OpenRouter models listed. Check the API key."
312
- : "No Codex OAuth models listed.",
345
+ : provider === "gemini-wrapper"
346
+ ? "No Gemini-Wrapper models listed. Check the bridge install and cookie setup."
347
+ : provider === "openrouter"
348
+ ? "No OpenRouter models listed. Check the API key."
349
+ : provider === "nvidia"
350
+ ? "No NVIDIA models listed. Check the API key."
351
+ : "No Codex OAuth models listed.",
313
352
  detail: "Use the back key to choose another provider or retry after fixing the provider setup."
314
353
  });
315
354
  return;
@@ -355,6 +394,11 @@ export function App(props) {
355
394
  case "host":
356
395
  case "api-key-choice":
357
396
  case "gemini-key":
397
+ case "gemini-wrapper-url":
398
+ case "gemini-wrapper-psid":
399
+ case "gemini-wrapper-psidts":
400
+ case "gemini-wrapper-model-mode":
401
+ case "gemini-wrapper-key":
358
402
  case "openrouter-key":
359
403
  case "nvidia-key":
360
404
  case "codex-login":
@@ -380,6 +424,12 @@ export function App(props) {
380
424
  openApiKeyChoice("gemini", setOnboarding, setOnboardingIndex);
381
425
  return;
382
426
  }
427
+ if (onboarding.provider === "gemini-wrapper") {
428
+ setOnboarding({
429
+ step: "gemini-wrapper-model-mode"
430
+ });
431
+ return;
432
+ }
383
433
  if (onboarding.provider === "nvidia") {
384
434
  openApiKeyChoice("nvidia", setOnboarding, setOnboardingIndex);
385
435
  return;
@@ -452,7 +502,7 @@ export function App(props) {
452
502
  }
453
503
  return;
454
504
  }
455
- if (selection === "gemini" || selection === "openrouter" || selection === "nvidia") {
505
+ if (selection === "gemini" || selection === "gemini-wrapper" || selection === "openrouter" || selection === "nvidia") {
456
506
  openApiKeyChoice(selection, setOnboarding, setOnboardingIndex);
457
507
  return;
458
508
  }
@@ -538,12 +588,20 @@ export function App(props) {
538
588
  return;
539
589
  }
540
590
  if (choice === 0 && onboarding.hasExistingKey) {
591
+ if (onboarding.provider === "gemini-wrapper") {
592
+ setOnboarding({
593
+ step: "gemini-wrapper-model-mode"
594
+ });
595
+ setOnboardingInput("");
596
+ setOnboardingIndex(0);
597
+ return;
598
+ }
541
599
  await openModelSelection(onboarding.provider, {
542
600
  currentModel: defaultModelForProvider(onboarding.provider, settings.model)
543
601
  });
544
602
  return;
545
603
  }
546
- setOnboarding({
604
+ setOnboarding(onboarding.provider === "gemini-wrapper" ? { step: "gemini-wrapper-psid" } : {
547
605
  step: `${onboarding.provider}-key`
548
606
  });
549
607
  setOnboardingInput("");
@@ -574,6 +632,151 @@ export function App(props) {
574
632
  });
575
633
  return;
576
634
  }
635
+ if (onboarding.step === "gemini-wrapper-psid") {
636
+ const secure1psid = value.trim();
637
+ if (!secure1psid) {
638
+ setOnboardingNotice({
639
+ tone: "warning",
640
+ text: "__Secure-1PSID cannot be empty.",
641
+ detail: "Paste the cookie value manually. PatchPilot will not scan browser profiles."
642
+ });
643
+ return;
644
+ }
645
+ setOnboarding({
646
+ step: "gemini-wrapper-psidts",
647
+ secure1psid
648
+ });
649
+ setOnboardingInput("");
650
+ setOnboardingIndex(0);
651
+ return;
652
+ }
653
+ if (onboarding.step === "gemini-wrapper-psidts") {
654
+ const secure1psidts = value.trim();
655
+ const cookiesPath = saveGeminiWrapperCookieFile({
656
+ secure1psid: onboarding.secure1psid,
657
+ secure1psidts
658
+ });
659
+ process.env.PATCHPILOT_GEMINI_WRAPPER_MODE = "python";
660
+ process.env.PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON = cookiesPath;
661
+ savePatchPilotEnvValues({
662
+ PATCHPILOT_PROVIDER: "gemini-wrapper",
663
+ PATCHPILOT_MODEL: defaultGeminiWrapperModel,
664
+ PATCHPILOT_GEMINI_WRAPPER_MODE: "python",
665
+ PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON: cookiesPath
666
+ });
667
+ setOnboardingNotice({
668
+ tone: "success",
669
+ text: "Gemini-API bridge cookies saved to PatchPilot config.",
670
+ detail: `${cookiesPath} was written with owner-only permissions. PatchPilot will run gemini_webapi through python3.`
671
+ });
672
+ setOnboarding({
673
+ step: "gemini-wrapper-model-mode"
674
+ });
675
+ setOnboardingInput("");
676
+ setOnboardingIndex(0);
677
+ return;
678
+ }
679
+ if (onboarding.step === "gemini-wrapper-model-mode") {
680
+ const choice = readIndexedSelection(value, onboardingIndex);
681
+ if (choice === null) {
682
+ return;
683
+ }
684
+ if (choice === 0) {
685
+ setTelemetry(null);
686
+ setModelOptions([defaultGeminiWrapperModel]);
687
+ setSettings((currentSettings) => ({
688
+ ...currentSettings,
689
+ provider: "gemini-wrapper",
690
+ model: defaultGeminiWrapperModel
691
+ }));
692
+ savePatchPilotEnvValues({
693
+ PATCHPILOT_PROVIDER: "gemini-wrapper",
694
+ PATCHPILOT_MODEL: defaultGeminiWrapperModel,
695
+ PATCHPILOT_ONBOARDING_COMPLETE: "1"
696
+ });
697
+ appendLine({
698
+ tone: "success",
699
+ label: "onboarding",
700
+ text: `ready: gemini-wrapper using ${defaultGeminiWrapperModel}`
701
+ });
702
+ closeOnboarding();
703
+ return;
704
+ }
705
+ await openModelSelection("gemini-wrapper", {
706
+ currentModel: settings.model
707
+ });
708
+ return;
709
+ }
710
+ if (onboarding.step === "gemini-wrapper-url") {
711
+ const baseUrl = value.trim().replace(/\/$/, "");
712
+ if (!baseUrl) {
713
+ setOnboardingNotice({
714
+ tone: "warning",
715
+ text: "Gemini-Wrapper URL cannot be empty."
716
+ });
717
+ return;
718
+ }
719
+ try {
720
+ new URL(baseUrl);
721
+ }
722
+ catch {
723
+ setOnboardingNotice({
724
+ tone: "warning",
725
+ text: "Gemini-Wrapper URL must be a valid URL.",
726
+ detail: "Example: http://localhost:8787/v1"
727
+ });
728
+ return;
729
+ }
730
+ process.env.PATCHPILOT_GEMINI_WRAPPER_BASE_URL = baseUrl;
731
+ savePatchPilotEnvValues({
732
+ PATCHPILOT_PROVIDER: "gemini-wrapper",
733
+ PATCHPILOT_MODEL: defaultGeminiWrapperModel,
734
+ PATCHPILOT_GEMINI_WRAPPER_BASE_URL: baseUrl
735
+ });
736
+ setOnboardingNotice({
737
+ tone: "success",
738
+ text: "Gemini-Wrapper URL saved to PatchPilot config.",
739
+ detail: "PatchPilot uses only this explicit URL and never reads browser cookies."
740
+ });
741
+ if (geminiWrapperRequiresApiKey(baseUrl) && !readGeminiWrapperApiKey()) {
742
+ setOnboarding({
743
+ step: "gemini-wrapper-key",
744
+ baseUrl
745
+ });
746
+ setOnboardingInput("");
747
+ setOnboardingIndex(0);
748
+ return;
749
+ }
750
+ await openModelSelection("gemini-wrapper", {
751
+ currentModel: defaultGeminiWrapperModel
752
+ });
753
+ return;
754
+ }
755
+ if (onboarding.step === "gemini-wrapper-key") {
756
+ const apiKey = value.trim();
757
+ if (geminiWrapperRequiresApiKey(onboarding.baseUrl) && !apiKey) {
758
+ setOnboardingNotice({
759
+ tone: "warning",
760
+ text: "Gemini-Wrapper API key cannot be empty for remote wrapper URLs."
761
+ });
762
+ return;
763
+ }
764
+ process.env.PATCHPILOT_GEMINI_WRAPPER_API_KEY = apiKey;
765
+ savePatchPilotEnvValues({
766
+ PATCHPILOT_PROVIDER: "gemini-wrapper",
767
+ PATCHPILOT_MODEL: defaultGeminiWrapperModel,
768
+ PATCHPILOT_GEMINI_WRAPPER_BASE_URL: onboarding.baseUrl,
769
+ ...(apiKey ? { PATCHPILOT_GEMINI_WRAPPER_API_KEY: apiKey } : {})
770
+ });
771
+ setOnboardingNotice({
772
+ tone: "success",
773
+ text: apiKey ? "Gemini-Wrapper API key saved to PatchPilot config." : "Gemini-Wrapper local URL saved without an API key."
774
+ });
775
+ await openModelSelection("gemini-wrapper", {
776
+ currentModel: defaultGeminiWrapperModel
777
+ });
778
+ return;
779
+ }
577
780
  if (onboarding.step === "openrouter-key") {
578
781
  const apiKey = value.trim();
579
782
  if (!apiKey) {
@@ -638,7 +841,7 @@ export function App(props) {
638
841
  }
639
842
  const visibleModels = selectableModels(onboardingInput, onboarding.models);
640
843
  const selectedModel = visibleModels[onboardingIndex] ?? selectModelFromInput(value, visibleModels, onboardingIndex, {
641
- allowManual: onboarding.provider !== "ollama"
844
+ allowManual: onboarding.provider !== "ollama" && onboarding.provider !== "gemini-wrapper"
642
845
  });
643
846
  if (!selectedModel) {
644
847
  setOnboardingNotice({
@@ -674,7 +877,7 @@ export function App(props) {
674
877
  }
675
878
  closeOnboarding();
676
879
  }, [activeHost?.host.url, appendLine, closeOnboarding, connectToHost, loadHostSuggestions, onboarding, onboardingBusyMessage, onboardingIndex, openModelSelection, settings.ollamaUrl]);
677
- const runTask = useCallback(async (task) => {
880
+ const runTask = useCallback(async (task, overrides = {}) => {
678
881
  if (!task.trim() || isRunning) {
679
882
  return;
680
883
  }
@@ -694,13 +897,17 @@ export function App(props) {
694
897
  }
695
898
  const abortController = new AbortController();
696
899
  abortControllerRef.current = abortController;
900
+ const effectiveMode = overrides.mode ?? agentMode;
697
901
  const taskRunner = new AgentRunner({
698
902
  ...runnableSettings,
699
- mode: agentMode,
903
+ allowExternalFileAnalysis: experimentalFlags.fileAnalysis,
904
+ memoryEnabled: experimentalFlags.memory,
905
+ mode: effectiveMode,
700
906
  signal: abortController.signal,
701
907
  sessionStore: sessionStoreRef.current,
908
+ resumeContext,
702
909
  approvalHandler: (request) => new Promise((resolve) => {
703
- if (agentMode === "plan") {
910
+ if (effectiveMode === "plan") {
704
911
  appendLine({
705
912
  kind: "approval",
706
913
  tone: "warning",
@@ -714,7 +921,7 @@ export function App(props) {
714
921
  resolve("deny");
715
922
  return;
716
923
  }
717
- if (agentMode === "bypass") {
924
+ if (effectiveMode === "bypass" && ((request.permission === "write" && runnableSettings.allowWrite) || (request.permission === "shell" && runnableSettings.allowShell))) {
718
925
  resolve("allow_session");
719
926
  return;
720
927
  }
@@ -772,7 +979,7 @@ export function App(props) {
772
979
  setWorkState("idle");
773
980
  setIsRunning(false);
774
981
  }
775
- }, [agentMode, appendLine, isRunning, modelOptions, settings]);
982
+ }, [agentMode, appendLine, experimentalFlags, isRunning, modelOptions, resumeContext, settings]);
776
983
  const handleSlashCommand = useCallback(async (rawCommand) => {
777
984
  const [commandName = "", ...args] = rawCommand.slice(1).trim().split(/\s+/);
778
985
  const command = commandName.toLowerCase();
@@ -821,11 +1028,11 @@ export function App(props) {
821
1028
  return;
822
1029
  case "provider": {
823
1030
  const nextProvider = args[0]?.toLowerCase();
824
- if (nextProvider !== "ollama" && nextProvider !== "gemini" && nextProvider !== "codex" && nextProvider !== "openrouter" && nextProvider !== "nvidia") {
1031
+ if (nextProvider !== "ollama" && nextProvider !== "gemini" && nextProvider !== "gemini-wrapper" && nextProvider !== "codex" && nextProvider !== "openrouter" && nextProvider !== "nvidia") {
825
1032
  appendLine({
826
1033
  tone: "accent",
827
1034
  label: "provider",
828
- text: `current ${settings.provider}. Use /provider ollama, gemini, openrouter, nvidia, or codex.`
1035
+ text: `current ${settings.provider}. Use /provider ollama, gemini, gemini-wrapper, openrouter, nvidia, or codex.`
829
1036
  });
830
1037
  return;
831
1038
  }
@@ -848,7 +1055,7 @@ export function App(props) {
848
1055
  tone: needsApiKey(nextProvider) && !hasApiKey(nextProvider) ? "warning" : "success",
849
1056
  label: "provider",
850
1057
  text: needsApiKey(nextProvider) && !hasApiKey(nextProvider)
851
- ? `${nextProvider} needs an API key. Setup opened.`
1058
+ ? `${nextProvider} needs setup. Setup opened.`
852
1059
  : `switched to ${nextProvider} using ${nextModel}`
853
1060
  });
854
1061
  return;
@@ -868,6 +1075,10 @@ export function App(props) {
868
1075
  ...currentSettings,
869
1076
  subagents: subagentsEnabled
870
1077
  }));
1078
+ setExperimentalFlags((currentFlags) => ({
1079
+ ...currentFlags,
1080
+ subagents: subagentsEnabled
1081
+ }));
871
1082
  appendLine({
872
1083
  tone: "success",
873
1084
  label: "agents",
@@ -924,38 +1135,30 @@ export function App(props) {
924
1135
  case "write":
925
1136
  case "apply": {
926
1137
  const writeEnabled = readToggle(args[0], !settings.allowWrite);
927
- if (writeEnabled) {
928
- requestBypassMode();
929
- return;
930
- }
931
- grantedPermissionsRef.current.allowWrite = writeEnabled;
932
- applyMode("build", false);
1138
+ setExplicitPermission("write", writeEnabled);
933
1139
  appendLine({
934
1140
  tone: "success",
935
1141
  label: "write",
936
- text: "workspace writes require approval in build mode"
1142
+ text: writeEnabled ? "workspace writes are allowed; shell remains separately controlled" : "workspace writes disabled"
937
1143
  });
938
1144
  return;
939
1145
  }
940
1146
  case "shell": {
941
1147
  const shellEnabled = readToggle(args[0], !settings.allowShell);
942
- if (shellEnabled) {
943
- requestBypassMode();
944
- return;
945
- }
946
- grantedPermissionsRef.current.allowShell = shellEnabled;
947
- applyMode("build", false);
1148
+ setExplicitPermission("shell", shellEnabled);
948
1149
  appendLine({
949
1150
  tone: "success",
950
1151
  label: "shell",
951
- text: "shell commands require approval in build mode"
1152
+ text: shellEnabled ? "shell commands are allowed; writes remain separately controlled" : "shell commands disabled"
952
1153
  });
953
1154
  return;
954
1155
  }
955
1156
  case "model": {
956
1157
  const requestedModel = normalizeModelAlias(args.join(" ").trim());
957
1158
  if (!requestedModel) {
958
- const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine);
1159
+ const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine, {
1160
+ refresh: settings.provider === "gemini-wrapper"
1161
+ });
959
1162
  if (!models) {
960
1163
  return;
961
1164
  }
@@ -968,12 +1171,14 @@ export function App(props) {
968
1171
  return;
969
1172
  }
970
1173
  {
971
- const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine);
1174
+ const models = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine, {
1175
+ refresh: settings.provider === "gemini-wrapper"
1176
+ });
972
1177
  if (!models) {
973
1178
  return;
974
1179
  }
975
1180
  const nextModel = selectModelFromInput(requestedModel, models, undefined, {
976
- allowManual: settings.provider !== "ollama"
1181
+ allowManual: settings.provider !== "ollama" && settings.provider !== "gemini-wrapper"
977
1182
  });
978
1183
  if (!nextModel) {
979
1184
  appendLine({
@@ -991,12 +1196,14 @@ export function App(props) {
991
1196
  case "models": {
992
1197
  const requestedModel = args.join(" ").trim();
993
1198
  if (requestedModel) {
994
- const installedModels = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine);
1199
+ const installedModels = await loadKnownOrAvailableModels(settings.provider, settings.ollamaUrl, modelOptions, setModelOptions, appendLine, {
1200
+ refresh: settings.provider === "gemini-wrapper"
1201
+ });
995
1202
  if (!installedModels) {
996
1203
  return;
997
1204
  }
998
1205
  const nextModel = selectModelFromInput(requestedModel, installedModels, undefined, {
999
- allowManual: settings.provider !== "ollama"
1206
+ allowManual: settings.provider !== "ollama" && settings.provider !== "gemini-wrapper"
1000
1207
  });
1001
1208
  if (!nextModel) {
1002
1209
  appendLine({
@@ -1074,11 +1281,26 @@ export function App(props) {
1074
1281
  const sessionId = args[0] ?? "";
1075
1282
  const sessions = await listWorkspaceSessions(settings.workspace);
1076
1283
  const selectedSession = sessionId ? await loadSessionSummary(settings.workspace, sessionId) : sessions[0] ?? null;
1284
+ if (selectedSession) {
1285
+ sessionStoreRef.current = new SessionStore({
1286
+ workspace: settings.workspace,
1287
+ sessionId: selectedSession.sessionId
1288
+ });
1289
+ await sessionStoreRef.current.append({
1290
+ type: "session.resumed",
1291
+ sessionId: selectedSession.sessionId,
1292
+ workspace: settings.workspace,
1293
+ resumedAt: new Date().toISOString()
1294
+ });
1295
+ setResumeContext(await buildSessionResumeContext(settings.workspace, selectedSession.sessionId));
1296
+ setSessionTelemetry(emptySessionTelemetry());
1297
+ setTelemetry(null);
1298
+ }
1077
1299
  appendLine({
1078
1300
  kind: "status",
1079
1301
  tone: selectedSession ? "accent" : "warning",
1080
1302
  label: "resume",
1081
- text: selectedSession ? `Loaded session ${selectedSession.sessionId}` : "No session available to resume.",
1303
+ text: selectedSession ? `Loaded session ${selectedSession.sessionId} and will inject its summary into the next run.` : "No session available to resume.",
1082
1304
  detail: selectedSession
1083
1305
  ? `workspace ${selectedSession.workspace}\nupdated ${selectedSession.updatedAt}\nmodel ${selectedSession.provider ?? "-"} ${selectedSession.model ?? "-"}\nlast task ${selectedSession.lastTask ?? "-"}`
1084
1306
  : "Run /sessions after at least one PatchPilot run."
@@ -1196,29 +1418,151 @@ export function App(props) {
1196
1418
  return;
1197
1419
  }
1198
1420
  case "doctor": {
1421
+ const shouldFix = args.some((arg) => arg.toLowerCase() === "fix" || arg.toLowerCase() === "--fix");
1199
1422
  appendLine({
1200
1423
  tone: "muted",
1201
1424
  label: "doctor",
1202
- text: "checking local requirements..."
1425
+ text: shouldFix ? "checking local requirements and applying safe fixes..." : "checking local requirements..."
1426
+ });
1427
+ const doctorResults = await runDoctor(settings.provider, settings.ollamaUrl, settings.model, {
1428
+ fix: shouldFix
1203
1429
  });
1204
- const doctorResults = await runDoctor(settings.provider, settings.ollamaUrl, settings.model);
1205
1430
  for (const result of doctorResults) {
1206
1431
  appendLine({
1207
1432
  tone: result.ok ? "success" : "danger",
1208
1433
  label: result.name,
1209
- text: result.details
1434
+ text: result.action && result.action !== "check" ? `${result.action}: ${result.details}` : result.details
1435
+ });
1436
+ }
1437
+ if (!shouldFix && doctorResults.some((result) => result.action === "skipped")) {
1438
+ appendLine({
1439
+ tone: "accent",
1440
+ label: "doctor",
1441
+ text: "Some safe fixes are available. Run /doctor fix to approve them."
1210
1442
  });
1211
1443
  }
1212
1444
  return;
1213
1445
  }
1446
+ case "cleanup": {
1447
+ const target = readCleanupTarget(args[0]);
1448
+ if (!target) {
1449
+ appendLine({
1450
+ tone: "accent",
1451
+ label: "cleanup",
1452
+ text: "Choose what to clean: /cleanup cache, /cleanup sessions, /cleanup temp, or /cleanup all.",
1453
+ detail: "Sessions deletes saved workspace transcripts. Cache/temp are safe first choices."
1454
+ });
1455
+ return;
1456
+ }
1457
+ const removed = await cleanupPatchPilot(settings.workspace, target);
1458
+ if (target === "sessions" || target === "all") {
1459
+ sessionStoreRef.current = new SessionStore({
1460
+ workspace: settings.workspace
1461
+ });
1462
+ await sessionStoreRef.current.create();
1463
+ setResumeContext("");
1464
+ setLines([]);
1465
+ setAdvisorNotes([]);
1466
+ setTelemetry(null);
1467
+ setSessionTelemetry(emptySessionTelemetry());
1468
+ }
1469
+ appendLine({
1470
+ tone: "success",
1471
+ label: "cleanup",
1472
+ text: `cleaned ${removed.join(", ") || target}`
1473
+ });
1474
+ return;
1475
+ }
1476
+ case "experimental": {
1477
+ const requestedFlag = args[0]?.toLowerCase();
1478
+ const requestedValue = args[1]?.toLowerCase();
1479
+ if (!requestedFlag) {
1480
+ setExperimentalOpen(true);
1481
+ setExperimentalIndex(0);
1482
+ setInput("");
1483
+ return;
1484
+ }
1485
+ const enabled = readToggle(requestedValue, true);
1486
+ if (requestedFlag === "subagents" || requestedFlag === "agents") {
1487
+ setSettings((currentSettings) => ({
1488
+ ...currentSettings,
1489
+ subagents: enabled
1490
+ }));
1491
+ }
1492
+ savePatchPilotEnvValues({
1493
+ [`PATCHPILOT_EXPERIMENTAL_${requestedFlag.replace(/-/g, "_").toUpperCase()}`]: enabled ? "1" : "0"
1494
+ });
1495
+ setExperimentalFlags((currentFlags) => ({
1496
+ ...currentFlags,
1497
+ ...(requestedFlag === "file-analysis"
1498
+ ? { fileAnalysis: enabled }
1499
+ : requestedFlag === "memory"
1500
+ ? { memory: enabled }
1501
+ : requestedFlag === "subagents" || requestedFlag === "agents"
1502
+ ? { subagents: enabled }
1503
+ : {})
1504
+ }));
1505
+ appendLine({
1506
+ tone: "success",
1507
+ label: "experimental",
1508
+ text: `${requestedFlag} ${enabled ? "enabled" : "disabled"}`
1509
+ });
1510
+ return;
1511
+ }
1512
+ case "init": {
1513
+ await ensurePatchPilotGitignore(settings.workspace);
1514
+ appendLine({
1515
+ tone: "accent",
1516
+ label: "init",
1517
+ text: "starting model-driven project init",
1518
+ detail: "PatchPilot will inspect the repository and create or update PATCHPILOT.md with approval-gated writes."
1519
+ });
1520
+ await runTask(patchPilotInitPrompt, {
1521
+ mode: "build"
1522
+ });
1523
+ return;
1524
+ }
1214
1525
  case "clear":
1215
1526
  setLines([]);
1216
1527
  setAdvisorNotes([]);
1217
1528
  setTelemetry(null);
1529
+ setResumeContext("");
1218
1530
  setSessionTelemetry(emptySessionTelemetry());
1219
1531
  setTranscriptScrollOffset(0);
1220
1532
  setSessionScrollOffset(0);
1221
1533
  return;
1534
+ case "new":
1535
+ if (isRunning) {
1536
+ appendLine({
1537
+ tone: "warning",
1538
+ label: "new",
1539
+ text: "Cannot start a new session while a run is active.",
1540
+ detail: "Stop the current run first, then use /new again."
1541
+ });
1542
+ return;
1543
+ }
1544
+ sessionStoreRef.current = new SessionStore({
1545
+ workspace: settings.workspace
1546
+ });
1547
+ await sessionStoreRef.current.create();
1548
+ setLines([]);
1549
+ setAdvisorNotes([]);
1550
+ setTelemetry(null);
1551
+ setSessionTelemetry(emptySessionTelemetry());
1552
+ setPendingApproval(null);
1553
+ approvalResolverRef.current = null;
1554
+ setBypassConfirmation(false);
1555
+ setInput("");
1556
+ setTranscriptScrollOffset(0);
1557
+ setSessionScrollOffset(0);
1558
+ setStatus("idle");
1559
+ setWorkState("idle");
1560
+ appendLine({
1561
+ tone: "success",
1562
+ label: "new",
1563
+ text: `started session ${sessionStoreRef.current.sessionId}`
1564
+ });
1565
+ return;
1222
1566
  case "exit":
1223
1567
  case "quit":
1224
1568
  case "q":
@@ -1245,6 +1589,7 @@ export function App(props) {
1245
1589
  loadHostSuggestions,
1246
1590
  loadProviderModels,
1247
1591
  modelOptions,
1592
+ isRunning,
1248
1593
  resolveApproval,
1249
1594
  sessionTelemetry,
1250
1595
  settings,
@@ -1286,6 +1631,10 @@ export function App(props) {
1286
1631
  useEffect(() => {
1287
1632
  void sessionStoreRef.current.create();
1288
1633
  }, []);
1634
+ useEffect(() => {
1635
+ runtimeStateRef.current.isRunning = isRunning;
1636
+ runtimeStateRef.current.hasPendingApproval = Boolean(pendingApproval || bypassConfirmation);
1637
+ }, [bypassConfirmation, isRunning, pendingApproval]);
1289
1638
  useEffect(() => {
1290
1639
  if (!props.initialTask || didRunInitialTask.current || onboarding || process.env.PATCHPILOT_ONBOARDING_COMPLETE !== "1") {
1291
1640
  return;
@@ -1374,6 +1723,44 @@ export function App(props) {
1374
1723
  }
1375
1724
  }, [hostOptions.length, input, isLoadingHosts, isLoadingModels, isRunning, loadHostSuggestions, loadProviderModels, modelOptions.length, onboarding, settings.provider]);
1376
1725
  useInput((inputValue, key) => {
1726
+ if (experimentalOpen) {
1727
+ if (key.upArrow) {
1728
+ setExperimentalIndex((currentIndex) => (currentIndex - 1 + experimentalFlagCount()) % experimentalFlagCount());
1729
+ return;
1730
+ }
1731
+ if (key.downArrow) {
1732
+ setExperimentalIndex((currentIndex) => (currentIndex + 1) % experimentalFlagCount());
1733
+ return;
1734
+ }
1735
+ if (inputValue === " ") {
1736
+ const flag = experimentalFlagAt(experimentalIndex);
1737
+ setExperimentalFlags((currentFlags) => {
1738
+ const nextFlags = {
1739
+ ...currentFlags,
1740
+ [flag]: !currentFlags[flag]
1741
+ };
1742
+ if (flag === "subagents") {
1743
+ setSettings((currentSettings) => ({
1744
+ ...currentSettings,
1745
+ subagents: nextFlags.subagents
1746
+ }));
1747
+ }
1748
+ savePatchPilotEnvValues({
1749
+ PATCHPILOT_EXPERIMENTAL_FILE_ANALYSIS: nextFlags.fileAnalysis ? "1" : "0",
1750
+ PATCHPILOT_EXPERIMENTAL_MEMORY: nextFlags.memory ? "1" : "0",
1751
+ PATCHPILOT_EXPERIMENTAL_SUBAGENTS: nextFlags.subagents ? "1" : "0"
1752
+ });
1753
+ return nextFlags;
1754
+ });
1755
+ return;
1756
+ }
1757
+ if (key.return || key.escape || key.leftArrow) {
1758
+ setExperimentalOpen(false);
1759
+ setInput("");
1760
+ return;
1761
+ }
1762
+ return;
1763
+ }
1377
1764
  if (bypassConfirmation) {
1378
1765
  const normalizedInput = inputValue.toLowerCase();
1379
1766
  if (key.tab) {
@@ -1432,7 +1819,7 @@ export function App(props) {
1432
1819
  setOnboardingIndex((currentIndex) => (currentIndex + 1) % optionCount);
1433
1820
  return;
1434
1821
  }
1435
- if (optionCount > 0 && key.return && onboarding.step !== "model") {
1822
+ if (optionCount > 0 && key.return) {
1436
1823
  void handleOnboardingSubmit(String(onboardingIndex + 1));
1437
1824
  return;
1438
1825
  }
@@ -1457,6 +1844,16 @@ export function App(props) {
1457
1844
  }
1458
1845
  }
1459
1846
  const canUsePanelKeys = input.length === 0 || isRunning;
1847
+ if (canUsePanelKeys && key.upArrow && paletteItems.length === 0) {
1848
+ const setOffset = activeScrollPane === "session" ? setSessionScrollOffset : setTranscriptScrollOffset;
1849
+ setOffset((currentOffset) => currentOffset + 1);
1850
+ return;
1851
+ }
1852
+ if (canUsePanelKeys && key.downArrow && paletteItems.length === 0) {
1853
+ const setOffset = activeScrollPane === "session" ? setSessionScrollOffset : setTranscriptScrollOffset;
1854
+ setOffset((currentOffset) => Math.max(0, currentOffset - 1));
1855
+ return;
1856
+ }
1460
1857
  if (canUsePanelKeys && key.leftArrow) {
1461
1858
  setActiveScrollPane("session");
1462
1859
  return;
@@ -1490,19 +1887,44 @@ export function App(props) {
1490
1887
  }
1491
1888
  });
1492
1889
  useEffect(() => {
1890
+ const gracefulStopOrExit = () => {
1891
+ const now = Date.now();
1892
+ const state = runtimeStateRef.current;
1893
+ if ((state.isRunning || state.hasPendingApproval) && now - state.lastSigintAt > 1500) {
1894
+ state.lastSigintAt = now;
1895
+ abortControllerRef.current?.abort();
1896
+ approvalResolverRef.current?.("deny");
1897
+ approvalResolverRef.current = null;
1898
+ setPendingApproval(null);
1899
+ setBypassConfirmation(false);
1900
+ setInput("");
1901
+ setStatus("stopping");
1902
+ setWorkState("idle");
1903
+ appendLine({
1904
+ kind: "status",
1905
+ tone: "warning",
1906
+ label: "stop",
1907
+ text: "Stopping current task. Press Ctrl-C again to quit."
1908
+ });
1909
+ return;
1910
+ }
1911
+ void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(() => {
1912
+ process.exit(0);
1913
+ });
1914
+ };
1493
1915
  const unloadAndExit = () => {
1494
1916
  void unloadUsedOllamaModels(usedOllamaModelsRef.current).finally(() => {
1495
1917
  process.exit(0);
1496
1918
  });
1497
1919
  };
1498
- process.once("SIGINT", unloadAndExit);
1499
- process.once("SIGTERM", unloadAndExit);
1920
+ process.on("SIGINT", gracefulStopOrExit);
1921
+ process.on("SIGTERM", unloadAndExit);
1500
1922
  return () => {
1501
- process.off("SIGINT", unloadAndExit);
1923
+ process.off("SIGINT", gracefulStopOrExit);
1502
1924
  process.off("SIGTERM", unloadAndExit);
1503
1925
  void unloadUsedOllamaModels(usedOllamaModelsRef.current);
1504
1926
  };
1505
- }, []);
1927
+ }, [appendLine]);
1506
1928
  useEffect(() => {
1507
1929
  let previousSnapshot = readSystemStats().snapshot;
1508
1930
  const timer = setInterval(() => {
@@ -1531,10 +1953,10 @@ export function App(props) {
1531
1953
  clearInterval(timer);
1532
1954
  };
1533
1955
  }, []);
1534
- 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 }), onboarding ? (_jsx(OnboardingPanel, { state: onboarding, height: panelHeight, selectedIndex: onboardingIndex, input: onboardingInput, busyMessage: onboardingBusyMessage, notice: onboardingNotice, onInputChange: setOnboardingInput, onInputSubmit: (value) => void handleOnboardingSubmit(value) })) : (_jsxs(Box, { flexDirection: "row", height: panelHeight + approvalReservedHeight + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Sidebar, { workspace: settings.workspace, model: settings.model, provider: settings.provider, ollamaUrl: settings.ollamaUrl, agentMode: agentMode, allowWrite: settings.allowWrite, allowShell: settings.allowShell, subagents: settings.subagents, workState: workState, sessionId: sessionStoreRef.current.sessionId, systemStats: systemStats, gpuStats: gpuStats, telemetry: telemetry, sessionTelemetry: sessionTelemetry, draftTokens: draftTokens, height: panelHeight, scrollOffset: sessionScrollOffset, advisors: advisorNotes, isActive: activeScrollPane === "session", activeHost: activeHost }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, height: panelHeight + approvalReservedHeight + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Transcript, { lines: lines, isRunning: isRunning, isActive: activeScrollPane === "transcript", height: panelHeight, width: transcriptWidth, scrollOffset: transcriptScrollOffset }), _jsx(ApprovalPanel, { request: pendingApproval, bypassConfirmation: bypassConfirmation }), _jsx(Composer, { input: input, isRunning: isRunning, status: status, draftTokens: draftTokens, isApprovalWaiting: Boolean(pendingApproval || bypassConfirmation), onChange: setInput, onSubmit: (value) => void handleSubmit(value) }), paletteItems.length > 0 ? _jsx(CommandSuggestions, { items: paletteItems, selectedIndex: paletteIndex }) : null, _jsx(FooterHints, { activePane: activeScrollPane })] })] }))] }));
1956
+ 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: panelHeight })) : onboarding ? (_jsx(OnboardingPanel, { state: onboarding, height: panelHeight, selectedIndex: onboardingIndex, input: onboardingInput, busyMessage: onboardingBusyMessage, notice: onboardingNotice, onInputChange: setOnboardingInput, onInputSubmit: (value) => void handleOnboardingSubmit(value) })) : (_jsxs(Box, { flexDirection: "row", height: panelHeight + approvalReservedHeight + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Sidebar, { workspace: settings.workspace, model: settings.model, provider: settings.provider, ollamaUrl: settings.ollamaUrl, agentMode: agentMode, allowWrite: settings.allowWrite, allowShell: settings.allowShell, subagents: settings.subagents, workState: workState, sessionId: sessionStoreRef.current.sessionId, systemStats: systemStats, gpuStats: gpuStats, telemetry: telemetry, sessionTelemetry: sessionTelemetry, draftTokens: draftTokens, height: panelHeight, scrollOffset: sessionScrollOffset, advisors: advisorNotes, isActive: activeScrollPane === "session", activeHost: activeHost }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, height: panelHeight + approvalReservedHeight + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Transcript, { lines: lines, isRunning: isRunning, isActive: activeScrollPane === "transcript", height: panelHeight, width: transcriptWidth, scrollOffset: transcriptScrollOffset }), _jsx(ApprovalPanel, { request: pendingApproval, bypassConfirmation: bypassConfirmation }), _jsx(Composer, { input: input, isRunning: isRunning, status: status, draftTokens: draftTokens, isApprovalWaiting: Boolean(pendingApproval || bypassConfirmation), onChange: setInput, onSubmit: (value) => void handleSubmit(value) }), paletteItems.length > 0 ? _jsx(CommandSuggestions, { items: paletteItems, selectedIndex: paletteIndex }) : null, _jsx(FooterHints, { activePane: activeScrollPane })] })] }))] }));
1535
1957
  }
1536
1958
  async function loadAvailableModels(provider, ollamaUrl, setModelOptions, refresh = false) {
1537
- const cacheKey = `${provider}:${provider === "ollama" ? ollamaUrl : "default"}`;
1959
+ const cacheKey = modelCacheKey(provider, ollamaUrl);
1538
1960
  const cachedModels = modelCache.get(cacheKey);
1539
1961
  if (!refresh && cachedModels && cachedModels.expiresAt > Date.now()) {
1540
1962
  setModelOptions(cachedModels.models);
@@ -1551,9 +1973,24 @@ async function loadAvailableModels(provider, ollamaUrl, setModelOptions, refresh
1551
1973
  setModelOptions(models);
1552
1974
  return models;
1553
1975
  }
1554
- async function loadKnownOrAvailableModels(provider, ollamaUrl, modelOptions, setModelOptions, appendLine) {
1976
+ function modelCacheKey(provider, ollamaUrl) {
1977
+ if (provider === "ollama") {
1978
+ return `${provider}:${ollamaUrl}`;
1979
+ }
1980
+ if (provider === "gemini-wrapper") {
1981
+ return [
1982
+ provider,
1983
+ readGeminiWrapperMode(),
1984
+ readGeminiWrapperBaseUrl() || "python",
1985
+ readGeminiWrapperPythonCommand(),
1986
+ readGeminiWrapperCookiesJson()
1987
+ ].join(":");
1988
+ }
1989
+ return `${provider}:default`;
1990
+ }
1991
+ async function loadKnownOrAvailableModels(provider, ollamaUrl, modelOptions, setModelOptions, appendLine, options = {}) {
1555
1992
  try {
1556
- return modelOptions.length > 0 ? modelOptions : await loadAvailableModels(provider, ollamaUrl, setModelOptions);
1993
+ return !options.refresh && modelOptions.length > 0 ? modelOptions : await loadAvailableModels(provider, ollamaUrl, setModelOptions, options.refresh);
1557
1994
  }
1558
1995
  catch (error) {
1559
1996
  appendLine({
@@ -1577,7 +2014,7 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
1577
2014
  if (!installedModels) {
1578
2015
  return;
1579
2016
  }
1580
- if (!installedModels.includes(nextModel) && !(provider !== "ollama" && isPlausibleCloudModelId(nextModel))) {
2017
+ if (!installedModels.includes(nextModel) && !canUseUnverifiedCloudModel(provider, nextModel)) {
1581
2018
  appendLine({
1582
2019
  tone: "warning",
1583
2020
  label: "model",
@@ -1588,9 +2025,11 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
1588
2025
  ? "No models installed on the selected host."
1589
2026
  : provider === "gemini"
1590
2027
  ? "Check GEMINI_API_KEY in PatchPilot config."
1591
- : provider === "openrouter"
1592
- ? "Check OPENROUTER_API_KEY in PatchPilot config."
1593
- : "Run codex login first."
2028
+ : provider === "gemini-wrapper"
2029
+ ? "Check PATCHPILOT_GEMINI_WRAPPER_BASE_URL in PatchPilot config."
2030
+ : provider === "openrouter"
2031
+ ? "Check OPENROUTER_API_KEY in PatchPilot config."
2032
+ : "Run codex login first."
1594
2033
  });
1595
2034
  return;
1596
2035
  }
@@ -1633,7 +2072,7 @@ async function resolveRunnableSettings(settings, modelOptions, appendLine, setMo
1633
2072
  });
1634
2073
  return null;
1635
2074
  }
1636
- if (installedModels.includes(settings.model) || (settings.provider !== "ollama" && isPlausibleCloudModelId(settings.model))) {
2075
+ if (installedModels.includes(settings.model) || canUseUnverifiedCloudModel(settings.provider, settings.model)) {
1637
2076
  if (!installedModels.includes(settings.model)) {
1638
2077
  appendLine({
1639
2078
  tone: "warning",
@@ -1654,9 +2093,11 @@ async function resolveRunnableSettings(settings, modelOptions, appendLine, setMo
1654
2093
  ? "No models installed on the selected host."
1655
2094
  : settings.provider === "gemini"
1656
2095
  ? "No Gemini models listed. Check GEMINI_API_KEY in PatchPilot config."
1657
- : settings.provider === "openrouter"
1658
- ? "No OpenRouter models listed. Check OPENROUTER_API_KEY in PatchPilot config."
1659
- : "Codex OAuth is not ready. Run codex login."
2096
+ : settings.provider === "gemini-wrapper"
2097
+ ? "No Gemini-Wrapper models listed. Check gemini_webapi install and PATCHPILOT_GEMINI_WRAPPER_COOKIES_JSON in PatchPilot config."
2098
+ : settings.provider === "openrouter"
2099
+ ? "No OpenRouter models listed. Check OPENROUTER_API_KEY in PatchPilot config."
2100
+ : "Codex OAuth is not ready. Run codex login."
1660
2101
  });
1661
2102
  return null;
1662
2103
  }
@@ -1666,7 +2107,6 @@ function buildCommandSuggestionItems(options) {
1666
2107
  }
1667
2108
  const trimmedInput = options.input.trimStart().toLowerCase();
1668
2109
  const items = filterSlashCommands(options.input)
1669
- .slice(0, 6)
1670
2110
  .map((command) => {
1671
2111
  const baseCommand = `/${command.name}`;
1672
2112
  return {
@@ -1701,7 +2141,7 @@ function buildCommandSuggestionItems(options) {
1701
2141
  })));
1702
2142
  }
1703
2143
  }
1704
- if (trimmedInput === "/models" || trimmedInput.startsWith("/models") || trimmedInput === "/model" || trimmedInput.startsWith("/model")) {
2144
+ if (trimmedInput.startsWith("/models ") || trimmedInput.startsWith("/model ")) {
1705
2145
  const modelQuery = trimmedInput.replace(/^\/models?/, "").trim();
1706
2146
  if (options.isLoadingModels) {
1707
2147
  items.unshift({
@@ -1724,16 +2164,18 @@ function buildCommandSuggestionItems(options) {
1724
2164
  })));
1725
2165
  }
1726
2166
  }
1727
- return items.slice(0, 8);
2167
+ return items;
1728
2168
  }
1729
2169
  function getOnboardingOptionCount(onboarding) {
1730
2170
  switch (onboarding.step) {
1731
2171
  case "entry":
1732
- return 6;
2172
+ return 7;
1733
2173
  case "host":
1734
2174
  return onboarding.hosts.length + 1;
1735
2175
  case "api-key-choice":
1736
2176
  return onboarding.hasExistingKey ? 2 : 1;
2177
+ case "gemini-wrapper-model-mode":
2178
+ return 2;
1737
2179
  case "model":
1738
2180
  return onboarding.models.length;
1739
2181
  default:
@@ -1743,7 +2185,7 @@ function getOnboardingOptionCount(onboarding) {
1743
2185
  function readEntrySelection(value, selectedIndex) {
1744
2186
  const normalizedValue = value.trim().toLowerCase();
1745
2187
  if (!normalizedValue) {
1746
- return ["local", "host", "gemini", "openrouter", "nvidia", "codex"][selectedIndex];
2188
+ return ["local", "host", "gemini", "gemini-wrapper", "openrouter", "nvidia", "codex"][selectedIndex];
1747
2189
  }
1748
2190
  if (normalizedValue === "1" || normalizedValue === "local" || normalizedValue === "this device") {
1749
2191
  return "local";
@@ -1754,17 +2196,33 @@ function readEntrySelection(value, selectedIndex) {
1754
2196
  if (normalizedValue === "3" || normalizedValue === "gemini" || normalizedValue === "google") {
1755
2197
  return "gemini";
1756
2198
  }
1757
- if (normalizedValue === "4" || normalizedValue === "openrouter" || normalizedValue === "open-router") {
2199
+ if (normalizedValue === "4" || normalizedValue === "gemini-wrapper" || normalizedValue === "geminiwrapper" || normalizedValue === "google-wrapper") {
2200
+ return "gemini-wrapper";
2201
+ }
2202
+ if (normalizedValue === "5" || normalizedValue === "openrouter" || normalizedValue === "open-router") {
1758
2203
  return "openrouter";
1759
2204
  }
1760
- if (normalizedValue === "5" || normalizedValue === "nvidia" || normalizedValue === "nim") {
2205
+ if (normalizedValue === "6" || normalizedValue === "nvidia" || normalizedValue === "nim") {
1761
2206
  return "nvidia";
1762
2207
  }
1763
- if (normalizedValue === "6" || normalizedValue === "codex") {
2208
+ if (normalizedValue === "7" || normalizedValue === "codex") {
1764
2209
  return "codex";
1765
2210
  }
1766
2211
  return null;
1767
2212
  }
2213
+ function readBooleanEnv(value, fallback) {
2214
+ if (!value) {
2215
+ return fallback;
2216
+ }
2217
+ const normalizedValue = value.trim().toLowerCase();
2218
+ if (["1", "true", "yes", "on", "enabled"].includes(normalizedValue)) {
2219
+ return true;
2220
+ }
2221
+ if (["0", "false", "no", "off", "disabled"].includes(normalizedValue)) {
2222
+ return false;
2223
+ }
2224
+ return fallback;
2225
+ }
1768
2226
  function readIndexedSelection(value, selectedIndex) {
1769
2227
  const normalizedValue = value.trim();
1770
2228
  if (!normalizedValue) {
@@ -1797,6 +2255,9 @@ function selectModelFromInput(value, models, selectedIndex, options = {}) {
1797
2255
  function isPlausibleCloudModelId(value) {
1798
2256
  return /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(value) && value.length >= 3;
1799
2257
  }
2258
+ function canUseUnverifiedCloudModel(provider, model) {
2259
+ return provider !== "ollama" && provider !== "gemini-wrapper" && isPlausibleCloudModelId(model);
2260
+ }
1800
2261
  function defaultModelForProvider(provider, currentModel) {
1801
2262
  if (provider === "nvidia") {
1802
2263
  return currentModel.includes("/") && !currentModel.startsWith("openrouter/") ? currentModel : defaultNvidiaModel;
@@ -1804,6 +2265,9 @@ function defaultModelForProvider(provider, currentModel) {
1804
2265
  if (provider === "openrouter") {
1805
2266
  return currentModel.includes("/") ? currentModel : defaultOpenRouterModel;
1806
2267
  }
2268
+ if (provider === "gemini-wrapper") {
2269
+ return currentModel === defaultGeminiWrapperModel || currentModel.startsWith("gemini-3-") ? currentModel : defaultGeminiWrapperModel;
2270
+ }
1807
2271
  if (provider === "gemini") {
1808
2272
  return currentModel.startsWith("gemini-") ? currentModel : defaultGeminiModel;
1809
2273
  }
@@ -1821,12 +2285,15 @@ function openApiKeyChoice(provider, setOnboarding, setOnboardingIndex) {
1821
2285
  setOnboardingIndex(0);
1822
2286
  }
1823
2287
  function needsApiKey(provider) {
1824
- return provider === "gemini" || provider === "openrouter" || provider === "nvidia";
2288
+ return provider === "gemini" || provider === "gemini-wrapper" || provider === "openrouter" || provider === "nvidia";
1825
2289
  }
1826
2290
  function hasApiKey(provider) {
1827
2291
  if (provider === "gemini") {
1828
2292
  return Boolean(readGeminiApiKey());
1829
2293
  }
2294
+ if (provider === "gemini-wrapper") {
2295
+ return Boolean(readGeminiWrapperBaseUrl() || readGeminiWrapperCookiesJson());
2296
+ }
1830
2297
  if (provider === "openrouter") {
1831
2298
  return Boolean(readOpenRouterApiKey());
1832
2299
  }