@vetala/vetala 0.1.0-dev → 0.2.0-dev

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 (70) hide show
  1. package/README.md +25 -1
  2. package/dist/src/agent.d.ts +12 -1
  3. package/dist/src/agent.js +92 -14
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/app-meta.d.ts +3 -0
  6. package/dist/src/app-meta.js +4 -0
  7. package/dist/src/app-meta.js.map +1 -0
  8. package/dist/src/cli.js +28 -10
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/config.d.ts +10 -1
  11. package/dist/src/config.js +160 -67
  12. package/dist/src/config.js.map +1 -1
  13. package/dist/src/edit-history.d.ts +4 -0
  14. package/dist/src/edit-history.js +77 -0
  15. package/dist/src/edit-history.js.map +1 -0
  16. package/dist/src/edits/diff.d.ts +1 -0
  17. package/dist/src/edits/diff.js +176 -0
  18. package/dist/src/edits/diff.js.map +1 -0
  19. package/dist/src/ink/command-suggestions.js +1 -0
  20. package/dist/src/ink/command-suggestions.js.map +1 -1
  21. package/dist/src/ink/ink-terminal-ui.d.ts +2 -2
  22. package/dist/src/ink/ink-terminal-ui.js +7 -3
  23. package/dist/src/ink/ink-terminal-ui.js.map +1 -1
  24. package/dist/src/ink/repl-app.d.ts +3 -2
  25. package/dist/src/ink/repl-app.js +333 -111
  26. package/dist/src/ink/repl-app.js.map +1 -1
  27. package/dist/src/process-utils.d.ts +1 -0
  28. package/dist/src/process-utils.js +3 -1
  29. package/dist/src/process-utils.js.map +1 -1
  30. package/dist/src/providers/catalog.d.ts +29 -0
  31. package/dist/src/providers/catalog.js +121 -0
  32. package/dist/src/providers/catalog.js.map +1 -0
  33. package/dist/src/providers/index.d.ts +9 -0
  34. package/dist/src/providers/index.js +11 -0
  35. package/dist/src/providers/index.js.map +1 -0
  36. package/dist/src/providers/openai-compatible-client.d.ts +23 -0
  37. package/dist/src/providers/openai-compatible-client.js +183 -0
  38. package/dist/src/providers/openai-compatible-client.js.map +1 -0
  39. package/dist/src/repl.d.ts +2 -1
  40. package/dist/src/repl.js +1 -1
  41. package/dist/src/repl.js.map +1 -1
  42. package/dist/src/runtime-profile.d.ts +16 -0
  43. package/dist/src/runtime-profile.js +112 -0
  44. package/dist/src/runtime-profile.js.map +1 -0
  45. package/dist/src/search-provider.d.ts +6 -1
  46. package/dist/src/search-provider.js +257 -0
  47. package/dist/src/search-provider.js.map +1 -1
  48. package/dist/src/session-store.d.ts +5 -3
  49. package/dist/src/session-store.js +75 -4
  50. package/dist/src/session-store.js.map +1 -1
  51. package/dist/src/terminal-ui.d.ts +3 -1
  52. package/dist/src/terminal-ui.js +13 -3
  53. package/dist/src/terminal-ui.js.map +1 -1
  54. package/dist/src/tools/filesystem.js +141 -143
  55. package/dist/src/tools/filesystem.js.map +1 -1
  56. package/dist/src/tools/index.js +2 -0
  57. package/dist/src/tools/index.js.map +1 -1
  58. package/dist/src/tools/repo-search.d.ts +23 -0
  59. package/dist/src/tools/repo-search.js +221 -0
  60. package/dist/src/tools/repo-search.js.map +1 -0
  61. package/dist/src/tools/shell.js +26 -3
  62. package/dist/src/tools/shell.js.map +1 -1
  63. package/dist/src/tools/timing.d.ts +2 -0
  64. package/dist/src/tools/timing.js +58 -0
  65. package/dist/src/tools/timing.js.map +1 -0
  66. package/dist/src/tools/web.js +70 -5
  67. package/dist/src/tools/web.js.map +1 -1
  68. package/dist/src/types.d.ts +89 -17
  69. package/package.json +11 -1
  70. package/terminal.png +0 -0
@@ -4,17 +4,28 @@ import { Box, Text, useApp, useInput } from "ink";
4
4
  import SelectInput from "ink-select-input";
5
5
  import Spinner from "ink-spinner";
6
6
  import TextInput from "ink-text-input";
7
- import { Agent } from "../agent.js";
7
+ import { Agent, isAgentInterruptedError } from "../agent.js";
8
+ import { latestUndoableEdit, undoLastEdit } from "../edit-history.js";
8
9
  import { ApprovalManager } from "../approvals.js";
9
- import { clearSavedAuth, loadConfig, saveChatDefaults, savePersistentAuth, withSessionAuth, withStoredAuth } from "../config.js";
10
+ import { clearProviderSavedAuth, loadConfig, providerConfigFor, saveProviderDefaults, saveProviderPersistentAuth, withProviderSessionAuth, withProviderStoredAuth } from "../config.js";
10
11
  import { PathPolicy } from "../path-policy.js";
11
- import { SARVAM_MODELS } from "../sarvam/models.js";
12
+ import { getProviderDefinition, listProviders, providerLabel } from "../providers/index.js";
12
13
  import { SkillRuntime } from "../skills/runtime.js";
13
14
  import { createToolRegistry } from "../tools/index.js";
15
+ import { buildTranscriptCards } from "./transcript-cards.js";
14
16
  import { InkTerminalUI } from "./ink-terminal-ui.js";
15
17
  import { buildSlashSuggestions } from "./command-suggestions.js";
