@jx-grxf/patchpilot 0.2.1 → 0.3.1-beta

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 (69) hide show
  1. package/README.md +70 -10
  2. package/SECURITY.md +20 -0
  3. package/dist/cli.js +52 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/agent.d.ts +6 -2
  6. package/dist/core/agent.js +197 -27
  7. package/dist/core/agent.js.map +1 -1
  8. package/dist/core/codex.js +1 -1
  9. package/dist/core/codex.js.map +1 -1
  10. package/dist/core/gemini.js +8 -21
  11. package/dist/core/gemini.js.map +1 -1
  12. package/dist/core/http.d.ts +6 -0
  13. package/dist/core/http.js +45 -0
  14. package/dist/core/http.js.map +1 -0
  15. package/dist/core/json.js +9 -0
  16. package/dist/core/json.js.map +1 -1
  17. package/dist/core/nvidia.js +9 -2
  18. package/dist/core/nvidia.js.map +1 -1
  19. package/dist/core/ollama.js +8 -1
  20. package/dist/core/ollama.js.map +1 -1
  21. package/dist/core/openrouter.js +13 -8
  22. package/dist/core/openrouter.js.map +1 -1
  23. package/dist/core/reasoning.d.ts +12 -0
  24. package/dist/core/reasoning.js +108 -0
  25. package/dist/core/reasoning.js.map +1 -0
  26. package/dist/core/session.d.ts +31 -0
  27. package/dist/core/session.js +154 -0
  28. package/dist/core/session.js.map +1 -0
  29. package/dist/core/types.d.ts +103 -2
  30. package/dist/core/workspace.d.ts +17 -1
  31. package/dist/core/workspace.js +498 -16
  32. package/dist/core/workspace.js.map +1 -1
  33. package/dist/tui/App.js +368 -109
  34. package/dist/tui/App.js.map +1 -1
  35. package/dist/tui/commands.js +45 -4
  36. package/dist/tui/commands.js.map +1 -1
  37. package/dist/tui/components/Composer.js +1 -1
  38. package/dist/tui/components/Composer.js.map +1 -1
  39. package/dist/tui/components/Header.d.ts +2 -2
  40. package/dist/tui/components/Header.js +32 -53
  41. package/dist/tui/components/Header.js.map +1 -1
  42. package/dist/tui/components/OnboardingPanel.d.ts +5 -0
  43. package/dist/tui/components/OnboardingPanel.js +11 -13
  44. package/dist/tui/components/OnboardingPanel.js.map +1 -1
  45. package/dist/tui/components/Sidebar.d.ts +6 -1
  46. package/dist/tui/components/Sidebar.js +33 -7
  47. package/dist/tui/components/Sidebar.js.map +1 -1
  48. package/dist/tui/components/Transcript.js +57 -8
  49. package/dist/tui/components/Transcript.js.map +1 -1
  50. package/dist/tui/hosts.js +7 -1
  51. package/dist/tui/hosts.js.map +1 -1
  52. package/dist/tui/modelSelection.d.ts +1 -0
  53. package/dist/tui/modelSelection.js +29 -0
  54. package/dist/tui/modelSelection.js.map +1 -0
  55. package/dist/tui/modes.d.ts +10 -0
  56. package/dist/tui/modes.js +37 -0
  57. package/dist/tui/modes.js.map +1 -0
  58. package/dist/tui/types.d.ts +13 -3
  59. package/dist/tui/types.js.map +1 -1
  60. package/docs/releases/v0.1.0.md +26 -0
  61. package/docs/releases/v0.2.0.md +21 -0
  62. package/docs/releases/v0.2.1.md +26 -0
  63. package/docs/releases/v0.3.0.md +26 -0
  64. package/docs/releases/v0.3.1-beta.md +19 -0
  65. package/docs/showcase/patchpilot-showcase.svg +83 -38
  66. package/package.json +5 -2
  67. package/dist/tui/inputRouting.d.ts +0 -8
  68. package/dist/tui/inputRouting.js +0 -94
  69. package/dist/tui/inputRouting.js.map +0 -1
package/dist/tui/App.js CHANGED
@@ -11,7 +11,10 @@ import { createModelClient } from "../core/modelClient.js";
11
11
  import { defaultNvidiaModel, readNvidiaApiKey } from "../core/nvidia.js";
12
12
  import { defaultOllamaModel, OllamaClient } from "../core/ollama.js";
13
13
  import { defaultOpenRouterModel, isOpenRouterFreeModel, readOpenRouterApiKey } from "../core/openrouter.js";
14
+ import { formatReasoningSupport } from "../core/reasoning.js";
15
+ import { listWorkspaceSessions, loadSessionSummary, SessionStore } from "../core/session.js";
14
16
  import { addTelemetryToSession, emptySessionTelemetry, estimateTokens } from "../core/tokenAccounting.js";
17
+ import { WorkspaceTools } from "../core/workspace.js";
15
18
  import { CommandSuggestions } from "./components/CommandSuggestions.js";
16
19
  import { Composer, FooterHints } from "./components/Composer.js";
17
20
  import { Header } from "./components/Header.js";
@@ -21,6 +24,8 @@ import { Transcript } from "./components/Transcript.js";
21
24
  import { filterSlashCommands, formatCommandDetail, formatCommandHelp } from "./commands.js";
22
25
  import { formatCost, formatSessionTokens, formatTokens, normalizeModelAlias, readToggle } from "./format.js";
23
26
  import { checkOllamaHost, discoverOllamaHosts, normalizeOllamaUrl, readOllamaHostDetails, startLocalOllamaAppAndWait } from "./hosts.js";
27
+ import { initialAgentMode, modeDescription, modePermissionLabel, nextAgentMode, permissionsForMode } from "./modes.js";
28
+ import { selectableModels } from "./modelSelection.js";
24
29
  import { readGpuStats, readSystemStats } from "./systemStats.js";
25
30
  import { maxTranscriptLines } from "./types.js";
26
31
  const modelCacheTtlMs = 5 * 60_000;
