@jx-grxf/patchpilot 0.3.0 → 0.4.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 (37) hide show
  1. package/README.md +17 -11
  2. package/SECURITY.md +3 -1
  3. package/dist/core/agent.d.ts +1 -0
  4. package/dist/core/agent.js +33 -6
  5. package/dist/core/agent.js.map +1 -1
  6. package/dist/core/gemini.js +27 -14
  7. package/dist/core/gemini.js.map +1 -1
  8. package/dist/core/nvidia.js +19 -1
  9. package/dist/core/nvidia.js.map +1 -1
  10. package/dist/core/openrouter.d.ts +2 -0
  11. package/dist/core/openrouter.js +51 -7
  12. package/dist/core/openrouter.js.map +1 -1
  13. package/dist/core/workspace.js +42 -21
  14. package/dist/core/workspace.js.map +1 -1
  15. package/dist/tui/App.js +111 -32
  16. package/dist/tui/App.js.map +1 -1
  17. package/dist/tui/commands.js +8 -2
  18. package/dist/tui/commands.js.map +1 -1
  19. package/dist/tui/components/ApprovalPanel.d.ts +6 -0
  20. package/dist/tui/components/ApprovalPanel.js +16 -0
  21. package/dist/tui/components/ApprovalPanel.js.map +1 -0
  22. package/dist/tui/components/Composer.d.ts +1 -0
  23. package/dist/tui/components/Composer.js +2 -2
  24. package/dist/tui/components/Composer.js.map +1 -1
  25. package/dist/tui/components/Header.js +17 -1
  26. package/dist/tui/components/Header.js.map +1 -1
  27. package/dist/tui/components/Sidebar.js +39 -20
  28. package/dist/tui/components/Sidebar.js.map +1 -1
  29. package/dist/tui/components/Transcript.js +3 -2
  30. package/dist/tui/components/Transcript.js.map +1 -1
  31. package/dist/tui/modes.d.ts +10 -0
  32. package/dist/tui/modes.js +37 -0
  33. package/dist/tui/modes.js.map +1 -0
  34. package/dist/tui/types.d.ts +1 -1
  35. package/docs/releases/v0.3.1-beta.md +19 -0
  36. package/docs/releases/v0.4.0.md +27 -0
  37. package/package.json +1 -1
package/dist/tui/App.js CHANGED
@@ -15,6 +15,7 @@ import { formatReasoningSupport } from "../core/reasoning.js";
15
15
  import { listWorkspaceSessions, loadSessionSummary, SessionStore } from "../core/session.js";
16
16
  import { addTelemetryToSession, emptySessionTelemetry, estimateTokens } from "../core/tokenAccounting.js";
17
17
  import { WorkspaceTools } from "../core/workspace.js";
18
+ import { ApprovalPanel } from "./components/ApprovalPanel.js";
18
19
  import { CommandSuggestions } from "./components/CommandSuggestions.js";
19
20
  import { Composer, FooterHints } from "./components/Composer.js";
20
21
  import { Header } from "./components/Header.js";
@@ -24,6 +25,7 @@ import { Transcript } from "./components/Transcript.js";
24
25
  import { filterSlashCommands, formatCommandDetail, formatCommandHelp } from "./commands.js";
25
26
  import { formatCost, formatSessionTokens, formatTokens, normalizeModelAlias, readToggle } from "./format.js";
26
27
  import { checkOllamaHost, discoverOllamaHosts, normalizeOllamaUrl, readOllamaHostDetails, startLocalOllamaAppAndWait } from "./hosts.js";
28
+ import { initialAgentMode, modeDescription, modePermissionLabel, nextAgentMode, permissionsForMode } from "./modes.js";
27
29
  import { selectableModels } from "./modelSelection.js";
28
30
  import { readGpuStats, readSystemStats } from "./systemStats.js";
29
31
  import { maxTranscriptLines } from "./types.js";