16
- import { buildTranscriptCards } from "./transcript-cards.js";
17
- export function ReplApp({ initialConfig, initialSession, store }) {
18
+ const ASSISTANT_FLUSH_INTERVAL_MS = 33;
19
+ const MAX_LIVE_ASSISTANT_LINES = 24;
20
+ const MAX_VISIBLE_TRANSCRIPT_TURNS = 6;
21
+ const UI_COLORS = {
22
+ accent: "blue",
23
+ muted: "gray",
24
+ border: "gray",
25
+ warning: "yellow",
26
+ danger: "red"
27
+ };
28
+ export function ReplApp({ initialConfig, initialSession, runtimeProfile, store }) {
18
29
  const { exit } = useApp();
19
30
  const [config, setConfig] = useState(initialConfig);
20
31
  const [session, setSession] = useState(() => cloneSession(initialSession));
@@ -26,18 +37,27 @@ export function ReplApp({ initialConfig, initialSession, store }) {
26
37
  const [spinnerLabel, setSpinnerLabel] = useState(null);
27
38
  const [status, setStatus] = useState("Ready");
28
39
  const [pendingApproval, setPendingApproval] = useState(null);
40
+ const [pendingBusyPrompt, setPendingBusyPrompt] = useState(null);
29
41
  const [modelPickerOpen, setModelPickerOpen] = useState(false);
42
+ const [pendingSarvamModelPicker, setPendingSarvamModelPicker] = useState(null);
43
+ const [pendingOpenRouterModelInput, setPendingOpenRouterModelInput] = useState(null);
30
44
  const [pendingReasoningSetup, setPendingReasoningSetup] = useState(null);
31
45
  const [pendingAuthInput, setPendingAuthInput] = useState(null);
32
46
  const [pendingAuthRetention, setPendingAuthRetention] = useState(null);
33
47
  const [availableSkills, setAvailableSkills] = useState([]);
34
48
  const [paused, setPaused] = useState(false);
35
49
  const [pendingExitConfirm, setPendingExitConfirm] = useState(false);
50
+ const [queuedPrompt, setQueuedPrompt] = useState(null);
51
+ const [turnRunning, setTurnRunning] = useState(false);
36
52
  const assistantBufferRef = useRef("");
53
+ const assistantFlushTimerRef = useRef(null);
37
54
  const nextEntryIdRef = useRef(0);
55
+ const queuedPromptRef = useRef(null);
38
56
  const sessionRef = useRef(session);
57
+ const turnRunningRef = useRef(false);
39
58
  const uiRef = useRef(null);
40
59
  const skillRuntimeRef = useRef(null);
60
+ const activeAgentRef = useRef(null);
41
61
  sessionRef.current = session;
42
62
  const pushEntry = (kind, text) => {
43
63
  setEntries((current) => [
@@ -49,7 +69,24 @@ export function ReplApp({ initialConfig, initialSession, store }) {
49
69
  }
50
70
  ]);
51
71
  };
72
+ const flushAssistantBuffer = () => {
73
+ if (assistantFlushTimerRef.current) {
74
+ clearTimeout(assistantFlushTimerRef.current);
75
+ assistantFlushTimerRef.current = null;
76
+ }
77
+ setAssistantBuffer(assistantBufferRef.current);
78
+ };
79
+ const scheduleAssistantFlush = () => {
80
+ if (assistantFlushTimerRef.current) {
81
+ return;
82
+ }
83
+ assistantFlushTimerRef.current = setTimeout(() => {
84
+ assistantFlushTimerRef.current = null;
85
+ setAssistantBuffer(assistantBufferRef.current);
86
+ }, ASSISTANT_FLUSH_INTERVAL_MS);
87
+ };
52
88
  const finalizeAssistant = () => {
89
+ flushAssistantBuffer();
53
90
  const buffered = assistantBufferRef.current.trimEnd();
54
91
  if (!buffered) {
55
92
  assistantBufferRef.current = "";
@@ -64,7 +101,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
64
101
  uiRef.current = new InkTerminalUI({
65
102
  appendAssistant: (text) => {
66
103
  assistantBufferRef.current += text;
67
- setAssistantBuffer(assistantBufferRef.current);
104
+ scheduleAssistantFlush();
68
105
  },
69
106
  finalizeAssistant,
70
107
  pushEntry,
@@ -75,7 +112,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
75
112
  setSpinnerLabel(label);
76
113
  setStatus(label ?? "Ready");
77
114
  }
78
- });
115
+ }, runtimeProfile);
79
116
  }
80
117
  const ui = uiRef.current;
81
118
  if (!skillRuntimeRef.current) {
@@ -85,17 +122,21 @@ export function ReplApp({ initialConfig, initialSession, store }) {
85
122
  });
86
123
  }
87
124
  const skills = skillRuntimeRef.current;
88
- const busy = spinnerLabel !== null;
89
- const transcriptCards = buildTranscriptCards(entries).slice(-8);
90
- const liveCardId = transcriptCards.at(-1)?.id ?? null;
91
125
  const visibleStatus = paused ? "Paused" : status;
126
+ const visibleAssistantBuffer = renderLiveAssistantBuffer(assistantBuffer);
127
+ const transcriptCards = buildTranscriptCards(entries);
128
+ const visibleTranscriptCards = transcriptCards.slice(-MAX_VISIBLE_TRANSCRIPT_TURNS);
129
+ const hiddenTranscriptTurnCount = Math.max(0, transcriptCards.length - visibleTranscriptCards.length);
92
130
  const slashSuggestions = buildSlashSuggestions(input, availableSkills).slice(0, 8);
93
131
  const showSlashSuggestions = Boolean(trusted &&
94
- !busy &&
132
+ !turnRunning &&
95
133
  !paused &&
96
134
  !pendingExitConfirm &&
97
135
  !pendingApproval &&
136
+ !pendingBusyPrompt &&
98
137
  !modelPickerOpen &&
138
+ !pendingSarvamModelPicker &&
139
+ !pendingOpenRouterModelInput &&
99
140
  !pendingReasoningSetup &&
100
141
  !pendingAuthInput &&
101
142
  !pendingAuthRetention &&
@@ -123,6 +164,11 @@ export function ReplApp({ initialConfig, initialSession, store }) {
123
164
  cancelled = true;
124
165
  };
125
166
  }, [skills]);