@@ -32,16 +37,27 @@ export function App(props) {
32
37
  const didRunInitialTask = useRef(false);
33
38
  const didOpenDefaultOnboarding = useRef(false);
34
39
  const abortControllerRef = useRef(null);
40
+ const sessionStoreRef = useRef(new SessionStore({ workspace: props.workspace }));
41
+ const approvalResolverRef = useRef(null);
42
+ const grantedPermissionsRef = useRef({
43
+ allowWrite: props.allowWrite,
44
+ allowShell: props.allowShell
45
+ });
46
+ const activeHostSyncInFlightRef = useRef(false);
47
+ const autoLoadKeysRef = useRef(new Set());
35
48
  const usedOllamaModelsRef = useRef(new Set());
36
49
  const [lines, setLines] = useState([]);
37
50
  const [advisorNotes, setAdvisorNotes] = useState([]);
38
51
  const [isRunning, setIsRunning] = useState(false);
39
52
  const [status, setStatus] = useState("idle");
53
+ const [workState, setWorkState] = useState("idle");
54
+ const [pendingApproval, setPendingApproval] = useState(null);
40
55
  const [telemetry, setTelemetry] = useState(null);
41
56
  const [sessionTelemetry, setSessionTelemetry] = useState(() => emptySessionTelemetry());
42
57
  const [systemStats, setSystemStats] = useState(() => readSystemStats().stats);
43
58
  const [gpuStats, setGpuStats] = useState(null);
44
- const [agentMode, setAgentMode] = useState(props.allowWrite || props.allowShell ? "build" : "plan");
59
+ const [agentMode, setAgentMode] = useState(() => initialAgentMode({ allowWrite: props.allowWrite, allowShell: props.allowShell }));
60
+ const [bypassConfirmation, setBypassConfirmation] = useState(false);
45
61
  const [hostOptions, setHostOptions] = useState([]);
46
62
  const [activeHost, setActiveHost] = useState(null);
47
63
  const [isLoadingHosts, setIsLoadingHosts] = useState(false);
@@ -51,6 +67,7 @@ export function App(props) {
51
67
  const [onboardingIndex, setOnboardingIndex] = useState(0);
52
68
  const [onboardingInput, setOnboardingInput] = useState("");
53
69
  const [onboardingBusyMessage, setOnboardingBusyMessage] = useState(null);
70
+ const [onboardingNotice, setOnboardingNotice] = useState(null);
54
71
  const [paletteIndex, setPaletteIndex] = useState(0);
55
72
  const [activeScrollPane, setActiveScrollPane] = useState("transcript");
56
73
  const [transcriptScrollOffset, setTranscriptScrollOffset] = useState(0);
@@ -82,7 +99,7 @@ export function App(props) {
82
99
  })
83
100
  : [];
84
101
  const rootHeight = Math.max(24, terminalRows);
85
- const headerReservedHeight = 8;
102
+ const headerReservedHeight = 5;
86
103
  const paletteReservedHeight = !onboarding && paletteItems.length > 0 ? Math.min(8, paletteItems.length) + 4 : 0;
87
104
  const composerReservedHeight = onboarding ? 0 : 2;
88
105
  const footerReservedHeight = onboarding ? 0 : 1;
@@ -94,28 +111,81 @@ export function App(props) {
94
111
  ...currentLines.slice(-maxTranscriptLines),
95
112
  {
96
113
  ...line,
114
+ kind: line.kind ?? defaultLogKind(line),
97
115
  id: Date.now() + Math.random()
98
116
  }
99
117
  ]);
100
118
  }, []);
119
+ const resolveApproval = useCallback((decision) => {
120
+ if (!pendingApproval || !approvalResolverRef.current) {
121
+ return;
122
+ }
123
+ approvalResolverRef.current(decision);
124
+ approvalResolverRef.current = null;
125
+ appendLine({
126
+ kind: "approval",
127
+ tone: decision === "deny" ? "warning" : "success",
128
+ label: "approval",
129
+ text: `${pendingApproval.tool} ${decision.replace("_", " ")}`,
130
+ detail: pendingApproval.preview,
131
+ workState: "waiting_approval",
132
+ tool: pendingApproval.tool
133
+ });
134
+ setPendingApproval(null);
135
+ }, [appendLine, pendingApproval]);
101
136
  const applyMode = useCallback((nextMode, announce = true) => {
137
+ const permissions = permissionsForMode(nextMode);
102
138
  setAgentMode(nextMode);
139
+ setBypassConfirmation(false);
103
140
  setSettings((currentSettings) => ({
104
141
  ...currentSettings,
105
- allowWrite: nextMode === "build" ? currentSettings.allowWrite : false,
106
- allowShell: nextMode === "build" ? currentSettings.allowShell : false
142
+ allowWrite: permissions.allowWrite,
143
+ allowShell: permissions.allowShell
107
144
  }));
108
145
  if (announce) {
109
146
  appendLine({
110
- tone: "success",
147
+ tone: nextMode === "bypass" ? "warning" : "success",
111
148
  label: "mode",
112
- text: `${nextMode} mode ${nextMode === "plan" ? "keeps tools read-only" : "uses enabled write/shell permissions"}`
149
+ text: modeDescription(nextMode),
150
+ detail: nextMode === "plan"
151
+ ? "Read/search/status tools can still run. Writes, tests, scripts, and shell are denied."
152
+ : nextMode === "build"
153
+ ? "Risky tools can run only after allow once/session approval."
154
+ : "Use only in a trusted workspace. Path guards and destructive shell guards still apply."
113
155
  });
114
156
  }
115
157
  }, [appendLine]);