@@ -55,7 +57,8 @@ export function App(props) {
55
57
  const [sessionTelemetry, setSessionTelemetry] = useState(() => emptySessionTelemetry());
56
58
  const [systemStats, setSystemStats] = useState(() => readSystemStats().stats);
57
59
  const [gpuStats, setGpuStats] = useState(null);
58
- const [agentMode, setAgentMode] = useState(props.allowWrite || props.allowShell ? "build" : "plan");
60
+ const [agentMode, setAgentMode] = useState(() => initialAgentMode({ allowWrite: props.allowWrite, allowShell: props.allowShell }));
61
+ const [bypassConfirmation, setBypassConfirmation] = useState(false);
59
62
  const [hostOptions, setHostOptions] = useState([]);
60
63
  const [activeHost, setActiveHost] = useState(null);
61
64
  const [isLoadingHosts, setIsLoadingHosts] = useState(false);
@@ -101,7 +104,8 @@ export function App(props) {
101
104
  const paletteReservedHeight = !onboarding && paletteItems.length > 0 ? Math.min(8, paletteItems.length) + 4 : 0;
102
105
  const composerReservedHeight = onboarding ? 0 : 2;
103
106
  const footerReservedHeight = onboarding ? 0 : 1;
104
- const panelHeight = Math.max(8, rootHeight - headerReservedHeight - composerReservedHeight - paletteReservedHeight - footerReservedHeight);
107
+ const approvalReservedHeight = !onboarding && (pendingApproval || bypassConfirmation) ? 6 : 0;
108
+ const panelHeight = Math.max(8, rootHeight - headerReservedHeight - composerReservedHeight - paletteReservedHeight - footerReservedHeight - approvalReservedHeight);
105
109
  const transcriptWidth = Math.max(42, terminalColumns - 38);
106
110
  const scrollStep = Math.max(4, Math.floor(panelHeight * 0.8));
107
111
  const appendLine = useCallback((line) => {
@@ -132,23 +136,58 @@ export function App(props) {
132
136
  setPendingApproval(null);
133
137
  }, [appendLine, pendingApproval]);
134
138
  const applyMode = useCallback((nextMode, announce = true) => {
139
+ const permissions = permissionsForMode(nextMode);
135
140
  setAgentMode(nextMode);
141
+ setBypassConfirmation(false);
136
142
  setSettings((currentSettings) => ({
137
143
  ...currentSettings,
138
- allowWrite: nextMode === "build" ? grantedPermissionsRef.current.allowWrite : false,
139
- allowShell: nextMode === "build" ? grantedPermissionsRef.current.allowShell : false
144
+ allowWrite: permissions.allowWrite,
145
+ allowShell: permissions.allowShell
140
146
  }));
141
147
  if (announce) {
142
148
  appendLine({
143
- tone: "success",
149
+ tone: nextMode === "bypass" ? "warning" : "success",
144
150
  label: "mode",
145
- text: `${nextMode} mode ${nextMode === "plan" ? "keeps tools read-only" : "uses enabled write/shell permissions"}`
151
+ text: modeDescription(nextMode),
152
+ detail: nextMode === "plan"
153
+ ? "Read/search/status tools can still run. Writes, tests, scripts, and shell are denied."
154
+ : nextMode === "build"
155
+ ? "Risky tools can run only after allow once/session approval."
156
+ : "Use only in a trusted workspace. Path guards and destructive shell guards still apply."
146
157
  });
147
158
  }
148
159
  }, [appendLine]);
160
+ const requestBypassMode = useCallback(() => {
161
+ if (bypassConfirmation) {
162
+ return;
163
+ }
164
+ setBypassConfirmation(true);
165
+ setStatus("bypass confirmation needed");
166
+ setWorkState("waiting_approval");
167
+ }, [bypassConfirmation]);
168
+ const confirmBypassMode = useCallback(() => {
169
+ applyMode("bypass");
170
+ }, [applyMode]);
171
+ const cancelBypassMode = useCallback(() => {
172
+ setBypassConfirmation(false);
173
+ setStatus("idle");
174
+ setWorkState("idle");
175
+ applyMode("build", false);
176
+ appendLine({
177
+ kind: "approval",
178
+ tone: "warning",
179
+ label: "bypass",
180
+ text: "Bypass cancelled. Build mode still uses approvals."
181
+ });
182
+ }, [appendLine, applyMode]);
149
183
  const toggleMode = useCallback(() => {
150
- applyMode(agentMode === "plan" ? "build" : "plan");
151
- }, [agentMode, applyMode]);
184
+ const mode = nextAgentMode(agentMode);
185
+ if (mode === "bypass") {
186
+ requestBypassMode();
187
+ return;
188
+ }
189
+ applyMode(mode);
190
+ }, [agentMode, applyMode, requestBypassMode]);
152
191
  const loadHostSuggestions = useCallback(async (refresh = false, announce = false) => {
153
192
  if (isLoadingHosts) {
154
193
  return hostOptions;
@@ -598,7 +637,7 @@ export function App(props) {
598
637
  return;
599
638
  }
600
639
  const visibleModels = selectableModels(onboardingInput, onboarding.models);
601
- const selectedModel = selectModelFromInput(value, visibleModels, onboardingIndex, {
640
+ const selectedModel = visibleModels[onboardingIndex] ?? selectModelFromInput(value, visibleModels, onboardingIndex, {
602
641
  allowManual: onboarding.provider !== "ollama"
603
642
  });
604
643
  if (!selectedModel) {
@@ -657,6 +696,7 @@ export function App(props) {
657
696
  abortControllerRef.current = abortController;
658
697
  const taskRunner = new AgentRunner({
659
698
  ...runnableSettings,
699
+ mode: agentMode,
660
700
  signal: abortController.signal,
661
701
  sessionStore: sessionStoreRef.current,
662
702
  approvalHandler: (request) => new Promise((resolve) => {
@@ -674,9 +714,14 @@ export function App(props) {
674
714
  resolve("deny");
675
715
  return;
676
716
  }
717
+ if (agentMode === "bypass") {
718
+ resolve("allow_session");
719
+ return;
720
+ }
677
721
  setPendingApproval(request);
678
722
  setWorkState("waiting_approval");
679
723
  setStatus(`approval needed for ${request.tool}`);
724
+ setTranscriptScrollOffset(0);
680
725
  appendLine({
681
726
  kind: "approval",
682
727
  tone: "warning",
@@ -747,16 +792,21 @@ export function App(props) {
747
792
  return;
748
793
  case "build":
749
794
  case "plan":
795
+ case "bypass":
750
796
  case "mode": {
751
797
  const nextMode = command === "mode" ? args[0]?.toLowerCase() : command;
752
- if (nextMode !== "plan" && nextMode !== "build") {
798
+ if (nextMode !== "plan" && nextMode !== "build" && nextMode !== "bypass") {
753
799
  appendLine({
754
800
  tone: "accent",
755
801
  label: "mode",
756
- text: `current ${agentMode}. Use /mode plan, /mode build, or press tab.`
802
+ text: `current ${agentMode}. Use /mode plan, /mode build, /mode bypass, or press tab.`
757
803
  });
758
804
  return;
759
805
  }
806
+ if (nextMode === "bypass" && agentMode !== "bypass") {
807
+ requestBypassMode();
808
+ return;
809
+ }
760
810
  applyMode(nextMode);
761
811
  return;
762
812
  }
@@ -765,7 +815,8 @@ export function App(props) {
765
815
  appendLine({
766
816
  tone: "accent",
767
817
  label: "permissions",
768
- text: `write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | subagents ${settings.subagents ? "on" : "off"}`
818
+ text: `mode ${agentMode} | write ${modePermissionLabel(agentMode, "write")} | shell ${modePermissionLabel(agentMode, "shell")} | subagents ${settings.subagents ? "on" : "off"}`,
819
+ detail: modeDescription(agentMode)
769
820
  });
770
821
  return;
771
822
  case "provider": {
@@ -874,34 +925,30 @@ export function App(props) {
874
925
  case "apply": {
875
926
  const writeEnabled = readToggle(args[0], !settings.allowWrite);
876
927
  if (writeEnabled) {
877
- setAgentMode("build");
928
+ requestBypassMode();
929
+ return;
878
930
  }
879
931
  grantedPermissionsRef.current.allowWrite = writeEnabled;
880
- setSettings((currentSettings) => ({
881
- ...currentSettings,
882
- allowWrite: writeEnabled
883
- }));
932
+ applyMode("build", false);
884
933
  appendLine({
885
934
  tone: "success",
886
935
  label: "write",
887
- text: `workspace writes ${writeEnabled ? "enabled" : "disabled"}`
936
+ text: "workspace writes require approval in build mode"
888
937
  });
889
938
  return;
890
939
  }
891
940
  case "shell": {
892
941
  const shellEnabled = readToggle(args[0], !settings.allowShell);
893
942
  if (shellEnabled) {
894
- setAgentMode("build");
943
+ requestBypassMode();
944
+ return;
895
945
  }
896
946
  grantedPermissionsRef.current.allowShell = shellEnabled;
897
- setSettings((currentSettings) => ({
898
- ...currentSettings,
899
- allowShell: shellEnabled
900
- }));
947
+ applyMode("build", false);
901
948
  appendLine({
902
949
  tone: "success",
903
950
  label: "shell",
904
- text: `shell commands ${shellEnabled ? "enabled" : "disabled"}`
951
+ text: "shell commands require approval in build mode"
905
952
  });
906
953
  return;
907
954
  }
@@ -1005,8 +1052,8 @@ export function App(props) {
1005
1052
  tone: "accent",
1006
1053
  label: "status",
1007
1054
  text: settings.provider === "ollama"
1008
- ? `provider ollama | model ${settings.model} | host ${activeHost?.host.deviceName ?? settings.ollamaUrl} | route ${activeHost?.host.url ?? settings.ollamaUrl} | compute ${describeComputeTarget(settings.ollamaUrl).kind} | tools local | agents ${settings.subagents ? "on" : "off"} | write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | draft ${draftTokens} tok | last ${formatTokens(telemetry)} | session ${formatSessionTokens(sessionTelemetry)} | cost ${formatCost(sessionTelemetry.estimatedCostUsd)}`
1009
- : `provider ${settings.provider} | model ${settings.model} | host ${settings.provider} api | compute cloud | agents ${settings.subagents ? "on" : "off"} | think ${settings.thinkingMode} | reasoning ${formatReasoningSupport(settings.provider, settings.model, settings.reasoningEffort === "adaptive" ? undefined : settings.reasoningEffort)} | write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | draft ${draftTokens} tok | last ${formatTokens(telemetry)} | session ${formatSessionTokens(sessionTelemetry)} | cost ${formatCost(sessionTelemetry.estimatedCostUsd)}`
1055
+ ? `provider ollama | model ${settings.model} | host ${activeHost?.host.deviceName ?? settings.ollamaUrl} | route ${activeHost?.host.url ?? settings.ollamaUrl} | compute ${describeComputeTarget(settings.ollamaUrl).kind} | tools local | agents ${settings.subagents ? "on" : "off"} | mode ${agentMode} | write ${modePermissionLabel(agentMode, "write")} | shell ${modePermissionLabel(agentMode, "shell")} | draft ${draftTokens} tok | last ${formatTokens(telemetry)} | session ${formatSessionTokens(sessionTelemetry)} | cost ${formatCost(sessionTelemetry.estimatedCostUsd)}`
1056
+ : `provider ${settings.provider} | model ${settings.model} | host ${settings.provider} api | compute cloud | agents ${settings.subagents ? "on" : "off"} | think ${settings.thinkingMode} | reasoning ${formatReasoningSupport(settings.provider, settings.model, settings.reasoningEffort === "adaptive" ? undefined : settings.reasoningEffort)} | mode ${agentMode} | write ${modePermissionLabel(agentMode, "write")} | shell ${modePermissionLabel(agentMode, "shell")} | draft ${draftTokens} tok | last ${formatTokens(telemetry)} | session ${formatSessionTokens(sessionTelemetry)} | cost ${formatCost(sessionTelemetry.estimatedCostUsd)}`
1010
1057
  });
1011
1058
  return;
1012
1059
  case "sessions": {
@@ -1191,6 +1238,7 @@ export function App(props) {
1191
1238
  appendLine,
1192
1239
  applyMode,
1193
1240
  connectToHost,
1241
+ requestBypassMode,
1194
1242
  draftTokens,
1195
1243
  exit,
1196
1244
  hostOptions,
@@ -1204,7 +1252,14 @@ export function App(props) {
1204
1252
  ]);
1205
1253
  const handleSubmit = useCallback(async (value) => {
1206
1254
  const nextValue = value.trim();
1207
- if (!nextValue || isRunning) {
1255
+ if (!nextValue) {
1256
+ return;
1257
+ }
1258
+ if (isRunning && nextValue.startsWith("/")) {
1259
+ await handleSlashCommand(nextValue);
1260
+ return;
1261
+ }
1262
+ if (isRunning) {
1208
1263
  return;
1209
1264
  }
1210
1265
  if (onboarding) {
@@ -1319,6 +1374,21 @@ export function App(props) {
1319
1374
  }
1320
1375
  }, [hostOptions.length, input, isLoadingHosts, isLoadingModels, isRunning, loadHostSuggestions, loadProviderModels, modelOptions.length, onboarding, settings.provider]);
1321
1376
  useInput((inputValue, key) => {
1377
+ if (bypassConfirmation) {
1378
+ const normalizedInput = inputValue.toLowerCase();
1379
+ if (key.tab) {
1380
+ cancelBypassMode();
1381
+ return;
1382
+ }
1383
+ if (normalizedInput === "y") {
1384
+ confirmBypassMode();
1385
+ return;
1386
+ }
1387
+ if (normalizedInput === "n" || key.escape) {
1388
+ cancelBypassMode();
1389
+ return;
1390
+ }
1391
+ }
1322
1392
  if (pendingApproval) {
1323
1393
  const normalizedInput = inputValue.toLowerCase();
1324
1394
  if (normalizedInput === "y") {
@@ -1461,7 +1531,7 @@ export function App(props) {
1461
1531
  clearInterval(timer);
1462
1532
  };
1463
1533
  }, []);
1464
- 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 + 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 + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Transcript, { lines: lines, isRunning: isRunning, isActive: activeScrollPane === "transcript", height: panelHeight, width: transcriptWidth, scrollOffset: transcriptScrollOffset }), _jsx(Composer, { input: input, isRunning: isRunning, status: status, draftTokens: draftTokens, onChange: setInput, onSubmit: (value) => void handleSubmit(value) }), paletteItems.length > 0 ? _jsx(CommandSuggestions, { items: paletteItems, selectedIndex: paletteIndex }) : null, _jsx(FooterHints, { activePane: activeScrollPane })] })] }))] }));
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 })] })] }))] }));
1465
1535
  }