167
+ useEffect(() => () => {
168
+ if (assistantFlushTimerRef.current) {
169
+ clearTimeout(assistantFlushTimerRef.current);
170
+ }
171
+ }, []);
126
172
  useInput((inputValue, key) => {
127
173
  if (showSlashSuggestions && isTabInput(inputValue, key)) {
128
174
  const firstSuggestion = slashSuggestions[0];
@@ -154,41 +200,67 @@ export function ReplApp({ initialConfig, initialSession, store }) {
154
200
  return cloned;
155
201
  };
156
202
  const resetTranscript = () => {
203
+ flushAssistantBuffer();
157
204
  assistantBufferRef.current = "";
158
205
  setAssistantBuffer("");
159
206
  setActivityLabel(null);
160
207
  nextEntryIdRef.current = 0;
161
208
  setEntries([]);
162
209
  };
163
- const mergeLoadedConfig = (loaded) => {
164
- if (config.authSource === "session" && config.authValue) {
165
- return {
166
- ...loaded,
167
- authMode: config.authMode,
168
- authValue: config.authValue,
169
- authFingerprint: config.authFingerprint,
170
- authSource: "session"
171
- };
172
- }
173
- return loaded;
210
+ const mergeLoadedConfig = (loaded, skipProvider) => {
211
+ let merged = loaded;
212
+ for (const provider of listProviders()) {
213
+ if (provider.name === skipProvider) {
214
+ continue;
215
+ }
216
+ const currentProfile = config.providers[provider.name];
217
+ if (currentProfile.authSource === "session" &&
218
+ currentProfile.authValue &&
219
+ currentProfile.authMode !== "missing") {
220
+ merged = withProviderSessionAuth(merged, provider.name, currentProfile.authMode, currentProfile.authValue);
221
+ }
222
+ }
223
+ return merged;
174
224
  };
175
225
  const closeCommandModals = () => {
176
226
  setModelPickerOpen(false);
227
+ setPendingSarvamModelPicker(null);
228
+ setPendingOpenRouterModelInput(null);
177
229
  setPendingReasoningSetup(null);
178
230
  setPendingAuthInput(null);
179
231
  setPendingAuthRetention(null);
180
232
  };
181
- const runPrompt = async (prompt) => {
233
+ const setQueuedPromptState = (prompt) => {
234
+ queuedPromptRef.current = prompt;
235
+ setQueuedPrompt(prompt);
236
+ };
237
+ const setTurnRunningState = (value) => {
238
+ turnRunningRef.current = value;
239
+ setTurnRunning(value);
240
+ };
241
+ const beginQueuedPrompt = (prompt) => {
242
+ setQueuedPromptState(null);
243
+ setPendingBusyPrompt(null);
244
+ void runPrompt(prompt, { forceRun: true });
245
+ };
246
+ const runPrompt = async (prompt, options = {}) => {
182
247
  const trimmed = prompt.trim();
183
248
  if (!trimmed) {
184
249
  return;
185
250
  }
251
+ if (turnRunningRef.current && !options.forceRun) {
252
+ setPendingBusyPrompt({ prompt: trimmed });
253
+ setInput("");
254
+ return;
255
+ }
186
256
  if (trimmed.startsWith("/")) {
257
+ setInput("");
187
258
  await handleCommand(trimmed);
188
259
  return;
189
260
  }
190
261
  setInput("");
191
262
  setPendingExitConfirm(false);
263
+ setPendingBusyPrompt(null);
192
264
  setActivityLabel(null);
193
265
  pushEntry("user", summarizeUserPrompt(trimmed));
194
266
  setStatus("Running agent");
@@ -199,23 +271,40 @@ export function ReplApp({ initialConfig, initialSession, store }) {
199
271
  sessionStore: store,
200
272
  approvals,
201
273
  pathPolicy: new PathPolicy(session.workspaceRoot, approvals),
274
+ runtimeProfile,
202
275
  skills,
203
276
  tools: createTools(),
204
277
  ui
205
278
  });
279
+ activeAgentRef.current = agent;
280
+ setTurnRunningState(true);
206
281
  try {
207
282
  await agent.runTurn(trimmed, true);
208
283
  syncSession(session);
209
284
  setActivityLabel(null);
210
- setStatus("Ready");
285
+ setStatus(queuedPromptRef.current ? "Running queued prompt" : "Ready");
211
286
  }
212
287
  catch (error) {
288
+ if (isAgentInterruptedError(error)) {
289
+ setActivityLabel(null);
290
+ syncSession(session);
291
+ setStatus(queuedPromptRef.current ? "Running queued prompt" : "Interrupted");
292
+ return;
293
+ }
213
294
  finalizeAssistant();
214
295
  setActivityLabel(null);
215
296
  pushEntry("error", error instanceof Error ? error.message : String(error));
216
297
  setStatus("Failed");
217
298
  syncSession(session);
218
299
  }
300
+ finally {
301
+ activeAgentRef.current = null;
302
+ setTurnRunningState(false);
303
+ const nextQueuedPrompt = queuedPromptRef.current;
304
+ if (nextQueuedPrompt) {
305
+ beginQueuedPrompt(nextQueuedPrompt);
306
+ }
307
+ }
219
308
  };
220
309
  const handleCommand = async (commandLine) => {
221
310
  const [command, ...args] = commandLine.slice(1).split(/\s+/);
@@ -227,6 +316,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
227
316
  pushEntry("info", [
228
317
  "/help",
229
318
  "/model",
319
+ "/undo",
230
320
  "/skill",
231
321
  "/tools",
232
322
  "/history",
@@ -245,8 +335,18 @@ export function ReplApp({ initialConfig, initialSession, store }) {
245
335
  }
246
336
  closeCommandModals();
247
337
  setModelPickerOpen(true);
248
- setStatus("Select a model");
338
+ setStatus("Select a provider and model");
339
+ return;
340
+ case "undo": {
341
+ const result = await undoLastEdit(session, store, (request) => {
342
+ const approvals = new ApprovalManager(session, store, null, requestApprovalDecision);
343
+ return approvals.requestApproval(request);
344
+ });
345
+ syncSession(session);
346
+ pushEntry(result.isError ? "warn" : "info", result.content);
347
+ setStatus(result.isError ? "Undo blocked" : "Ready");
249
348
  return;
349
+ }
250
350
  case "skill":
251
351
  case "skills":
252
352
  await handleSkillsCommand(args);
@@ -282,7 +382,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
282
382
  return;
283
383
  }
284
384
  case "new": {
285
- const nextSession = await store.createSession(session.workspaceRoot, session.model);
385
+ const nextSession = await store.createSession(session.workspaceRoot, session.provider, session.model);
286
386
  const nextCloned = syncSession(nextSession);
287
387
  closeCommandModals();
288
388
  resetTranscript();
@@ -300,22 +400,22 @@ export function ReplApp({ initialConfig, initialSession, store }) {
300
400
  ui.printConfig(config);
301
401
  return;
302
402
  case "logout": {
303
- const previousAuthSource = config.authSource;
304
- await clearSavedAuth();
403
+ const previousProfile = providerConfigFor(config, session.provider);
404
+ await clearProviderSavedAuth(session.provider);
305
405
  closeCommandModals();
306
- const reloaded = await loadConfig();
406
+ const reloaded = mergeLoadedConfig(await loadConfig(), session.provider);
307
407
  setConfig(reloaded);
308
- if (reloaded.authSource === "env") {
309
- pushEntry("warn", "Cleared local auth state, but environment credentials are still active for this process.");
408
+ if (providerConfigFor(reloaded, session.provider).authSource === "env") {
409
+ pushEntry("warn", `Cleared local ${providerLabel(session.provider)} auth state, but environment credentials are still active for this process.`);
310
410
  }
311
- else if (previousAuthSource === "stored" || previousAuthSource === "stored_hash") {
312
- pushEntry("info", "Cleared the saved API key for future launches and removed current local auth.");
411
+ else if (previousProfile.authSource === "stored" || previousProfile.authSource === "stored_hash") {
412
+ pushEntry("info", `Cleared the saved ${providerLabel(session.provider)} credential for future launches.`);
313
413
  }
314
- else if (previousAuthSource === "session") {
315
- pushEntry("info", "Cleared the API key that was only active in this session.");
414
+ else if (previousProfile.authSource === "session") {
415
+ pushEntry("info", `Cleared the ${providerLabel(session.provider)} credential that was only active in this session.`);
316
416
  }
317
417
  else {
318
- pushEntry("info", "No saved API key remained after logout.");
418
+ pushEntry("info", `No saved ${providerLabel(session.provider)} credential remained after logout.`);
319
419
  }
320
420
  setStatus("Logged out");
321
421
  return;
@@ -423,6 +523,51 @@ export function ReplApp({ initialConfig, initialSession, store }) {
423
523
  setPendingApproval(null);
424
524
  promptState.resolve(value);
425
525
  };
526
+ const onBusyPromptSelect = async (choice) => {
527
+ const current = pendingBusyPrompt;
528
+ if (!current) {
529
+ return;
530
+ }
531
+ if (choice === "cancel") {
532
+ setPendingBusyPrompt(null);
533
+ setInput(current.prompt);
534
+ return;
535
+ }
536
+ const replacedExistingQueue = queuedPromptRef.current !== null;
537
+ setQueuedPromptState(current.prompt);
538
+ setPendingBusyPrompt(null);
539
+ pushEntry("info", choice === "force"
540
+ ? `Stopping the current turn and sending next: ${summarizeUserPrompt(current.prompt)}`
541
+ : `${replacedExistingQueue ? "Replaced" : "Queued"} next prompt: ${summarizeUserPrompt(current.prompt)}`);
542
+ if (choice === "force") {
543
+ setStatus("Stopping current turn");
544
+ activeAgentRef.current?.requestStop();
545
+ return;
546
+ }
547
+ setStatus("Queued next prompt");
548
+ };
549
+ const applyModelSelection = async (nextSettings) => {
550
+ const definition = getProviderDefinition(nextSettings.provider);
551
+ await store.updateModel(session, nextSettings.provider, nextSettings.model);
552
+ await saveProviderDefaults(nextSettings.provider, nextSettings.model, definition.supportsReasoningEffort
553
+ ? { reasoningEffort: nextSettings.reasoningEffort }
554
+ : {});
555
+ syncSession(session);
556
+ const nextConfig = mergeLoadedConfig(await loadConfig());
557
+ setConfig(nextConfig);
558
+ const nextProfile = providerConfigFor(nextConfig, nextSettings.provider);
559
+ if (nextProfile.authSource === "missing" || nextProfile.authSource === "stored_hash") {
560
+ setPendingAuthInput({
561
+ ...nextSettings,
562
+ authMode: definition.auth.defaultMode,
563
+ value: ""
564
+ });
565
+ setStatus(`Enter ${definition.auth.inputLabel.toLowerCase()} for ${providerLabel(nextSettings.provider)} / ${nextSettings.model}`);
566
+ return;
567
+ }
568
+ pushEntry("info", formatModelSetupSummary(nextSettings, nextConfig));
569
+ setStatus("Ready");
570
+ };
426
571
  const onModelSelect = async (value) => {
427
572
  if (value === "cancel") {
428
573
  setModelPickerOpen(false);
@@ -430,11 +575,30 @@ export function ReplApp({ initialConfig, initialSession, store }) {
430
575
  return;
431
576
  }
432
577
  setModelPickerOpen(false);
578
+ if (value === "sarvam") {
579
+ setPendingSarvamModelPicker("sarvam");
580
+ setStatus("Select a Sarvam model");
581
+ return;
582
+ }
583
+ setPendingOpenRouterModelInput({
584
+ provider: "openrouter",
585
+ value: session.provider === "openrouter" ? session.model : providerConfigFor(config, "openrouter").defaultModel
586
+ });
587
+ setStatus("Enter an OpenRouter model id");
588
+ };
589
+ const onSarvamModelSelect = async (value) => {
590
+ if (value === "cancel") {
591
+ setPendingSarvamModelPicker(null);
592
+ setStatus("Ready");
593
+ return;
594
+ }
595
+ setPendingSarvamModelPicker(null);
433
596
  setPendingReasoningSetup({
597
+ provider: "sarvam",
434
598
  model: value,
435
599
  reasoningEffort: config.reasoningEffort
436
600
  });
437
- setStatus(`Select reasoning effort for ${value}`);
601
+ setStatus(`Select reasoning effort for Sarvam / ${value}`);
438
602
  };
439
603
  const onReasoningSelect = async (value) => {
440
604
  const current = pendingReasoningSetup;
@@ -451,23 +615,28 @@ export function ReplApp({ initialConfig, initialSession, store }) {
451
615
  ...current,
452
616
  reasoningEffort: value === "none" ? null : value
453
617
  };
454
- await store.updateModel(session, nextSettings.model);
455
- await saveChatDefaults(nextSettings.model, nextSettings.reasoningEffort);
456
- syncSession(session);
457
- const loadedConfig = await loadConfig();
458
- const nextConfig = mergeLoadedConfig(loadedConfig);
459
- setConfig(nextConfig);
460
- if (nextConfig.authSource === "missing" || nextConfig.authSource === "stored_hash") {
461
- setPendingAuthInput({
462
- ...nextSettings,
463
- authMode: "subscription_key",
464
- value: ""
465
- });
466
- setStatus(`Enter API key for ${nextSettings.model}`);
618
+ await applyModelSelection(nextSettings);
619
+ };
620
+ const onOpenRouterModelInputChange = (value) => {
621
+ setPendingOpenRouterModelInput((current) => (current ? { ...current, value } : current));
622
+ };
623
+ const onOpenRouterModelInputSubmit = async (value) => {
624
+ const trimmed = value.trim();
625
+ if (!pendingOpenRouterModelInput) {
467
626
  return;
468
627
  }
469
- pushEntry("info", formatModelSetupSummary(nextSettings, nextConfig));
470
- setStatus("Ready");
628
+ if (!trimmed) {
629
+ setPendingOpenRouterModelInput(null);
630
+ pushEntry("warn", "OpenRouter model selection cancelled.");
631
+ setStatus("Ready");
632
+ return;
633
+ }
634
+ setPendingOpenRouterModelInput(null);
635
+ await applyModelSelection({
636
+ provider: "openrouter",
637
+ model: trimmed,
638
+ reasoningEffort: null
639
+ });
471
640
  };
472
641
  const onAuthInputChange = (value) => {
473
642
  setPendingAuthInput((current) => (current ? { ...current, value } : current));
@@ -480,7 +649,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
480
649
  const trimmed = value.trim();
481
650
  if (!trimmed) {
482
651
  setPendingAuthInput(null);
483
- pushEntry("warn", "API key entry cancelled. Model settings were saved, but no usable Sarvam credential is active.");
652
+ pushEntry("warn", `Credential entry cancelled. Model settings were saved, but no usable ${providerLabel(current.provider)} credential is active.`);
484
653
  setStatus("Ready");
485
654
  return;
486
655
  }
@@ -489,7 +658,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
489
658
  ...current,
490
659
  value: trimmed
491
660
  });
492
- setStatus("Choose how long to keep this API key");
661
+ setStatus("Choose how long to keep this credential");
493
662
  };
494
663
  const onAuthRetentionSelect = async (choice) => {
495
664
  const current = pendingAuthRetention;
@@ -504,15 +673,15 @@ export function ReplApp({ initialConfig, initialSession, store }) {
504
673
  }
505
674
  const loadedConfig = await loadConfig();
506
675
  const nextConfig = choice === "persist"
507
- ? withStoredAuth(loadedConfig, current.authMode, current.value)
508
- : withSessionAuth(loadedConfig, current.authMode, current.value);
676
+ ? withProviderStoredAuth(loadedConfig, current.provider, current.authMode, current.value)
677
+ : withProviderSessionAuth(loadedConfig, current.provider, current.authMode, current.value);
509
678
  if (choice === "persist") {
510
- await savePersistentAuth(current.authMode, current.value);
679
+ await saveProviderPersistentAuth(current.provider, current.authMode, current.value);
511
680
  }
512
681
  setConfig(nextConfig);
513
682
  setPendingAuthRetention(null);
514
683
  pushEntry("info", formatModelSetupSummary(current, nextConfig, choice));
515
- if (choice === "persist" && loadedConfig.authSource === "env") {
684
+ if (choice === "persist" && providerConfigFor(loadedConfig, current.provider).authSource === "env") {
516
685
  pushEntry("warn", "Environment credentials are still set in this shell. They may take precedence on future launches.");
517
686
  }
518
687
  setStatus("Ready");
@@ -524,51 +693,68 @@ export function ReplApp({ initialConfig, initialSession, store }) {
524
693
  }
525
694
  setPendingExitConfirm(false);
526
695
  };
527
- return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: !trusted ? (_jsxs(_Fragment, { children: [_jsx(TrustScreen, { workspaceRoot: session.workspaceRoot, onSelect: onTrustSelect }), pendingExitConfirm ? _jsx(ExitConfirmBox, { onSelect: onExitConfirmSelect }) : null] })) : (_jsxs(_Fragment, { children: [_jsx(Dashboard, { config: config, session: session, status: visibleStatus }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [transcriptCards.length === 0 && !assistantBuffer && !spinnerLabel ? (_jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", children: "New transcript. Use /history if you want earlier session messages." }) })) : null, transcriptCards.map((card) => (_jsx(TranscriptCard, { card: card, assistantBuffer: card.id === liveCardId ? assistantBuffer : "", liveLabel: card.id === liveCardId ? activityLabel ?? spinnerLabel : null }, card.id))), transcriptCards.length === 0 && (assistantBuffer || activityLabel || spinnerLabel) ? (_jsx(LiveStatusCard, { assistantBuffer: assistantBuffer, liveLabel: activityLabel ?? spinnerLabel })) : null] }), pendingExitConfirm ? (_jsx(ExitConfirmBox, { onSelect: onExitConfirmSelect })) : paused ? (_jsx(PauseBox, {})) : pendingApproval ? (_jsx(ApprovalBox, { request: pendingApproval.request, onSelect: onApprovalSelect })) : modelPickerOpen ? (_jsx(ModelPicker, { currentModel: session.model, onSelect: onModelSelect })) : pendingReasoningSetup ? (_jsx(ReasoningEffortPicker, { currentValue: pendingReasoningSetup.reasoningEffort, model: pendingReasoningSetup.model, onSelect: onReasoningSelect })) : pendingAuthInput ? (_jsx(AuthInputBox, { state: pendingAuthInput, onChange: onAuthInputChange, onSubmit: onAuthInputSubmit })) : pendingAuthRetention ? (_jsx(AuthRetentionBox, { state: pendingAuthRetention, onSelect: onAuthRetentionSelect })) : (_jsxs(_Fragment, { children: [_jsx(InputBox, { busy: busy, value: input, onChange: setInput, onSubmit: runPrompt }), showSlashSuggestions ? _jsx(SlashSuggestionBox, { suggestions: slashSuggestions }) : null] })), _jsx(Footer, { config: config, status: visibleStatus, session: session })] })) }));
696
+ return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: !trusted ? (_jsxs(_Fragment, { children: [_jsx(TrustScreen, { workspaceRoot: session.workspaceRoot, onSelect: onTrustSelect }), pendingExitConfirm ? _jsx(ExitConfirmBox, { onSelect: onExitConfirmSelect }) : null] })) : (_jsxs(_Fragment, { children: [_jsx(Dashboard, { config: config, session: session, status: visibleStatus }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [entries.length === 0 && !assistantBuffer && !spinnerLabel ? (_jsx(Box, { borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, children: _jsx(Text, { color: UI_COLORS.muted, children: "New transcript. Use /history if you want earlier session messages." }) })) : null, hiddenTranscriptTurnCount > 0 ? (_jsx(Box, { marginBottom: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, children: _jsxs(Text, { color: UI_COLORS.muted, children: [hiddenTranscriptTurnCount, " earlier turn", hiddenTranscriptTurnCount === 1 ? "" : "s", " hidden. Use /history to inspect older messages or /clear to reset the visible transcript."] }) })) : null, visibleTranscriptCards.map((card) => (_jsx(TranscriptTurnCard, { card: card }, card.id))), (assistantBuffer || activityLabel || spinnerLabel) ? (_jsx(LiveStatusCard, { assistantBuffer: visibleAssistantBuffer, liveLabel: activityLabel ?? spinnerLabel })) : null] }), pendingExitConfirm ? (_jsx(ExitConfirmBox, { onSelect: onExitConfirmSelect })) : paused ? (_jsx(PauseBox, {})) : pendingApproval ? (_jsx(ApprovalBox, { request: pendingApproval.request, onSelect: onApprovalSelect })) : pendingBusyPrompt ? (_jsx(BusyPromptBox, { prompt: pendingBusyPrompt.prompt, onSelect: onBusyPromptSelect })) : modelPickerOpen ? (_jsx(ModelPicker, { currentProvider: session.provider, onSelect: onModelSelect })) : pendingSarvamModelPicker ? (_jsx(SarvamModelPicker, { currentModel: session.provider === "sarvam" ? session.model : null, onSelect: onSarvamModelSelect })) : pendingOpenRouterModelInput ? (_jsx(OpenRouterModelIdBox, { state: pendingOpenRouterModelInput, onChange: onOpenRouterModelInputChange, onSubmit: onOpenRouterModelInputSubmit })) : pendingReasoningSetup ? (_jsx(ReasoningEffortPicker, { currentValue: pendingReasoningSetup.reasoningEffort, provider: pendingReasoningSetup.provider, model: pendingReasoningSetup.model, onSelect: onReasoningSelect })) : pendingAuthInput ? (_jsx(AuthInputBox, { state: pendingAuthInput, onChange: onAuthInputChange, onSubmit: onAuthInputSubmit })) : pendingAuthRetention ? (_jsx(AuthRetentionBox, { state: pendingAuthRetention, onSelect: onAuthRetentionSelect })) : (_jsxs(_Fragment, { children: [_jsx(InputBox, { busy: turnRunning, value: input, onChange: setInput, onSubmit: runPrompt }), showSlashSuggestions ? _jsx(SlashSuggestionBox, { suggestions: slashSuggestions }) : null] })), _jsx(Footer, { config: config, queuedPrompt: queuedPrompt, status: visibleStatus, session: session })] })) }));
528
697
  }
529
698
  function Dashboard({ config, session, status }) {
699
+ const activeProvider = providerConfigFor(config, session.provider);
530
700
  const infoRows = [
701
+ { item: "provider", value: providerLabel(session.provider) },
531
702
  { item: "model", value: session.model },
532
703
  { item: "directory", value: session.workspaceRoot },
533
704
  { item: "session", value: session.id.slice(0, 8) },
534
705
  { item: "updated", value: formatTimestamp(session.updatedAt) }
535
706
  ];
536
707
  const stateRows = [
537
- { item: "auth", value: describeAuth(config) },
538
- { item: "reasoning", value: formatReasoningEffort(config.reasoningEffort) },
708
+ { item: "auth", value: describeAuth(activeProvider) },
709
+ { item: "reasoning", value: formatReasoningEffort(config.reasoningEffort, session.provider) },
539
710
  { item: "skills", value: describeSkills(session.pinnedSkills.length) },
540
- { item: "sha256", value: config.authFingerprint?.slice(0, 12) ?? "(none)" },
711
+ { item: "undo", value: latestUndoableEdit(session) ? "ready" : "none" },
712
+ { item: "sha256", value: activeProvider.authFingerprint?.slice(0, 12) ?? "(none)" },
541
713
  { item: "context", value: describeContext(session.messages.length) }
542
714
  ];
543
- return (_jsxs(_Fragment, { children: [_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", width: "60%", paddingRight: 2, children: [_jsx(Text, { color: "yellow", children: "Vetala" }), _jsx(Text, { bold: true, children: "Ready." }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: infoRows.map((row) => (_jsx(InfoRow, { item: row.item, value: row.value }, row.item))) })] }), _jsxs(Box, { flexDirection: "column", width: "40%", children: [_jsx(Text, { color: "yellow", children: "Tips" }), _jsx(Text, { children: "/help for commands" }), _jsx(Text, { children: "/model for model + reasoning" }), _jsx(Text, { children: "/skill to inspect local skills" }), _jsx(Text, { children: "/logout to clear local auth" }), _jsx(Text, { children: "Ctrl+C to pause" }), _jsx(Text, { children: "Ctrl+D to exit" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: ["status: ", status] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "Context" }) }), stateRows.map((row) => (_jsx(InfoRow, { item: row.item, value: row.value }, row.item)))] })] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Text, { children: "Try \"explain this codebase\" or \"write a test for <filepath>\"" }) })] }));
715
+ return (_jsxs(_Fragment, { children: [_jsxs(Box, { borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", width: "60%", paddingRight: 2, children: [_jsx(Text, { color: UI_COLORS.accent, children: "Vetala" }), _jsx(Text, { bold: true, children: "Ready." }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: infoRows.map((row) => (_jsx(InfoRow, { item: row.item, value: row.value }, row.item))) })] }), _jsxs(Box, { flexDirection: "column", width: "40%", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Tips" }), _jsx(Text, { children: "/help for commands" }), _jsx(Text, { children: "/model for provider + model" }), _jsx(Text, { children: "/undo to revert last edit" }), _jsx(Text, { children: "/skill to inspect local skills" }), _jsx(Text, { children: "/logout to clear local auth" }), _jsx(Text, { children: "Ctrl+C to pause" }), _jsx(Text, { children: "Ctrl+D to exit" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: UI_COLORS.muted, children: ["status: ", status] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: UI_COLORS.accent, children: "Context" }) }), stateRows.map((row) => (_jsx(InfoRow, { item: row.item, value: row.value }, row.item)))] })] }), _jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, children: _jsx(Text, { children: "Try \"explain this codebase\" or \"write a test for <filepath>\"" }) })] }));
544
716
  }
545
717
  function InfoRow({ item, value }) {
546
- return (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: "gray", children: item }) }), _jsx(Text, { children: value })] }));
718
+ return (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: UI_COLORS.muted, children: item }) }), _jsx(Text, { children: value })] }));
547
719
  }