158
+ const requestBypassMode = useCallback(() => {
159
+ setBypassConfirmation(true);
160
+ appendLine({
161
+ kind: "approval",
162
+ tone: "danger",
163
+ label: "bypass",
164
+ text: "Build + bypass will enable write and shell permissions without per-tool prompts.",
165
+ detail: "Press y to accept for this session, or n/Esc to stay in build mode. Use this only in a trusted workspace."
166
+ });
167
+ }, [appendLine]);
168
+ const confirmBypassMode = useCallback(() => {
169
+ applyMode("bypass");
170
+ }, [applyMode]);
171
+ const cancelBypassMode = useCallback(() => {
172
+ setBypassConfirmation(false);
173
+ applyMode("build", false);
174
+ appendLine({
175
+ kind: "approval",
176
+ tone: "warning",
177
+ label: "bypass",
178
+ text: "Bypass cancelled. Build mode still uses approvals."
179
+ });
180
+ }, [appendLine, applyMode]);
116
181
  const toggleMode = useCallback(() => {
117
- applyMode(agentMode === "plan" ? "build" : "plan");
118
- }, [agentMode, applyMode]);
182
+ const mode = nextAgentMode(agentMode);
183
+ if (mode === "bypass") {
184
+ requestBypassMode();
185
+ return;
186
+ }
187
+ applyMode(mode);
188
+ }, [agentMode, applyMode, requestBypassMode]);
119
189
  const loadHostSuggestions = useCallback(async (refresh = false, announce = false) => {
120
190
  if (isLoadingHosts) {
121
191
  return hostOptions;
@@ -217,6 +287,8 @@ export function App(props) {
217
287
  }, [appendLine, settings.model]);
218
288
  const openModelSelection = useCallback(async (provider, options = {}) => {
219
289
  setTelemetry(null);
290
+ setOnboardingInput("");
291
+ setOnboardingNotice(null);
220
292
  setOnboardingBusyMessage(`Loading ${provider} models...`);
221
293
  const nextModel = defaultModelForProvider(provider, options.currentModel ?? settings.model);
222
294
  setSettings((currentSettings) => ({
@@ -227,16 +299,16 @@ export function App(props) {
227
299
  try {
228
300
  const models = await loadAvailableModels(provider, options.ollamaUrl ?? settings.ollamaUrl, setModelOptions, true);
229
301
  if (models.length === 0) {
230
- appendLine({
302
+ setOnboardingNotice({
231
303
  tone: "warning",
232
- label: "onboarding",
233
304
  text: provider === "ollama"
234
305
  ? "No Ollama models found on that host."
235
306
  : provider === "gemini"
236
307
  ? "No Gemini models listed. Check the API key."
237
308
  : provider === "openrouter"
238
309
  ? "No OpenRouter models listed. Check the API key."
239
- : "No Codex OAuth models listed."
310
+ : "No Codex OAuth models listed.",
311
+ detail: "Use the back key to choose another provider or retry after fixing the provider setup."
240
312
  });
241
313
  return;
242
314
  }
@@ -249,21 +321,22 @@ export function App(props) {
249
321
  setOnboardingIndex(0);
250
322
  }
251
323
  catch (error) {
252
- appendLine({
324
+ setOnboardingNotice({
253
325
  tone: "danger",
254
- label: "onboarding",
255
- text: error instanceof Error ? error.message : String(error)
326
+ text: error instanceof Error ? error.message : String(error),
327
+ detail: "Fix the provider setup, then press Enter or go back and retry."
256
328
  });
257
329
  }
258
330
  finally {
259
331
  setOnboardingBusyMessage(null);
260
332
  }
261
- }, [appendLine, settings.model, settings.ollamaUrl]);
333
+ }, [settings.model, settings.ollamaUrl]);
262
334
  const closeOnboarding = useCallback(() => {
263
335
  setOnboarding(null);
264
336
  setOnboardingIndex(0);
265
337
  setOnboardingInput("");
266
338
  setOnboardingBusyMessage(null);
339
+ setOnboardingNotice(null);
267
340
  }, []);
268
341
  const goBackOnboarding = useCallback(() => {
269
342
  if (!onboarding) {
@@ -271,6 +344,7 @@ export function App(props) {
271
344
  }
272
345
  setOnboardingBusyMessage(null);
273
346
  setOnboardingInput("");
347
+ setOnboardingNotice(null);
274
348
  setOnboardingIndex(0);
275
349
  switch (onboarding.step) {
276
350
  case "entry":
@@ -327,6 +401,10 @@ export function App(props) {
327
401
  if (!onboarding) {
328
402
  return;
329
403
  }
404
+ if (onboardingBusyMessage) {
405
+ return;
406
+ }
407
+ setOnboardingNotice(null);
330
408
  if (onboarding.step === "entry") {
331
409
  const selection = readEntrySelection(value, onboardingIndex);
332
410
  if (!selection) {
@@ -343,7 +421,12 @@ export function App(props) {
343
421
  details = startedHost ? await connectToHost(startedHost, { announce: false }) : null;
344
422
  }
345
423
  if (!details) {
346
- setOnboardingBusyMessage("Local Ollama is not reachable. Start Ollama.app or run `ollama serve`, then press Enter again.");
424
+ setOnboardingBusyMessage(null);
425
+ setOnboardingNotice({
426
+ tone: "warning",
427
+ text: "Local Ollama is not reachable.",
428
+ detail: "Start Ollama.app or run `ollama serve`, then press Enter to retry."
429
+ });
347
430
  return;
348
431
  }
349
432
  await openModelSelection("ollama", {
@@ -394,9 +477,8 @@ export function App(props) {
394
477
  }
395
478
  const selectedHost = onboarding.hosts[selectionIndex - 1];
396
479
  if (!selectedHost) {
397
- appendLine({
480
+ setOnboardingNotice({
398
481
  tone: "warning",
399
- label: "onboarding",
400
482
  text: "Unknown host selection."
401
483
  });
402
484
  return;
@@ -407,6 +489,11 @@ export function App(props) {
407
489
  });
408
490
  if (!details) {
409
491
  setOnboardingBusyMessage(null);
492
+ setOnboardingNotice({
493
+ tone: "warning",
494
+ text: `No Ollama server answered at ${selectedHost.url}.`,
495
+ detail: "Check firewall, MagicDNS/IP, and whether Ollama is listening on that machine."
496
+ });
410
497
  return;
411
498
  }
412
499
  await openModelSelection("ollama", {
@@ -418,9 +505,8 @@ export function App(props) {
418
505
  if (onboarding.step === "host-input") {
419
506
  const hostValue = value.trim();
420
507
  if (!hostValue) {
421
- appendLine({
508
+ setOnboardingNotice({
422
509
  tone: "warning",
423
- label: "onboarding",
424
510
  text: "Host cannot be empty."
425
511
  });
426
512
  return;
@@ -431,6 +517,11 @@ export function App(props) {
431
517
  });
432
518
  if (!details) {
433
519
  setOnboardingBusyMessage(null);
520
+ setOnboardingNotice({
521
+ tone: "warning",
522
+ text: `No Ollama server answered at ${hostValue}.`,
523
+ detail: "Check the IP, MagicDNS name, firewall rules, and whether Ollama is running."
524
+ });
434
525
  return;
435
526
  }
436
527
  await openModelSelection("ollama", {
@@ -460,9 +551,8 @@ export function App(props) {
460
551
  if (onboarding.step === "gemini-key") {
461
552
  const apiKey = value.trim();
462
553
  if (!apiKey) {
463
- appendLine({
554
+ setOnboardingNotice({
464
555
  tone: "warning",
465
- label: "onboarding",
466
556
  text: "Gemini API key cannot be empty."
467
557
  });
468
558
  return;
@@ -473,9 +563,8 @@ export function App(props) {
473
563
  PATCHPILOT_MODEL: defaultGeminiModel,
474
564
  GEMINI_API_KEY: apiKey
475
565
  });
476
- appendLine({
566
+ setOnboardingNotice({
477
567
  tone: "success",
478
- label: "onboarding",
479
568
  text: "Gemini API key saved to PatchPilot config."
480
569
  });
481
570
  await openModelSelection("gemini", {
@@ -486,9 +575,8 @@ export function App(props) {
486
575
  if (onboarding.step === "openrouter-key") {
487
576
  const apiKey = value.trim();
488
577
  if (!apiKey) {
489
- appendLine({
578
+ setOnboardingNotice({
490
579
  tone: "warning",
491
- label: "onboarding",
492
580
  text: "OpenRouter API key cannot be empty."
493
581
  });
494
582
  return;
@@ -499,9 +587,8 @@ export function App(props) {
499
587
  PATCHPILOT_MODEL: defaultOpenRouterModel,
500
588
  OPENROUTER_API_KEY: apiKey
501
589
  });
502
- appendLine({
590
+ setOnboardingNotice({
503
591
  tone: "success",
504
- label: "onboarding",
505
592
  text: "OpenRouter API key saved to PatchPilot config."
506
593
  });
507
594
  await openModelSelection("openrouter", {
@@ -512,9 +599,8 @@ export function App(props) {
512
599
  if (onboarding.step === "nvidia-key") {
513
600
  const apiKey = value.trim();
514
601
  if (!apiKey) {
515
- appendLine({
602
+ setOnboardingNotice({
516
603
  tone: "warning",
517
- label: "onboarding",
518
604
  text: "NVIDIA API key cannot be empty."
519
605
  });
520
606
  return;
@@ -525,9 +611,8 @@ export function App(props) {
525
611
  PATCHPILOT_MODEL: defaultNvidiaModel,
526
612
  NVIDIA_API_KEY: apiKey
527
613
  });
528
- appendLine({
614
+ setOnboardingNotice({
529
615
  tone: "success",
530
- label: "onboarding",
531
616
  text: "NVIDIA API key saved to PatchPilot config."
532
617
  });
533
618
  await openModelSelection("nvidia", {
@@ -537,10 +622,10 @@ export function App(props) {
537
622
  }
538
623
  if (onboarding.step === "codex-login") {
539
624
  if (!hasCodexCliOAuth()) {
540
- appendLine({
625
+ setOnboardingNotice({
541
626
  tone: "warning",
542
- label: "onboarding",
543
- text: "Codex OAuth is still missing. Run `codex login`, then press Enter again."
627
+ text: "Codex OAuth is still missing.",
628
+ detail: "Run `codex login` in another terminal, then press Enter to retry."
544
629
  });
545
630
  return;
546
631
  }
@@ -549,14 +634,13 @@ export function App(props) {
549
634
  });
550
635
  return;
551
636
  }
552
- const selectableModels = filterModelOptions(onboardingInput, onboarding.models);
553
- const selectedModel = selectModelFromInput(value, selectableModels, onboardingIndex, {
637
+ const visibleModels = selectableModels(onboardingInput, onboarding.models);
638
+ const selectedModel = selectModelFromInput(value, visibleModels, onboardingIndex, {
554
639
  allowManual: onboarding.provider !== "ollama"
555
640
  });
556
641
  if (!selectedModel) {
557
- appendLine({
642
+ setOnboardingNotice({
558
643
  tone: "warning",
559
- label: "onboarding",
560
644
  text: "Unknown model selection. Pick a listed model."
561
645
  });
562
646
  return;
@@ -587,7 +671,7 @@ export function App(props) {
587
671
  });
588
672
  }
589
673
  closeOnboarding();
590
- }, [activeHost?.host.url, appendLine, closeOnboarding, connectToHost, loadHostSuggestions, onboarding, onboardingIndex, openModelSelection, settings.ollamaUrl]);
674
+ }, [activeHost?.host.url, appendLine, closeOnboarding, connectToHost, loadHostSuggestions, onboarding, onboardingBusyMessage, onboardingIndex, openModelSelection, settings.ollamaUrl]);
591
675
  const runTask = useCallback(async (task) => {
592
676
  if (!task.trim() || isRunning) {
593
677
  return;
@@ -596,6 +680,7 @@ export function App(props) {
596
680
  setTranscriptScrollOffset(0);
597
681
  setIsRunning(true);
598
682
  appendLine({
683
+ kind: "user",
599
684
  tone: "normal",
600
685
  label: "you",
601
686
  text: task
@@ -609,9 +694,46 @@ export function App(props) {
609
694
  abortControllerRef.current = abortController;
610
695
  const taskRunner = new AgentRunner({
611
696
  ...runnableSettings,
612
- signal: abortController.signal
697
+ mode: agentMode,
698
+ signal: abortController.signal,
699
+ sessionStore: sessionStoreRef.current,
700
+ approvalHandler: (request) => new Promise((resolve) => {
701
+ if (agentMode === "plan") {
702
+ appendLine({
703
+ kind: "approval",
704
+ tone: "warning",
705
+ label: "approval",
706
+ text: `${request.tool} blocked in plan mode`,
707
+ detail: "Switch to /mode build before approving write, script, test, or shell tools.",
708
+ workState: "waiting_approval",
709
+ tool: request.tool,
710
+ preview: request.preview
711
+ });
712
+ resolve("deny");
713
+ return;
714
+ }
715
+ if (agentMode === "bypass") {
716
+ resolve("allow_session");
717
+ return;
718
+ }
719
+ setPendingApproval(request);
720
+ setWorkState("waiting_approval");
721
+ setStatus(`approval needed for ${request.tool}`);
722
+ appendLine({
723
+ kind: "approval",
724
+ tone: "warning",
725
+ label: "approval",
726
+ text: `${request.tool} needs ${request.permission} approval`,
727
+ detail: `${request.preview} Press y once, a session, or n deny.`,
728
+ workState: "waiting_approval",
729
+ tool: request.tool,
730
+ preview: request.preview
731
+ });
732
+ approvalResolverRef.current = resolve;
733
+ })
613
734
  });
614
735
  for await (const event of taskRunner.run(task)) {
736
+ setWorkState(event.workState);
615
737
  if (event.type === "metrics") {
616
738
  if (runnableSettings.provider === "ollama") {
617
739
  usedOllamaModelsRef.current.add(`${runnableSettings.ollamaUrl}|${runnableSettings.model}`);
@@ -634,17 +756,20 @@ export function App(props) {
634
756
  }
635
757
  catch (error) {
636
758
  appendLine({
759
+ kind: "error",
637
760
  tone: "danger",
638
761
  label: "error",
639
- text: error instanceof Error ? error.message : String(error)
762
+ text: error instanceof Error ? error.message : String(error),
763
+ workState: "error"
640
764
  });
641
765
  }
642
766
  finally {
643
767
  abortControllerRef.current = null;
644
768
  setStatus("idle");
769
+ setWorkState("idle");
645
770
  setIsRunning(false);
646
771
  }
647
- }, [appendLine, isRunning, modelOptions, settings]);
772
+ }, [agentMode, appendLine, isRunning, modelOptions, settings]);
648
773
  const handleSlashCommand = useCallback(async (rawCommand) => {
649
774
  const [commandName = "", ...args] = rawCommand.slice(1).trim().split(/\s+/);
650
775
  const command = commandName.toLowerCase();
@@ -664,16 +789,21 @@ export function App(props) {
664
789
  return;
665
790
  case "build":
666
791
  case "plan":
792
+ case "bypass":
667
793
  case "mode": {
668
794
  const nextMode = command === "mode" ? args[0]?.toLowerCase() : command;
669
- if (nextMode !== "plan" && nextMode !== "build") {
795
+ if (nextMode !== "plan" && nextMode !== "build" && nextMode !== "bypass") {
670
796
  appendLine({
671
797
  tone: "accent",
672
798
  label: "mode",
673
- text: `current ${agentMode}. Use /mode plan, /mode build, or press tab.`
799
+ text: `current ${agentMode}. Use /mode plan, /mode build, /mode bypass, or press tab.`
674
800
  });
675
801
  return;
676
802
  }
803
+ if (nextMode === "bypass" && agentMode !== "bypass") {
804
+ requestBypassMode();
805
+ return;
806
+ }
677
807
  applyMode(nextMode);
678
808
  return;
679
809
  }
@@ -682,7 +812,8 @@ export function App(props) {
682
812
  appendLine({
683
813
  tone: "accent",
684
814
  label: "permissions",
685
- text: `write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | subagents ${settings.subagents ? "on" : "off"}`
815
+ text: `mode ${agentMode} | write ${modePermissionLabel(agentMode, "write")} | shell ${modePermissionLabel(agentMode, "shell")} | subagents ${settings.subagents ? "on" : "off"}`,
816
+ detail: modeDescription(agentMode)
686
817
  });
687
818
  return;
688
819
  case "provider": {
@@ -769,7 +900,7 @@ export function App(props) {
769
900
  appendLine({
770
901
  tone: "accent",
771
902
  label: "reasoning",
772
- text: `current ${settings.reasoningEffort}. Use /reasoning low, medium, high, xhigh, or adaptive.`
903
+ text: `current ${settings.reasoningEffort}. Use /reasoning none, low, medium, high, xhigh, or adaptive.`
773
904
  });
774
905
  return;
775
906
  }
@@ -783,7 +914,7 @@ export function App(props) {
783
914
  appendLine({
784
915
  tone: "success",
785
916
  label: "reasoning",
786
- text: `provider reasoning ${nextEffort}${settings.provider === "ollama" ? " (Ollama ignores common reasoning effort)" : ""}`
917
+ text: formatReasoningSupport(settings.provider, settings.model, nextEffort === "adaptive" ? undefined : nextEffort)
787
918
  });
788
919
  return;
789
920
  }
@@ -791,32 +922,30 @@ export function App(props) {
791
922
  case "apply": {
792
923
  const writeEnabled = readToggle(args[0], !settings.allowWrite);
793
924
  if (writeEnabled) {
794
- setAgentMode("build");
925
+ requestBypassMode();
926
+ return;
795
927
  }
796
- setSettings((currentSettings) => ({
797
- ...currentSettings,
798
- allowWrite: writeEnabled
799
- }));
928
+ grantedPermissionsRef.current.allowWrite = writeEnabled;
929
+ applyMode("build", false);
800
930
  appendLine({
801
931
  tone: "success",
802
932
  label: "write",
803
- text: `workspace writes ${writeEnabled ? "enabled" : "disabled"}`
933
+ text: "workspace writes require approval in build mode"
804
934
  });
805
935
  return;
806
936
  }
807
937
  case "shell": {
808
938
  const shellEnabled = readToggle(args[0], !settings.allowShell);
809
939
  if (shellEnabled) {
810
- setAgentMode("build");
940
+ requestBypassMode();
941
+ return;
811
942
  }
812
- setSettings((currentSettings) => ({
813
- ...currentSettings,
814
- allowShell: shellEnabled
815
- }));
943
+ grantedPermissionsRef.current.allowShell = shellEnabled;
944
+ applyMode("build", false);
816
945
  appendLine({
817
946
  tone: "success",
818
947
  label: "shell",
819
- text: `shell commands ${shellEnabled ? "enabled" : "disabled"}`
948
+ text: "shell commands require approval in build mode"
820
949
  });
821
950
  return;
822
951
  }
@@ -848,7 +977,7 @@ export function App(props) {
848
977
  tone: "warning",
849
978
  label: "model",
850
979
  text: `No unique model match for "${requestedModel}".`,
851
- detail: formatModelOptions(filterModelOptions(requestedModel, models).slice(0, 12), settings.model)
980
+ detail: formatModelOptions(selectableModels(requestedModel, models).slice(0, 12), settings.model)
852
981
  });
853
982
  return;
854
983
  }
@@ -897,6 +1026,8 @@ export function App(props) {
897
1026
  });
898
1027
  return;
899
1028
  }
1029
+ setInput("/models ");
1030
+ setPaletteIndex(0);
900
1031
  appendLine({
901
1032
  tone: "accent",
902
1033
  label: "models",
@@ -918,9 +1049,65 @@ export function App(props) {
918
1049
  tone: "accent",
919
1050
  label: "status",
920
1051
  text: settings.provider === "ollama"
921
- ? `provider ollama | model ${settings.model} | host ${activeHost?.host.deviceName ?? settings.ollamaUrl} | route ${activeHost?.host.url ?? settings.ollamaUrl} | compute ${describeComputeTarget(settings.ollamaUrl).kind} | tools local | agents ${settings.subagents ? "on" : "off"} | write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | draft ${draftTokens} tok | last ${formatTokens(telemetry)} | session ${formatSessionTokens(sessionTelemetry)} | cost ${formatCost(sessionTelemetry.estimatedCostUsd)}`
922
- : `provider ${settings.provider} | model ${settings.model} | host ${settings.provider} api | compute cloud | agents ${settings.subagents ? "on" : "off"} | think ${settings.thinkingMode} | reasoning ${settings.reasoningEffort} | write ${settings.allowWrite ? "on" : "off"} | shell ${settings.allowShell ? "on" : "off"} | draft ${draftTokens} tok | last ${formatTokens(telemetry)} | session ${formatSessionTokens(sessionTelemetry)} | cost ${formatCost(sessionTelemetry.estimatedCostUsd)}`
1052
+ ? `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)}`
1053
+ : `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)}`
1054
+ });
1055
+ return;
1056
+ case "sessions": {
1057
+ const sessions = await listWorkspaceSessions(settings.workspace);
1058
+ appendLine({
1059
+ kind: "status",
1060
+ tone: sessions.length > 0 ? "accent" : "muted",
1061
+ label: "sessions",
1062
+ text: sessions.length > 0 ? `Found ${sessions.length} workspace session${sessions.length === 1 ? "" : "s"}.` : "No workspace sessions yet.",
1063
+ detail: sessions
1064
+ .slice(0, 8)
1065
+ .map((session, index) => `${index + 1}. ${session.sessionId} ${session.updatedAt} ${session.lastTask ?? "no task"}`)
1066
+ .join("\n")
1067
+ });
1068
+ return;
1069
+ }
1070
+ case "resume": {
1071
+ const sessionId = args[0] ?? "";
1072
+ const sessions = await listWorkspaceSessions(settings.workspace);
1073
+ const selectedSession = sessionId ? await loadSessionSummary(settings.workspace, sessionId) : sessions[0] ?? null;
1074
+ appendLine({
1075
+ kind: "status",
1076
+ tone: selectedSession ? "accent" : "warning",
1077
+ label: "resume",
1078
+ text: selectedSession ? `Loaded session ${selectedSession.sessionId}` : "No session available to resume.",
1079
+ detail: selectedSession
1080
+ ? `workspace ${selectedSession.workspace}\nupdated ${selectedSession.updatedAt}\nmodel ${selectedSession.provider ?? "-"} ${selectedSession.model ?? "-"}\nlast task ${selectedSession.lastTask ?? "-"}`
1081
+ : "Run /sessions after at least one PatchPilot run."
1082
+ });
1083
+ return;
1084
+ }
1085
+ case "diff": {
1086
+ const result = await new WorkspaceTools({
1087
+ root: settings.workspace,
1088
+ allowWrite: false,
1089
+ allowShell: false
1090
+ }).execute({
1091
+ name: "git_diff",
1092
+ arguments: {}
923
1093
  });
1094
+ appendLine({
1095
+ kind: "diff",
1096
+ tone: result.ok ? "accent" : "warning",
1097
+ label: "diff",
1098
+ text: result.summary,
1099
+ detail: result.content,
1100
+ tool: "git_diff"
1101
+ });
1102
+ return;
1103
+ }
1104
+ case "approve": {
1105
+ const decision = args[0] === "session" ? "allow_session" : "allow_once";
1106
+ resolveApproval(decision);
1107
+ return;
1108
+ }
1109
+ case "deny":
1110
+ resolveApproval("deny");
924
1111
  return;
925
1112
  case "connect":
926
1113
  case "host":
@@ -940,6 +1127,8 @@ export function App(props) {
940
1127
  text: "Scanning LAN and Tailscale for Ollama hosts..."
941
1128
  });
942
1129
  await loadHostSuggestions(true, true);
1130
+ setInput("/connect ");
1131
+ setPaletteIndex(0);
943
1132
  return;
944
1133
  }
945
1134
  if (args.join(" ").trim().toLowerCase() === "local") {
@@ -965,6 +1154,8 @@ export function App(props) {
965
1154
  text: "Scanning LAN and Tailscale for Ollama hosts..."
966
1155
  });
967
1156
  await loadHostSuggestions(true, true);
1157
+ setInput("/connect ");
1158
+ setPaletteIndex(0);
968
1159
  return;
969
1160
  case "eject": {
970
1161
  if (settings.provider !== "ollama") {
@@ -1044,12 +1235,14 @@ export function App(props) {
1044
1235
  appendLine,
1045
1236
  applyMode,
1046
1237
  connectToHost,
1238
+ requestBypassMode,
1047
1239
  draftTokens,
1048
1240
  exit,
1049
1241
  hostOptions,
1050
1242
  loadHostSuggestions,
1051
1243
  loadProviderModels,
1052
1244
  modelOptions,
1245
+ resolveApproval,
1053
1246
  sessionTelemetry,
1054
1247
  settings,
1055
1248
  telemetry
@@ -1081,7 +1274,10 @@ export function App(props) {
1081
1274
  await runTask(nextValue);
1082
1275
  }, [handleOnboardingSubmit, handleSlashCommand, isRunning, onboarding, paletteIndex, paletteItems, runTask]);
1083
1276
  useEffect(() => {
1084
- if (!props.initialTask || didRunInitialTask.current || onboarding) {
1277
+ void sessionStoreRef.current.create();
1278
+ }, []);
1279
+ useEffect(() => {
1280
+ if (!props.initialTask || didRunInitialTask.current || onboarding || process.env.PATCHPILOT_ONBOARDING_COMPLETE !== "1") {
1085
1281
  return;
1086
1282
  }
1087
1283
  didRunInitialTask.current = true;
@@ -1091,7 +1287,7 @@ export function App(props) {
1091
1287
  setPaletteIndex(0);
1092
1288
  }, [hostOptions, input, modelOptions, onboarding, settings.model, settings.provider]);
1093
1289
  useEffect(() => {
1094
- if (didOpenDefaultOnboarding.current || props.initialTask || onboarding || process.env.PATCHPILOT_ONBOARDING_COMPLETE === "1") {
1290
+ if (didOpenDefaultOnboarding.current || onboarding || process.env.PATCHPILOT_ONBOARDING_COMPLETE === "1") {
1095
1291
  return;
1096
1292
  }
1097
1293
  didOpenDefaultOnboarding.current = true;
@@ -1109,6 +1305,10 @@ export function App(props) {
1109
1305
  }
1110
1306
  let cancelled = false;
1111
1307
  async function syncActiveHost() {
1308
+ if (activeHostSyncInFlightRef.current) {
1309
+ return;
1310
+ }
1311
+ activeHostSyncInFlightRef.current = true;
1112
1312
  const verifiedHost = await checkOllamaHost(settings.ollamaUrl, {
1113
1313
  timeoutMs: 800
1114
1314
  });
@@ -1116,6 +1316,7 @@ export function App(props) {
1116
1316
  if (!cancelled) {
1117
1317
  setActiveHost((currentHost) => (currentHost?.host.url === settings.ollamaUrl ? currentHost : null));
1118
1318
  }
1319
+ activeHostSyncInFlightRef.current = false;
1119
1320
  return;
1120
1321
  }
1121
1322
  const details = await readOllamaHostDetails(verifiedHost).catch(() => ({
@@ -1124,6 +1325,7 @@ export function App(props) {
1124
1325
  runningModels: [],
1125
1326
  fetchedAt: Date.now()
1126
1327
  }));
1328
+ activeHostSyncInFlightRef.current = false;
1127
1329
  if (cancelled) {
1128
1330
  return;
1129
1331
  }
@@ -1147,16 +1349,51 @@ export function App(props) {
1147
1349
  }
1148
1350
  const trimmedInput = input.trim();
1149
1351
  if (settings.provider === "ollama" && (trimmedInput === "/connect" || trimmedInput === "/hosts") && hostOptions.length === 0 && !isLoadingHosts) {
1150
- void loadHostSuggestions(false, false);
1352
+ const key = `${settings.provider}:${settings.ollamaUrl}:${trimmedInput}:hosts`;
1353
+ if (!autoLoadKeysRef.current.has(key)) {
1354
+ autoLoadKeysRef.current.add(key);
1355
+ void loadHostSuggestions(false, false);
1356
+ }
1151
1357
  }
1152
1358
  if ((trimmedInput === "/models" || trimmedInput === "/model") && modelOptions.length === 0 && !isLoadingModels) {
1153
- void loadProviderModels(false);
1359
+ const key = `${settings.provider}:${settings.ollamaUrl}:${trimmedInput}:models`;
1360
+ if (!autoLoadKeysRef.current.has(key)) {
1361
+ autoLoadKeysRef.current.add(key);
1362
+ void loadProviderModels(false);
1363
+ }
1154
1364
  }
1155
1365
  }, [hostOptions.length, input, isLoadingHosts, isLoadingModels, isRunning, loadHostSuggestions, loadProviderModels, modelOptions.length, onboarding, settings.provider]);
1156
1366
  useInput((inputValue, key) => {
1367
+ if (bypassConfirmation) {
1368
+ const normalizedInput = inputValue.toLowerCase();
1369
+ if (normalizedInput === "y") {
1370
+ confirmBypassMode();
1371
+ return;
1372
+ }
1373
+ if (normalizedInput === "n" || key.escape) {
1374
+ cancelBypassMode();
1375
+ return;
1376
+ }
1377
+ }
1378
+ if (pendingApproval) {
1379
+ const normalizedInput = inputValue.toLowerCase();
1380
+ if (normalizedInput === "y") {
1381
+ resolveApproval("allow_once");
1382
+ return;
1383
+ }
1384
+ if (normalizedInput === "a") {
1385
+ resolveApproval("allow_session");
1386
+ return;
1387
+ }
1388
+ if (normalizedInput === "n" || key.escape) {
1389
+ resolveApproval("deny");
1390
+ return;
1391
+ }
1392
+ }
1157
1393
  if (isRunning && key.escape) {
1158
1394
  abortControllerRef.current?.abort();
1159
1395
  appendLine({
1396
+ kind: "status",
1160
1397
  tone: "warning",
1161
1398
  label: "stop",
1162
1399
  text: "Stopping current task..."
@@ -1169,7 +1406,10 @@ export function App(props) {
1169
1406
  goBackOnboarding();
1170
1407
  return;
1171
1408
  }
1172
- const optionCount = onboarding.step === "model" ? filterModelOptions(onboardingInput, onboarding.models).length : getOnboardingOptionCount(onboarding);
1409
+ if (onboardingBusyMessage) {
1410
+ return;
1411
+ }
1412
+ const optionCount = onboarding.step === "model" ? selectableModels(onboardingInput, onboarding.models).length : getOnboardingOptionCount(onboarding);
1173
1413
  if (optionCount > 0 && key.upArrow) {
1174
1414
  setOnboardingIndex((currentIndex) => (currentIndex - 1 + optionCount) % optionCount);
1175
1415
  return;
@@ -1277,7 +1517,7 @@ export function App(props) {
1277
1517
  clearInterval(timer);
1278
1518
  };
1279
1519
  }, []);
1280
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: rootHeight, overflowY: "hidden", children: [_jsx(Header, { model: settings.model, provider: settings.provider, workspace: settings.workspace, status: status, allowWrite: settings.allowWrite, allowShell: settings.allowShell, agentMode: agentMode, subagents: settings.subagents, thinkingMode: settings.thinkingMode, reasoningEffort: settings.reasoningEffort, ollamaUrl: settings.ollamaUrl, telemetry: telemetry, sessionTelemetry: sessionTelemetry, draftTokens: draftTokens, systemStats: systemStats, gpuStats: gpuStats, activeHost: activeHost }), onboarding ? (_jsx(OnboardingPanel, { state: onboarding, height: panelHeight, selectedIndex: onboardingIndex, input: onboardingInput, busyMessage: onboardingBusyMessage, onInputChange: setOnboardingInput, onInputSubmit: (value) => void handleOnboardingSubmit(value) })) : (_jsxs(Box, { flexDirection: "row", height: panelHeight + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Sidebar, { workspace: settings.workspace, model: settings.model, provider: settings.provider, ollamaUrl: settings.ollamaUrl, agentMode: agentMode, allowWrite: settings.allowWrite, allowShell: settings.allowShell, subagents: settings.subagents, telemetry: telemetry, sessionTelemetry: sessionTelemetry, draftTokens: draftTokens, height: panelHeight, scrollOffset: sessionScrollOffset, advisors: advisorNotes, isActive: activeScrollPane === "session", activeHost: activeHost }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, height: panelHeight + composerReservedHeight + paletteReservedHeight + footerReservedHeight, overflowY: "hidden", children: [_jsx(Transcript, { lines: lines, isRunning: isRunning, isActive: activeScrollPane === "transcript", height: panelHeight, width: transcriptWidth, scrollOffset: transcriptScrollOffset }), _jsx(Composer, { input: input, isRunning: isRunning, status: status, draftTokens: draftTokens, onChange: setInput, onSubmit: (value) => void handleSubmit(value) }), paletteItems.length > 0 ? _jsx(CommandSuggestions, { items: paletteItems, selectedIndex: paletteIndex }) : null, _jsx(FooterHints, { activePane: activeScrollPane })] })] }))] }));
1520
+ 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 })] })] }))] }));
1281
1521
  }
1282
1522
  async function loadAvailableModels(provider, ollamaUrl, setModelOptions, refresh = false) {
1283
1523
  const cacheKey = `${provider}:${provider === "ollama" ? ollamaUrl : "default"}`;
@@ -1451,7 +1691,7 @@ function buildCommandSuggestionItems(options) {
1451
1691
  });
1452
1692
  }
1453
1693
  else {
1454
- items.unshift(...filterModelOptions(modelQuery, options.modelOptions).slice(0, 8).map((model) => ({
1694
+ items.unshift(...selectableModels(modelQuery, options.modelOptions).slice(0, 8).map((model) => ({
1455
1695
  key: `model-${model}`,
1456
1696
  category: "model",
1457
1697
  label: model,
@@ -1470,7 +1710,7 @@ function getOnboardingOptionCount(onboarding) {
1470
1710
  case "host":
1471
1711
  return onboarding.hosts.length + 1;
1472
1712
  case "api-key-choice":
1473
- return 2;
1713
+ return onboarding.hasExistingKey ? 2 : 1;
1474
1714
  case "model":
1475
1715
  return onboarding.models.length;
1476
1716
  default:
@@ -1525,7 +1765,7 @@ function selectModelFromInput(value, models, selectedIndex, options = {}) {
1525
1765
  if (models.includes(normalizedValue)) {
1526
1766
  return normalizedValue;
1527
1767
  }
1528
- const matches = filterModelOptions(normalizedValue, models);
1768
+ const matches = selectableModels(normalizedValue, models);
1529
1769
  if (matches.length === 1) {
1530
1770
  return matches[0] ?? null;
1531
1771
  }
@@ -1534,34 +1774,6 @@ function selectModelFromInput(value, models, selectedIndex, options = {}) {
1534
1774
  function isPlausibleCloudModelId(value) {
1535
1775
  return /^[A-Za-z0-9][A-Za-z0-9._:/+-]*$/.test(value) && value.length >= 3;
1536
1776
  }
1537
- function filterModelOptions(query, models) {
1538
- const normalizedQuery = query.trim().toLowerCase();
1539
- if (!normalizedQuery) {
1540
- return models;
1541
- }
1542
- return models
1543
- .map((model) => ({
1544
- model,
1545
- score: scoreModelMatch(model, normalizedQuery)
1546
- }))
1547
- .filter((item) => item.score !== null)
1548
- .sort((left, right) => left.score - right.score || left.model.localeCompare(right.model))
1549
- .map((item) => item.model);
1550
- }
1551
- function scoreModelMatch(model, query) {
1552
- const normalizedModel = model.toLowerCase();
1553
- if (normalizedModel === query) {
1554
- return 0;
1555
- }
1556
- if (normalizedModel.startsWith(query)) {
1557
- return 1;
1558
- }
1559
- if (normalizedModel.includes(query)) {
1560
- return 2 + normalizedModel.indexOf(query) / 1000;
1561
- }
1562
- const tokens = query.split(/[\s/:_-]+/).filter(Boolean);
1563
- return tokens.length > 0 && tokens.every((token) => normalizedModel.includes(token)) ? 10 : null;
1564
- }
1565
1777
  function defaultModelForProvider(provider, currentModel) {
1566
1778
  if (provider === "nvidia") {
1567
1779
  return currentModel.includes("/") && !currentModel.startsWith("openrouter/") ? currentModel : defaultNvidiaModel;
@@ -1633,7 +1845,7 @@ async function ejectOllamaModels(options) {
1633
1845
  return ejected;
1634
1846
  }
1635
1847
  function isReasoningEffort(value) {
1636
- return value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "adaptive";
1848
+ return value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "adaptive";
1637
1849
  }
1638
1850
  function upsertAdvisorNote(notes, nextNote) {
1639
1851
  const nextNotes = notes.filter((note) => note.role !== nextNote.role);
@@ -1643,46 +1855,75 @@ function eventToLine(event) {
1643
1855
  switch (event.type) {
1644
1856
  case "status":
1645
1857
  return {
1858
+ kind: "status",
1646
1859
  tone: "muted",
1647
- label: "thinking",
1648
- text: event.message
1860
+ label: event.workState,
1861
+ text: event.message,
1862
+ workState: event.workState
1649
1863
  };
1650
1864
  case "assistant":
1651
1865
  return {
1866
+ kind: "assistant",
1652
1867
  tone: "accent",
1653
1868
  label: "pilot",
1654
- text: event.message
1869
+ text: event.message,
1870
+ workState: event.workState
1655
1871
  };
1656
1872
  case "subagent":
1657
1873
  return {
1874
+ kind: "assistant",
1658
1875
  tone: "accent",
1659
1876
  label: event.role,
1660
1877
  text: "advisor brief updated",
1661
- detail: event.message
1878
+ detail: event.message,
1879
+ workState: event.workState
1662
1880
  };
1663
1881
  case "tool":
1664
1882
  return {
1883
+ kind: event.name === "git_diff" ? "diff" : "tool",
1665
1884
  tone: event.ok ? "success" : "warning",
1666
1885
  label: event.name,
1667
- text: event.summary
1886
+ text: event.summary,
1887
+ workState: event.workState,
1888
+ tool: event.name,
1889
+ toolCallId: event.toolCallId,
1890
+ category: event.category,
1891
+ preview: event.preview
1892
+ };
1893
+ case "approval":
1894
+ return {
1895
+ kind: "approval",
1896
+ tone: event.decision === "deny" ? "warning" : "success",
1897
+ label: "approval",
1898
+ text: `${event.request.tool} ${event.decision.replace("_", " ")}`,
1899
+ detail: event.request.preview,
1900
+ workState: event.workState,
1901
+ tool: event.request.tool,
1902
+ preview: event.request.preview
1668
1903
  };
1669
1904
  case "final":
1670
1905
  return {
1906
+ kind: "final",
1671
1907
  tone: "success",
1672
1908
  label: "final",
1673
- text: event.message
1909
+ text: event.message,
1910
+ workState: event.workState
1674
1911
  };
1675
1912
  case "error":
1676
1913
  return {
1914
+ kind: "error",
1677
1915
  tone: "danger",
1678
1916
  label: "error",
1679
- text: event.message
1917
+ text: event.message,
1918
+ workState: event.workState
1680
1919
  };
1681
1920
  case "metrics":
1682
1921
  return {
1922
+ kind: "status",
1683
1923
  tone: "muted",
1684
1924
  label: "metrics",
1685
- text: formatTokens(event.metrics)
1925
+ text: formatTokens(event.metrics),
1926
+ workState: event.workState
1686
1927
  };
1687
1928
  }
1688
1929
  }
@@ -1696,8 +1937,26 @@ function eventToStatus(event) {
1696
1937
  if (event.type === "subagent") {
1697
1938
  return `${event.role} subagent`;
1698
1939
  }
1940
+ if (event.type === "approval") {
1941
+ return `${event.request.tool}: ${event.decision.replace("_", " ")}`;
1942
+ }
1699
1943
  return event.type;
1700
1944
  }
1945
+ function defaultLogKind(line) {
1946
+ if (line.kind) {
1947
+ return line.kind;
1948
+ }
1949
+ if (line.label === "you") {
1950
+ return "user";
1951
+ }
1952
+ if (line.label === "error") {
1953
+ return "error";
1954
+ }
1955
+ if (line.label === "final") {
1956
+ return "final";
1957
+ }
1958
+ return "status";
1959
+ }
1701
1960
  function formatHostOptions(hosts) {
1702
1961
  return hosts
1703
1962
  .map((host, index) => {