1466
1536
  async function loadAvailableModels(provider, ollamaUrl, setModelOptions, refresh = false) {
1467
1537
  const cacheKey = `${provider}:${provider === "ollama" ? ollamaUrl : "default"}`;
@@ -1507,7 +1577,7 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
1507
1577
  if (!installedModels) {
1508
1578
  return;
1509
1579
  }
1510
- if (!installedModels.includes(nextModel)) {
1580
+ if (!installedModels.includes(nextModel) && !(provider !== "ollama" && isPlausibleCloudModelId(nextModel))) {
1511
1581
  appendLine({
1512
1582
  tone: "warning",
1513
1583
  label: "model",
@@ -1534,9 +1604,10 @@ async function switchModel(provider, nextModel, ollamaUrl, currentModel, appendL
1534
1604
  PATCHPILOT_MODEL: nextModel
1535
1605
  });
1536
1606
  appendLine({
1537
- tone: "success",
1607
+ tone: installedModels.includes(nextModel) ? "success" : "warning",
1538
1608
  label: "model",
1539
- text: `switched to ${nextModel}`
1609
+ text: installedModels.includes(nextModel) ? `switched to ${nextModel}` : `switched to unverified ${provider} model ${nextModel}`,
1610
+ 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."
1540
1611
  });
1541
1612
  if (provider === "openrouter" && isOpenRouterFreeModel(nextModel)) {
1542
1613
  appendLine({
@@ -1562,7 +1633,15 @@ async function resolveRunnableSettings(settings, modelOptions, appendLine, setMo
1562
1633
  });
1563
1634
  return null;
1564
1635
  }
1565
- if (installedModels.includes(settings.model)) {
1636
+ if (installedModels.includes(settings.model) || (settings.provider !== "ollama" && isPlausibleCloudModelId(settings.model))) {
1637
+ if (!installedModels.includes(settings.model)) {
1638
+ appendLine({
1639
+ tone: "warning",
1640
+ label: "model",
1641
+ text: `using unverified ${settings.provider} model ${settings.model}`,
1642
+ detail: "Model discovery did not list it; the next provider request will be the compatibility check."
1643
+ });
1644
+ }
1566
1645
  return settings;
1567
1646
  }
1568
1647
  appendLine({