548
720
  function TrustScreen({ workspaceRoot, onSelect }) {
549
- return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Accessing workspace" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: workspaceRoot }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: "Quick safety check: is this a project you created or one you trust? If not, review it before continuing." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Vetala will be able to read, edit, and execute files here." }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
721
+ return (_jsxs(Box, { borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Accessing workspace" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: workspaceRoot }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: "Quick safety check: is this a project you created or one you trust? If not, review it before continuing." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Vetala will be able to read, edit, and execute files here." }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
550
722
  { label: "Yes, I trust this folder", value: "trust" },
551
723
  { label: "No, exit", value: "exit" }
552
724
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
553
725
  }
554
726
  function ApprovalBox({ request, onSelect }) {
555
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "magenta", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "magenta", children: "Approval required" }), request.label.split("\n").map((line) => (_jsx(Text, { children: line }, line))), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
727
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.warning, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.warning, children: "Approval required" }), request.label.split("\n").map((line) => (_jsx(Text, { children: line }, line))), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
556
728
  { label: "Allow once", value: "once" },
557
729
  { label: "Allow for session", value: "session" },
558
730
  { label: "Deny", value: "deny" }
559
731
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
560
732
  }
561
- function ModelPicker({ currentModel, onSelect }) {
562
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "green", children: "Select model" }), _jsxs(Text, { color: "gray", children: ["Current: ", currentModel] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
563
- ...SARVAM_MODELS.map((model) => ({
564
- label: model === currentModel ? `${model} (current)` : model,
565
- value: model
566
- })),
567
- { label: "Cancel", value: "cancel" }
568
- ], onSelect: (item) => void onSelect(item.value) }) })] }));
733
+ function ModelPicker({ currentProvider, onSelect }) {
734
+ const items = [
735
+ ...listProviders().map((provider) => ({
736
+ label: provider.name === currentProvider ? `${provider.label} (current)` : provider.label,
737
+ value: provider.name
738
+ })),
739
+ { label: "Cancel", value: "cancel" }
740
+ ];
741
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Select provider" }), _jsxs(Text, { color: UI_COLORS.muted, children: ["Current: ", providerLabel(currentProvider)] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: items, onSelect: (item) => void onSelect(item.value) }) })] }));
742
+ }
743
+ function SarvamModelPicker({ currentModel, onSelect }) {
744
+ const items = [
745
+ ...getProviderDefinition("sarvam").suggestedModels.map((model) => ({
746
+ label: model === currentModel ? `${model} (current)` : model,
747
+ value: model
748
+ })),
749
+ { label: "Cancel", value: "cancel" }
750
+ ];
751
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Select a Sarvam model" }), _jsx(Text, { color: UI_COLORS.muted, children: "After model selection, Vetala will ask for reasoning effort." }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: items, onSelect: (item) => void onSelect(item.value) }) })] }));
752
+ }
753
+ function OpenRouterModelIdBox({ state, onChange, onSubmit }) {
754
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Enter an OpenRouter model id" }), _jsx(Text, { color: UI_COLORS.muted, children: "Examples: `openai/gpt-4o-mini`, `anthropic/claude-3.5-haiku`, `google/gemini-2.0-flash-001`" }), _jsx(Text, { color: UI_COLORS.muted, children: "Reasoning differs by OpenRouter model. Vetala will use the provider default instead of forcing one global reasoning setting." }), _jsx(Text, { color: UI_COLORS.muted, children: "Press Enter on an empty field to cancel." }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: UI_COLORS.accent, children: "model " }), _jsx(TextInput, { highlightPastedText: false, value: state.value, onChange: onChange, onSubmit: (value) => void onSubmit(value) })] })] }));
569
755
  }
570
- function ReasoningEffortPicker({ currentValue, model, onSelect }) {
571
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "green", children: "Select reasoning effort" }), _jsxs(Text, { color: "gray", children: ["Model: ", model, " \u00B7 Current: ", formatReasoningEffort(currentValue)] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
756
+ function ReasoningEffortPicker({ currentValue, provider, model, onSelect }) {
757
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Select reasoning effort" }), _jsxs(Text, { color: UI_COLORS.muted, children: ["Provider: ", providerLabel(provider), " \u00B7 Model: ", model, " \u00B7 Current: ", formatReasoningEffort(currentValue, provider)] }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
572
758
  { label: "None (null / let Sarvam decide)", value: "none" },
573
759
  { label: "Low", value: "low" },
574
760
  { label: "Medium", value: "medium" },
@@ -577,10 +763,11 @@ function ReasoningEffortPicker({ currentValue, model, onSelect }) {
577
763
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
578
764
  }
579
765
  function AuthInputBox({ state, onChange, onSubmit }) {
580
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "green", children: ["Enter API key for ", state.model] }), _jsx(Text, { color: "gray", children: "This will be used as Sarvam's `apiSubscriptionKey`. After you press Enter, choose whether Vetala keeps it for all sessions or only for this session." }), _jsxs(Text, { color: "gray", children: ["Reasoning: ", formatReasoningEffort(state.reasoningEffort)] }), _jsx(Text, { color: "gray", children: "Press Enter on an empty field to cancel." }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "key " }), _jsx(TextInput, { mask: "*", highlightPastedText: false, value: state.value, onChange: onChange, onSubmit: (value) => void onSubmit(value) })] })] }));
766
+ const definition = getProviderDefinition(state.provider);
767
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: UI_COLORS.accent, children: ["Enter ", definition.auth.inputLabel.toLowerCase(), " for ", providerLabel(state.provider), " / ", state.model] }), _jsxs(Text, { color: UI_COLORS.muted, children: [definition.auth.helpText, " After you press Enter, choose whether Vetala keeps it for all sessions or only for this session."] }), _jsxs(Text, { color: UI_COLORS.muted, children: ["Reasoning: ", formatReasoningEffort(state.reasoningEffort, state.provider)] }), _jsx(Text, { color: UI_COLORS.muted, children: "Press Enter on an empty field to cancel." }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: UI_COLORS.accent, children: "key " }), _jsx(TextInput, { mask: "*", highlightPastedText: false, value: state.value, onChange: onChange, onSubmit: (value) => void onSubmit(value) })] })] }));
581
768
  }
582
769
  function AuthRetentionBox({ state, onSelect }) {
583
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "green", children: ["Keep API key for ", state.model] }), _jsxs(Text, { color: "gray", children: ["Key preview: ", maskSecretPreview(state.value)] }), _jsxs(Text, { color: "gray", children: ["Reasoning: ", formatReasoningEffort(state.reasoningEffort)] }), _jsx(Text, { color: "gray", children: "Future-session mode stores the raw key locally until you run /logout." }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { children: "Choose how long Vetala should keep this key:" }) }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
770
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: UI_COLORS.accent, children: ["Keep credential for ", providerLabel(state.provider), " / ", state.model] }), _jsxs(Text, { color: UI_COLORS.muted, children: ["Key preview: ", maskSecretPreview(state.value)] }), _jsxs(Text, { color: UI_COLORS.muted, children: ["Reasoning: ", formatReasoningEffort(state.reasoningEffort, state.provider)] }), _jsx(Text, { color: UI_COLORS.muted, children: "Future-session mode stores the raw key locally until you run /logout." }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { children: "Choose how long Vetala should keep this key:" }) }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
584
771
  {
585
772
  label: "Keep for all sessions until /logout",
586
773
  value: "persist"
@@ -596,36 +783,54 @@ function AuthRetentionBox({ state, onSelect }) {
596
783
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
597
784
  }
598
785
  function InputBox({ busy, value, onChange, onSubmit }) {
599
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "white", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "\u276F " }), busy ? (_jsx(Text, { color: "gray", children: "Agent is busy. Wait for the current turn to finish." })) : (_jsx(TextInput, { highlightPastedText: false, value: value, onChange: onChange, onSubmit: (nextValue) => void onSubmit(nextValue) }))] }));
786
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, children: [_jsx(Text, { color: UI_COLORS.accent, children: "\u276F " }), _jsx(TextInput, { highlightPastedText: false, value: value, onChange: onChange, onSubmit: (nextValue) => void onSubmit(nextValue) }), busy ? _jsx(Text, { color: UI_COLORS.muted, children: " Enter to queue or force-send." }) : null] }));
787
+ }
788
+ function BusyPromptBox({ prompt, onSelect }) {
789
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.warning, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.warning, children: "Current turn is still running" }), _jsx(Text, { color: UI_COLORS.muted, children: "Choose what to do with the next prompt:" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: summarizeUserPrompt(prompt).split("\n").map((line) => (_jsx(Text, { children: line }, line))) }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
790
+ {
791
+ label: "Send now (stop current turn)",
792
+ value: "force"
793
+ },
794
+ {
795
+ label: "Send after current turn",
796
+ value: "queue"
797
+ },
798
+ {
799
+ label: "Cancel",
800
+ value: "cancel"
801
+ }
802
+ ], onSelect: (item) => void onSelect(item.value) }) })] }));
600
803
  }
601
804
  function SlashSuggestionBox({ suggestions }) {
602
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "blue", children: "Commands" }), _jsx(Text, { color: "gray", children: "Tab autocompletes the first match." }), suggestions.map((suggestion, index) => (_jsxs(Box, { children: [_jsx(Box, { width: "45%", children: _jsxs(Text, { color: index === 0 ? "cyan" : "white", children: [index === 0 ? "❯ " : " ", suggestion.label] }) }), _jsx(Text, { color: "gray", children: suggestion.detail })] }, suggestion.label)))] }));
805
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Commands" }), _jsx(Text, { color: UI_COLORS.muted, children: "Tab autocompletes the first match." }), suggestions.map((suggestion, index) => (_jsxs(Box, { children: [_jsx(Box, { width: "45%", children: index === 0 ? (_jsxs(Text, { color: UI_COLORS.accent, children: ["\u276F ", suggestion.label] })) : (_jsxs(Text, { children: [" ", suggestion.label] })) }), _jsx(Text, { color: UI_COLORS.muted, children: suggestion.detail })] }, suggestion.label)))] }));
603
806
  }
604
807
  function PauseBox() {
605
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Paused" }), _jsx(Text, { children: "Press Ctrl+C again to resume." }), _jsx(Text, { children: "Press Ctrl+D if you want to exit." })] }));
808
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.border, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Paused" }), _jsx(Text, { children: "Press Ctrl+C again to resume." }), _jsx(Text, { children: "Press Ctrl+D if you want to exit." })] }));
606
809
  }
607
810
  function ExitConfirmBox({ onSelect }) {
608
- return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Exit Vetala?" }), _jsx(Text, { color: "gray", children: "Current session state is already written to disk as it changes." }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
811
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Exit Vetala?" }), _jsx(Text, { color: UI_COLORS.muted, children: "Current session state is already written to disk as it changes." }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
609
812
  { label: "Exit", value: "exit" },
610
813
  { label: "Stay", value: "stay" }
611
814
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
612
815
  }
613
- function Footer({ config, status, session }) {
614
- return (_jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: "gray", children: "/help for commands \u00B7 Ctrl+C pause \u00B7 Ctrl+D exit" }), _jsxs(Text, { color: "gray", children: [status, " \u00B7 ", describeAuth(config), " \u00B7 ", describeContext(session.messages.length)] })] }));
816
+ function Footer({ config, queuedPrompt, status, session }) {
817
+ const activeProvider = providerConfigFor(config, session.provider);
818
+ return (_jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: UI_COLORS.muted, children: "/help for commands \u00B7 /undo reverts last edit \u00B7 Ctrl+C pause \u00B7 Ctrl+D exit" }), _jsxs(Text, { color: UI_COLORS.muted, children: [status, queuedPrompt ? " · queued next prompt" : "", " \u00B7 ", describeAuth(activeProvider), " \u00B7 ", describeContext(session.messages.length)] })] }));
615
819
  }
616
- function TranscriptCard({ card, assistantBuffer, liveLabel }) {
820
+ function TranscriptTurnCard({ card }) {
617
821
  const borderColor = transcriptCardBorder(card.entries);
618
- return (_jsxs(Box, { marginBottom: 1, borderStyle: "round", borderColor: borderColor, paddingX: 1, flexDirection: "column", children: [card.entries.map((entry) => (_jsx(TranscriptSection, { entry: entry }, entry.id))), liveLabel ? _jsx(LiveActivitySection, { label: liveLabel }) : null, assistantBuffer ? (_jsx(TranscriptSection, { entry: { id: `${card.id}:stream`, kind: "assistant", text: assistantBuffer } })) : null] }));
822
+ return (_jsx(Box, { marginBottom: 1, borderStyle: "round", borderColor: borderColor, paddingX: 1, flexDirection: "column", children: card.entries.map((entry) => (_jsx(TranscriptSection, { entry: entry }, entry.id))) }));
619
823
  }
620
824
  function LiveStatusCard({ assistantBuffer, liveLabel }) {
621
- return (_jsxs(Box, { marginBottom: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [liveLabel ? _jsx(LiveActivitySection, { label: liveLabel }) : null, assistantBuffer ? (_jsx(TranscriptSection, { entry: { id: "live:assistant", kind: "assistant", text: assistantBuffer } })) : null] }));
825
+ return (_jsxs(Box, { marginBottom: 1, borderStyle: "round", borderColor: UI_COLORS.accent, paddingX: 1, flexDirection: "column", children: [liveLabel ? _jsx(LiveActivitySection, { label: liveLabel }) : null, assistantBuffer ? (_jsx(TranscriptSection, { entry: { id: "live:assistant", kind: "assistant", text: assistantBuffer } })) : null] }));
622
826
  }
623
827
  function TranscriptSection({ entry }) {
624
828
  const isActivity = entry.kind === "activity";
625
- return (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { color: entryColor(entry.kind), children: entryLabel(entry.kind) }), entry.text.split("\n").map((line, index) => isActivity ? (_jsx(Text, { color: "gray", children: line.length > 0 ? line : " " }, `${entry.id}:${index}`)) : (_jsx(Text, { children: line.length > 0 ? line : " " }, `${entry.id}:${index}`)))] }));
829
+ const labelColor = entryColor(entry.kind);
830
+ return (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [labelColor ? _jsx(Text, { color: labelColor, children: entryLabel(entry.kind) }) : _jsx(Text, { children: entryLabel(entry.kind) }), entry.text.split("\n").map((line, index) => isActivity ? (_jsx(Text, { color: UI_COLORS.muted, children: line.length > 0 ? line : " " }, `${entry.id}:${index}`)) : (_jsx(Text, { children: line.length > 0 ? line : " " }, `${entry.id}:${index}`)))] }));
626
831
  }
627
832
  function LiveActivitySection({ label }) {
628
- return (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "doing" }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" ", label] })] })] }));
833
+ return (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.muted, children: "doing" }), _jsxs(Box, { children: [_jsx(Text, { color: UI_COLORS.accent, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: UI_COLORS.muted, children: [" ", label] })] })] }));
629
834
  }
630
835
  function cloneSession(session) {
631
836
  return {
@@ -638,14 +843,15 @@ function cloneSession(session) {
638
843
  messages: [...session.messages],
639
844
  referencedFiles: [...session.referencedFiles],
640
845
  readFiles: [...session.readFiles],
641
- pinnedSkills: [...session.pinnedSkills]
846
+ pinnedSkills: [...session.pinnedSkills],
847
+ edits: session.edits.map((edit) => ({ ...edit }))
642
848
  };
643
849
  }
644
850
  function formatSessionList(sessions) {
645
851
  return sessions.length > 0
646
852
  ? sessions
647
853
  .slice(0, 10)
648
- .map((item) => `${item.id} ${item.updatedAt} ${item.workspaceRoot}`)
854
+ .map((item) => `${item.id} ${item.provider}/${item.model} ${item.updatedAt} ${item.workspaceRoot}`)
649
855
  .join("\n")
650
856
  : "(no sessions)";
651
857
  }
@@ -667,18 +873,23 @@ function describeAuth(config) {
667
873
  return "missing";
668
874
  }
669
875
  }
670
- function formatReasoningEffort(value) {
876
+ function formatReasoningEffort(value, provider) {
877
+ if (!getProviderDefinition(provider).supportsReasoningEffort) {
878
+ return "provider default (model-specific)";
879
+ }
671
880
  return value ?? "(none)";
672
881
  }
673
882
  function describeSkills(pinnedCount) {
674
883
  return pinnedCount > 0 ? `${pinnedCount} pinned` : "none pinned";
675
884
  }
676
885
  function formatModelSetupSummary(state, config, authRetention) {
886
+ const profile = providerConfigFor(config, state.provider);
677
887
  const lines = [
888
+ `Provider: ${providerLabel(state.provider)}`,
678
889
  `Model: ${state.model}`,
679
- `Reasoning effort: ${formatReasoningEffort(state.reasoningEffort)}`,
680
- `Credential: ${describeAuth(config)}`,
681
- `Stored SHA-256: ${config.authFingerprint?.slice(0, 16) ?? "(none)"}`
890
+ `Reasoning effort: ${formatReasoningEffort(state.reasoningEffort, state.provider)}`,
891
+ `Credential: ${describeAuth(profile)}`,
892
+ `Stored SHA-256: ${profile.authFingerprint?.slice(0, 16) ?? "(none)"}`
682
893
  ];
683
894
  if (authRetention === "persist") {
684
895
  lines.push("Raw key is stored locally for all future sessions until /logout.");
@@ -706,6 +917,20 @@ function summarizeUserPrompt(prompt) {
706
917
  `Preview: ${preview}${preview.length < prompt.replace(/\s+/g, " ").trim().length ? "..." : ""}`
707
918
  ].join("\n");
708
919
  }
920
+ function renderLiveAssistantBuffer(buffer) {
921
+ if (!buffer) {
922
+ return "";
923
+ }
924
+ const lines = buffer.split("\n");
925
+ if (lines.length <= MAX_LIVE_ASSISTANT_LINES) {
926
+ return buffer;
927
+ }
928
+ const hiddenLineCount = lines.length - MAX_LIVE_ASSISTANT_LINES;
929
+ return [
930
+ `[${hiddenLineCount} earlier line${hiddenLineCount === 1 ? "" : "s"} hidden while streaming]`,
931
+ ...lines.slice(-MAX_LIVE_ASSISTANT_LINES)
932
+ ].join("\n");
933
+ }
709
934
  function renderAuthMode(authMode) {
710
935
  switch (authMode) {
711
936
  case "bearer":
@@ -732,19 +957,19 @@ function maskSecretPreview(value) {
732
957
  function entryColor(kind) {
733
958
  switch (kind) {
734
959
  case "assistant":
735
- return "cyan";
960
+ return UI_COLORS.accent;
736
961
  case "user":
737
- return "green";
962
+ return undefined;
738
963
  case "tool":
739
- return "magenta";
964
+ return UI_COLORS.accent;
740
965
  case "activity":
741
- return "gray";
966
+ return UI_COLORS.muted;
742
967
  case "info":
743
- return "blue";
968
+ return UI_COLORS.accent;
744
969
  case "warn":
745
- return "yellow";
970
+ return UI_COLORS.warning;
746
971
  case "error":
747
- return "red";
972
+ return UI_COLORS.danger;
748
973
  }
749
974
  }
750
975
  function entryLabel(kind) {
@@ -767,23 +992,20 @@ function entryLabel(kind) {
767
992
  }
768
993
  function transcriptCardBorder(entries) {
769
994
  if (entries.some((entry) => entry.kind === "error")) {
770
- return "red";
995
+ return UI_COLORS.danger;
771
996
  }
772
997
  if (entries.some((entry) => entry.kind === "warn")) {
773
- return "yellow";
998
+ return UI_COLORS.warning;
774
999
  }
775
1000
  if (entries.some((entry) => entry.kind === "tool")) {
776
- return "magenta";
777
- }
778
- if (entries.some((entry) => entry.kind === "assistant")) {
779
- return "white";
1001
+ return UI_COLORS.accent;
780
1002
  }
781
1003
  if (entries.some((entry) => entry.kind === "info")) {
782
- return "blue";
1004
+ return UI_COLORS.accent;
783
1005
  }
784
1006
  if (entries.some((entry) => entry.kind === "activity")) {
785
- return "gray";
1007
+ return UI_COLORS.muted;
786
1008
  }
787
- return "white";
1009
+ return UI_COLORS.border;
788
1010
  }
789
1011
  //# sourceMappingURL=repl-app.js.map