@vetala/vetala 0.1.0-dev → 0.2.1-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 (73) hide show
  1. package/README.md +31 -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 +419 -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/dist/src/update-notifier.d.ts +41 -0
  70. package/dist/src/update-notifier.js +145 -0
  71. package/dist/src/update-notifier.js.map +1 -0
  72. package/package.json +13 -2
  73. package/terminal.png +0 -0
@@ -4,17 +4,29 @@ 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 { checkForAppUpdate, installAppUpdate, snoozeAppUpdate } from "../update-notifier.js";
16
+ import { buildTranscriptCards } from "./transcript-cards.js";
14
17
  import { InkTerminalUI } from "./ink-terminal-ui.js";
15
18
  import { buildSlashSuggestions } from "./command-suggestions.js";
16
- import { buildTranscriptCards } from "./transcript-cards.js";
17
- export function ReplApp({ initialConfig, initialSession, store }) {
19
+ const ASSISTANT_FLUSH_INTERVAL_MS = 33;
20
+ const MAX_LIVE_ASSISTANT_LINES = 24;
21
+ const MAX_VISIBLE_TRANSCRIPT_TURNS = 6;
22
+ const UI_COLORS = {
23
+ accent: "blue",
24
+ muted: "gray",
25
+ border: "gray",
26
+ warning: "yellow",
27
+ danger: "red"
28
+ };
29
+ export function ReplApp({ initialConfig, initialSession, runtimeProfile, store }) {
18
30
  const { exit } = useApp();
19
31
  const [config, setConfig] = useState(initialConfig);
20
32
  const [session, setSession] = useState(() => cloneSession(initialSession));
@@ -26,18 +38,30 @@ export function ReplApp({ initialConfig, initialSession, store }) {
26
38
  const [spinnerLabel, setSpinnerLabel] = useState(null);
27
39
  const [status, setStatus] = useState("Ready");
28
40
  const [pendingApproval, setPendingApproval] = useState(null);
41
+ const [pendingBusyPrompt, setPendingBusyPrompt] = useState(null);
42
+ const [pendingUpdatePrompt, setPendingUpdatePrompt] = useState(null);
29
43
  const [modelPickerOpen, setModelPickerOpen] = useState(false);
44
+ const [pendingSarvamModelPicker, setPendingSarvamModelPicker] = useState(null);
45
+ const [pendingOpenRouterModelInput, setPendingOpenRouterModelInput] = useState(null);
30
46
  const [pendingReasoningSetup, setPendingReasoningSetup] = useState(null);
31
47
  const [pendingAuthInput, setPendingAuthInput] = useState(null);
32
48
  const [pendingAuthRetention, setPendingAuthRetention] = useState(null);
33
49
  const [availableSkills, setAvailableSkills] = useState([]);
34
50
  const [paused, setPaused] = useState(false);
35
51
  const [pendingExitConfirm, setPendingExitConfirm] = useState(false);
52
+ const [queuedPrompt, setQueuedPrompt] = useState(null);
53
+ const [turnRunning, setTurnRunning] = useState(false);
54
+ const [installingUpdate, setInstallingUpdate] = useState(null);
36
55
  const assistantBufferRef = useRef("");
56
+ const assistantFlushTimerRef = useRef(null);
37
57
  const nextEntryIdRef = useRef(0);
58
+ const queuedPromptRef = useRef(null);
38
59
  const sessionRef = useRef(session);
60
+ const turnRunningRef = useRef(false);
61
+ const updateCheckStartedRef = useRef(false);
39
62
  const uiRef = useRef(null);
40
63
  const skillRuntimeRef = useRef(null);
64
+ const activeAgentRef = useRef(null);
41
65
  sessionRef.current = session;
42
66
  const pushEntry = (kind, text) => {
43
67
  setEntries((current) => [
@@ -49,7 +73,24 @@ export function ReplApp({ initialConfig, initialSession, store }) {
49
73
  }
50
74
  ]);
51
75
  };
76
+ const flushAssistantBuffer = () => {
77
+ if (assistantFlushTimerRef.current) {
78
+ clearTimeout(assistantFlushTimerRef.current);
79
+ assistantFlushTimerRef.current = null;
80
+ }
81
+ setAssistantBuffer(assistantBufferRef.current);
82
+ };
83
+ const scheduleAssistantFlush = () => {
84
+ if (assistantFlushTimerRef.current) {
85
+ return;
86
+ }
87
+ assistantFlushTimerRef.current = setTimeout(() => {
88
+ assistantFlushTimerRef.current = null;
89
+ setAssistantBuffer(assistantBufferRef.current);
90
+ }, ASSISTANT_FLUSH_INTERVAL_MS);
91
+ };
52
92
  const finalizeAssistant = () => {
93
+ flushAssistantBuffer();
53
94
  const buffered = assistantBufferRef.current.trimEnd();
54
95
  if (!buffered) {
55
96
  assistantBufferRef.current = "";
@@ -64,7 +105,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
64
105
  uiRef.current = new InkTerminalUI({
65
106
  appendAssistant: (text) => {
66
107
  assistantBufferRef.current += text;
67
- setAssistantBuffer(assistantBufferRef.current);
108
+ scheduleAssistantFlush();
68
109
  },
69
110
  finalizeAssistant,
70
111
  pushEntry,
@@ -75,7 +116,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
75
116
  setSpinnerLabel(label);
76
117
  setStatus(label ?? "Ready");
77
118
  }
78
- });
119
+ }, runtimeProfile);
79
120
  }
80
121
  const ui = uiRef.current;
81
122
  if (!skillRuntimeRef.current) {
@@ -85,17 +126,23 @@ export function ReplApp({ initialConfig, initialSession, store }) {
85
126
  });
86
127
  }
87
128
  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
129
  const visibleStatus = paused ? "Paused" : status;
130
+ const visibleAssistantBuffer = renderLiveAssistantBuffer(assistantBuffer);
131
+ const transcriptCards = buildTranscriptCards(entries);
132
+ const visibleTranscriptCards = transcriptCards.slice(-MAX_VISIBLE_TRANSCRIPT_TURNS);
133
+ const hiddenTranscriptTurnCount = Math.max(0, transcriptCards.length - visibleTranscriptCards.length);
92
134
  const slashSuggestions = buildSlashSuggestions(input, availableSkills).slice(0, 8);
93
135
  const showSlashSuggestions = Boolean(trusted &&
94
- !busy &&
136
+ !turnRunning &&
95
137
  !paused &&
96
138
  !pendingExitConfirm &&
97
139
  !pendingApproval &&
140
+ !pendingBusyPrompt &&
141
+ !pendingUpdatePrompt &&
142
+ !installingUpdate &&
98
143
  !modelPickerOpen &&
144
+ !pendingSarvamModelPicker &&
145
+ !pendingOpenRouterModelInput &&
99
146
  !pendingReasoningSetup &&
100
147
  !pendingAuthInput &&
101
148
  !pendingAuthRetention &&
@@ -123,6 +170,31 @@ export function ReplApp({ initialConfig, initialSession, store }) {
123
170
  cancelled = true;
124
171
  };
125
172
  }, [skills]);
173
+ useEffect(() => () => {
174
+ if (assistantFlushTimerRef.current) {
175
+ clearTimeout(assistantFlushTimerRef.current);
176
+ }
177
+ }, []);
178
+ useEffect(() => {
179
+ if (!trusted || updateCheckStartedRef.current) {
180
+ return;
181
+ }
182
+ updateCheckStartedRef.current = true;
183
+ let cancelled = false;
184
+ void checkForAppUpdate()
185
+ .then((update) => {
186
+ if (!cancelled && update) {
187
+ setPendingUpdatePrompt(update);
188
+ setStatus(`Update available: ${update.latestVersion}`);
189
+ }
190
+ })
191
+ .catch(() => {
192
+ // Keep startup quiet if the registry cannot be reached.
193
+ });
194
+ return () => {
195
+ cancelled = true;
196
+ };
197
+ }, [trusted]);
126
198
  useInput((inputValue, key) => {
127
199
  if (showSlashSuggestions && isTabInput(inputValue, key)) {
128
200
  const firstSuggestion = slashSuggestions[0];
@@ -154,41 +226,67 @@ export function ReplApp({ initialConfig, initialSession, store }) {
154
226
  return cloned;
155
227
  };
156
228
  const resetTranscript = () => {
229
+ flushAssistantBuffer();
157
230
  assistantBufferRef.current = "";
158
231
  setAssistantBuffer("");
159
232
  setActivityLabel(null);
160
233
  nextEntryIdRef.current = 0;
161
234
  setEntries([]);
162
235
  };
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;
236
+ const mergeLoadedConfig = (loaded, skipProvider) => {
237
+ let merged = loaded;
238
+ for (const provider of listProviders()) {
239
+ if (provider.name === skipProvider) {
240
+ continue;
241
+ }
242
+ const currentProfile = config.providers[provider.name];
243
+ if (currentProfile.authSource === "session" &&
244
+ currentProfile.authValue &&
245
+ currentProfile.authMode !== "missing") {
246
+ merged = withProviderSessionAuth(merged, provider.name, currentProfile.authMode, currentProfile.authValue);
247
+ }
248
+ }
249
+ return merged;
174
250
  };
175
251
  const closeCommandModals = () => {
176
252
  setModelPickerOpen(false);
253
+ setPendingSarvamModelPicker(null);
254
+ setPendingOpenRouterModelInput(null);
177
255
  setPendingReasoningSetup(null);
178
256
  setPendingAuthInput(null);
179
257
  setPendingAuthRetention(null);
180
258
  };
181
- const runPrompt = async (prompt) => {
259
+ const setQueuedPromptState = (prompt) => {
260
+ queuedPromptRef.current = prompt;
261
+ setQueuedPrompt(prompt);
262
+ };
263
+ const setTurnRunningState = (value) => {
264
+ turnRunningRef.current = value;
265
+ setTurnRunning(value);
266
+ };
267
+ const beginQueuedPrompt = (prompt) => {
268
+ setQueuedPromptState(null);
269
+ setPendingBusyPrompt(null);
270
+ void runPrompt(prompt, { forceRun: true });
271
+ };
272
+ const runPrompt = async (prompt, options = {}) => {
182
273
  const trimmed = prompt.trim();
183
274
  if (!trimmed) {
184
275
  return;
185
276
  }
277
+ if (turnRunningRef.current && !options.forceRun) {
278
+ setPendingBusyPrompt({ prompt: trimmed });
279
+ setInput("");
280
+ return;
281
+ }
186
282
  if (trimmed.startsWith("/")) {
283
+ setInput("");
187
284
  await handleCommand(trimmed);
188
285
  return;
189
286
  }
190
287
  setInput("");
191
288
  setPendingExitConfirm(false);
289
+ setPendingBusyPrompt(null);
192
290
  setActivityLabel(null);
193
291
  pushEntry("user", summarizeUserPrompt(trimmed));
194
292
  setStatus("Running agent");
@@ -199,23 +297,40 @@ export function ReplApp({ initialConfig, initialSession, store }) {
199
297
  sessionStore: store,
200
298
  approvals,
201
299
  pathPolicy: new PathPolicy(session.workspaceRoot, approvals),
300
+ runtimeProfile,
202
301
  skills,
203
302
  tools: createTools(),
204
303
  ui
205
304
  });
305
+ activeAgentRef.current = agent;
306
+ setTurnRunningState(true);
206
307
  try {
207
308
  await agent.runTurn(trimmed, true);
208
309
  syncSession(session);
209
310
  setActivityLabel(null);
210
- setStatus("Ready");
311
+ setStatus(queuedPromptRef.current ? "Running queued prompt" : "Ready");
211
312
  }
212
313
  catch (error) {
314
+ if (isAgentInterruptedError(error)) {
315
+ setActivityLabel(null);
316
+ syncSession(session);
317
+ setStatus(queuedPromptRef.current ? "Running queued prompt" : "Interrupted");
318
+ return;
319
+ }
213
320
  finalizeAssistant();
214
321
  setActivityLabel(null);
215
322
  pushEntry("error", error instanceof Error ? error.message : String(error));
216
323
  setStatus("Failed");
217
324
  syncSession(session);
218
325
  }
326
+ finally {
327
+ activeAgentRef.current = null;
328
+ setTurnRunningState(false);
329
+ const nextQueuedPrompt = queuedPromptRef.current;
330
+ if (nextQueuedPrompt) {
331
+ beginQueuedPrompt(nextQueuedPrompt);
332
+ }
333
+ }
219
334
  };
220
335
  const handleCommand = async (commandLine) => {
221
336
  const [command, ...args] = commandLine.slice(1).split(/\s+/);
@@ -227,6 +342,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
227
342
  pushEntry("info", [
228
343
  "/help",
229
344
  "/model",
345
+ "/undo",
230
346
  "/skill",
231
347
  "/tools",
232
348
  "/history",
@@ -245,8 +361,18 @@ export function ReplApp({ initialConfig, initialSession, store }) {
245
361
  }
246
362
  closeCommandModals();
247
363
  setModelPickerOpen(true);
248
- setStatus("Select a model");
364
+ setStatus("Select a provider and model");
365
+ return;
366
+ case "undo": {
367
+ const result = await undoLastEdit(session, store, (request) => {
368
+ const approvals = new ApprovalManager(session, store, null, requestApprovalDecision);
369
+ return approvals.requestApproval(request);
370
+ });
371
+ syncSession(session);
372
+ pushEntry(result.isError ? "warn" : "info", result.content);
373
+ setStatus(result.isError ? "Undo blocked" : "Ready");
249
374
  return;
375
+ }
250
376
  case "skill":
251
377
  case "skills":
252
378
  await handleSkillsCommand(args);
@@ -282,7 +408,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
282
408
  return;
283
409
  }
284
410
  case "new": {
285
- const nextSession = await store.createSession(session.workspaceRoot, session.model);
411
+ const nextSession = await store.createSession(session.workspaceRoot, session.provider, session.model);
286
412
  const nextCloned = syncSession(nextSession);
287
413
  closeCommandModals();
288
414
  resetTranscript();
@@ -300,22 +426,22 @@ export function ReplApp({ initialConfig, initialSession, store }) {
300
426
  ui.printConfig(config);
301
427
  return;
302
428
  case "logout": {
303
- const previousAuthSource = config.authSource;
304
- await clearSavedAuth();
429
+ const previousProfile = providerConfigFor(config, session.provider);
430
+ await clearProviderSavedAuth(session.provider);
305
431
  closeCommandModals();
306
- const reloaded = await loadConfig();
432
+ const reloaded = mergeLoadedConfig(await loadConfig(), session.provider);
307
433
  setConfig(reloaded);
308
- if (reloaded.authSource === "env") {
309
- pushEntry("warn", "Cleared local auth state, but environment credentials are still active for this process.");
434
+ if (providerConfigFor(reloaded, session.provider).authSource === "env") {
435
+ pushEntry("warn", `Cleared local ${providerLabel(session.provider)} auth state, but environment credentials are still active for this process.`);
310
436
  }
311
- else if (previousAuthSource === "stored" || previousAuthSource === "stored_hash") {
312
- pushEntry("info", "Cleared the saved API key for future launches and removed current local auth.");
437
+ else if (previousProfile.authSource === "stored" || previousProfile.authSource === "stored_hash") {
438
+ pushEntry("info", `Cleared the saved ${providerLabel(session.provider)} credential for future launches.`);
313
439
  }
314
- else if (previousAuthSource === "session") {
315
- pushEntry("info", "Cleared the API key that was only active in this session.");
440
+ else if (previousProfile.authSource === "session") {
441
+ pushEntry("info", `Cleared the ${providerLabel(session.provider)} credential that was only active in this session.`);
316
442
  }
317
443
  else {
318
- pushEntry("info", "No saved API key remained after logout.");
444
+ pushEntry("info", `No saved ${providerLabel(session.provider)} credential remained after logout.`);
319
445
  }
320
446
  setStatus("Logged out");
321
447
  return;
@@ -423,6 +549,96 @@ export function ReplApp({ initialConfig, initialSession, store }) {
423
549
  setPendingApproval(null);
424
550
  promptState.resolve(value);
425
551
  };
552
+ const onBusyPromptSelect = async (choice) => {
553
+ const current = pendingBusyPrompt;
554
+ if (!current) {
555
+ return;
556
+ }
557
+ if (choice === "cancel") {
558
+ setPendingBusyPrompt(null);
559
+ setInput(current.prompt);
560
+ return;
561
+ }
562
+ const replacedExistingQueue = queuedPromptRef.current !== null;
563
+ setQueuedPromptState(current.prompt);
564
+ setPendingBusyPrompt(null);
565
+ pushEntry("info", choice === "force"
566
+ ? `Stopping the current turn and sending next: ${summarizeUserPrompt(current.prompt)}`
567
+ : `${replacedExistingQueue ? "Replaced" : "Queued"} next prompt: ${summarizeUserPrompt(current.prompt)}`);
568
+ if (choice === "force") {
569
+ setStatus("Stopping current turn");
570
+ activeAgentRef.current?.requestStop();
571
+ return;
572
+ }
573
+ setStatus("Queued next prompt");
574
+ };
575
+ const onUpdatePromptSelect = async (choice) => {
576
+ const current = pendingUpdatePrompt;
577
+ if (!current) {
578
+ return;
579
+ }
580
+ if (choice === "update_later") {
581
+ await snoozeAppUpdate(current.latestVersion);
582
+ setPendingUpdatePrompt(null);
583
+ setStatus("Ready");
584
+ return;
585
+ }
586
+ setPendingUpdatePrompt(null);
587
+ setInstallingUpdate(current);
588
+ setStatus(`Installing ${current.latestVersion}`);
589
+ try {
590
+ const result = await installAppUpdate(current.latestVersion);
591
+ const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
592
+ if (result.timedOut || (result.exitCode ?? 1) !== 0) {
593
+ pushEntry("warn", [
594
+ `Update failed for ${current.latestVersion}.`,
595
+ current.installCommand,
596
+ output || "(no output)"
597
+ ].join("\n"));
598
+ setStatus("Update failed");
599
+ return;
600
+ }
601
+ pushEntry("info", [
602
+ `Updated Vetala to ${current.latestVersion}.`,
603
+ "Restart Vetala to use the new version.",
604
+ current.installCommand
605
+ ].join("\n"));
606
+ setStatus("Update installed; restart Vetala");
607
+ }
608
+ catch (error) {
609
+ pushEntry("warn", [
610
+ `Update failed for ${current.latestVersion}.`,
611
+ current.installCommand,
612
+ error instanceof Error ? error.message : String(error)
613
+ ].join("\n"));
614
+ setStatus("Update failed");
615
+ }
616
+ finally {
617
+ setInstallingUpdate(null);
618
+ }
619
+ };
620
+ const applyModelSelection = async (nextSettings) => {
621
+ const definition = getProviderDefinition(nextSettings.provider);
622
+ await store.updateModel(session, nextSettings.provider, nextSettings.model);
623
+ await saveProviderDefaults(nextSettings.provider, nextSettings.model, definition.supportsReasoningEffort
624
+ ? { reasoningEffort: nextSettings.reasoningEffort }
625
+ : {});
626
+ syncSession(session);
627
+ const nextConfig = mergeLoadedConfig(await loadConfig());
628
+ setConfig(nextConfig);
629
+ const nextProfile = providerConfigFor(nextConfig, nextSettings.provider);
630
+ if (nextProfile.authSource === "missing" || nextProfile.authSource === "stored_hash") {
631
+ setPendingAuthInput({
632
+ ...nextSettings,
633
+ authMode: definition.auth.defaultMode,
634
+ value: ""
635
+ });
636
+ setStatus(`Enter ${definition.auth.inputLabel.toLowerCase()} for ${providerLabel(nextSettings.provider)} / ${nextSettings.model}`);
637
+ return;
638
+ }
639
+ pushEntry("info", formatModelSetupSummary(nextSettings, nextConfig));
640
+ setStatus("Ready");
641
+ };
426
642
  const onModelSelect = async (value) => {
427
643
  if (value === "cancel") {
428
644
  setModelPickerOpen(false);
@@ -430,11 +646,30 @@ export function ReplApp({ initialConfig, initialSession, store }) {
430
646
  return;
431
647
  }
432
648
  setModelPickerOpen(false);
649
+ if (value === "sarvam") {
650
+ setPendingSarvamModelPicker("sarvam");
651
+ setStatus("Select a Sarvam model");
652
+ return;
653
+ }
654
+ setPendingOpenRouterModelInput({
655
+ provider: "openrouter",
656
+ value: session.provider === "openrouter" ? session.model : providerConfigFor(config, "openrouter").defaultModel
657
+ });
658
+ setStatus("Enter an OpenRouter model id");
659
+ };
660
+ const onSarvamModelSelect = async (value) => {
661
+ if (value === "cancel") {
662
+ setPendingSarvamModelPicker(null);
663
+ setStatus("Ready");
664
+ return;
665
+ }
666
+ setPendingSarvamModelPicker(null);
433
667
  setPendingReasoningSetup({
668
+ provider: "sarvam",
434
669
  model: value,
435
670
  reasoningEffort: config.reasoningEffort
436
671
  });
437
- setStatus(`Select reasoning effort for ${value}`);
672
+ setStatus(`Select reasoning effort for Sarvam / ${value}`);
438
673
  };
439
674
  const onReasoningSelect = async (value) => {
440
675
  const current = pendingReasoningSetup;
@@ -451,23 +686,28 @@ export function ReplApp({ initialConfig, initialSession, store }) {
451
686
  ...current,
452
687
  reasoningEffort: value === "none" ? null : value
453
688
  };
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}`);
689
+ await applyModelSelection(nextSettings);
690
+ };
691
+ const onOpenRouterModelInputChange = (value) => {
692
+ setPendingOpenRouterModelInput((current) => (current ? { ...current, value } : current));
693
+ };
694
+ const onOpenRouterModelInputSubmit = async (value) => {
695
+ const trimmed = value.trim();
696
+ if (!pendingOpenRouterModelInput) {
467
697
  return;
468
698
  }
469
- pushEntry("info", formatModelSetupSummary(nextSettings, nextConfig));
470
- setStatus("Ready");
699
+ if (!trimmed) {
700
+ setPendingOpenRouterModelInput(null);
701
+ pushEntry("warn", "OpenRouter model selection cancelled.");
702
+ setStatus("Ready");
703
+ return;
704
+ }
705
+ setPendingOpenRouterModelInput(null);
706
+ await applyModelSelection({
707
+ provider: "openrouter",
708
+ model: trimmed,
709
+ reasoningEffort: null
710
+ });
471
711
  };
472
712
  const onAuthInputChange = (value) => {
473
713
  setPendingAuthInput((current) => (current ? { ...current, value } : current));
@@ -480,7 +720,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
480
720
  const trimmed = value.trim();
481
721
  if (!trimmed) {
482
722
  setPendingAuthInput(null);
483
- pushEntry("warn", "API key entry cancelled. Model settings were saved, but no usable Sarvam credential is active.");
723
+ pushEntry("warn", `Credential entry cancelled. Model settings were saved, but no usable ${providerLabel(current.provider)} credential is active.`);
484
724
  setStatus("Ready");
485
725
  return;
486
726
  }
@@ -489,7 +729,7 @@ export function ReplApp({ initialConfig, initialSession, store }) {
489
729
  ...current,
490
730
  value: trimmed
491
731
  });
492
- setStatus("Choose how long to keep this API key");
732
+ setStatus("Choose how long to keep this credential");
493
733
  };
494
734
  const onAuthRetentionSelect = async (choice) => {
495
735
  const current = pendingAuthRetention;
@@ -504,15 +744,15 @@ export function ReplApp({ initialConfig, initialSession, store }) {
504
744
  }
505
745
  const loadedConfig = await loadConfig();
506
746
  const nextConfig = choice === "persist"
507
- ? withStoredAuth(loadedConfig, current.authMode, current.value)
508
- : withSessionAuth(loadedConfig, current.authMode, current.value);
747
+ ? withProviderStoredAuth(loadedConfig, current.provider, current.authMode, current.value)
748
+ : withProviderSessionAuth(loadedConfig, current.provider, current.authMode, current.value);
509
749
  if (choice === "persist") {
510
- await savePersistentAuth(current.authMode, current.value);
750
+ await saveProviderPersistentAuth(current.provider, current.authMode, current.value);
511
751
  }
512
752
  setConfig(nextConfig);
513
753
  setPendingAuthRetention(null);
514
754
  pushEntry("info", formatModelSetupSummary(current, nextConfig, choice));
515
- if (choice === "persist" && loadedConfig.authSource === "env") {
755
+ if (choice === "persist" && providerConfigFor(loadedConfig, current.provider).authSource === "env") {
516
756
  pushEntry("warn", "Environment credentials are still set in this shell. They may take precedence on future launches.");
517
757
  }
518
758
  setStatus("Ready");
@@ -524,51 +764,68 @@ export function ReplApp({ initialConfig, initialSession, store }) {
524
764
  }
525
765
  setPendingExitConfirm(false);
526
766
  };
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 })] })) }));
767
+ 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 })) : installingUpdate ? (_jsx(UpdateInstallBox, { update: installingUpdate })) : pendingUpdatePrompt ? (_jsx(UpdatePromptBox, { update: pendingUpdatePrompt, onSelect: onUpdatePromptSelect })) : 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
768
  }
529
769
  function Dashboard({ config, session, status }) {
770
+ const activeProvider = providerConfigFor(config, session.provider);
530
771
  const infoRows = [
772
+ { item: "provider", value: providerLabel(session.provider) },
531
773
  { item: "model", value: session.model },
532
774
  { item: "directory", value: session.workspaceRoot },
533
775
  { item: "session", value: session.id.slice(0, 8) },
534
776
  { item: "updated", value: formatTimestamp(session.updatedAt) }
535
777
  ];
536
778
  const stateRows = [
537
- { item: "auth", value: describeAuth(config) },
538
- { item: "reasoning", value: formatReasoningEffort(config.reasoningEffort) },
779
+ { item: "auth", value: describeAuth(activeProvider) },
780
+ { item: "reasoning", value: formatReasoningEffort(config.reasoningEffort, session.provider) },
539
781
  { item: "skills", value: describeSkills(session.pinnedSkills.length) },
540
- { item: "sha256", value: config.authFingerprint?.slice(0, 12) ?? "(none)" },
782
+ { item: "undo", value: latestUndoableEdit(session) ? "ready" : "none" },
783
+ { item: "sha256", value: activeProvider.authFingerprint?.slice(0, 12) ?? "(none)" },
541
784
  { item: "context", value: describeContext(session.messages.length) }
542
785
  ];
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>\"" }) })] }));
786
+ 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
787
  }
545
788
  function InfoRow({ item, value }) {
546
- return (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: "gray", children: item }) }), _jsx(Text, { children: value })] }));
789
+ return (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: UI_COLORS.muted, children: item }) }), _jsx(Text, { children: value })] }));
547
790
  }
548
791
  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: [
792
+ 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
793
  { label: "Yes, I trust this folder", value: "trust" },
551
794
  { label: "No, exit", value: "exit" }
552
795
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
553
796
  }
554
797
  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: [
798
+ 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
799
  { label: "Allow once", value: "once" },
557
800
  { label: "Allow for session", value: "session" },
558
801
  { label: "Deny", value: "deny" }
559
802
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
560
803
  }
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) }) })] }));
804
+ function ModelPicker({ currentProvider, onSelect }) {
805
+ const items = [
806
+ ...listProviders().map((provider) => ({
807
+ label: provider.name === currentProvider ? `${provider.label} (current)` : provider.label,
808
+ value: provider.name
809
+ })),
810
+ { label: "Cancel", value: "cancel" }
811
+ ];
812
+ 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) }) })] }));
569
813
  }
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: [
814
+ function SarvamModelPicker({ currentModel, onSelect }) {
815
+ const items = [
816
+ ...getProviderDefinition("sarvam").suggestedModels.map((model) => ({
817
+ label: model === currentModel ? `${model} (current)` : model,
818
+ value: model
819
+ })),
820
+ { label: "Cancel", value: "cancel" }
821
+ ];
822
+ 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) }) })] }));
823
+ }
824
+ function OpenRouterModelIdBox({ state, onChange, onSubmit }) {
825
+ 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) })] })] }));
826
+ }
827
+ function ReasoningEffortPicker({ currentValue, provider, model, onSelect }) {
828
+ 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
829
  { label: "None (null / let Sarvam decide)", value: "none" },
573
830
  { label: "Low", value: "low" },
574
831
  { label: "Medium", value: "medium" },
@@ -577,10 +834,11 @@ function ReasoningEffortPicker({ currentValue, model, onSelect }) {
577
834
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
578
835
  }
579
836
  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) })] })] }));
837
+ const definition = getProviderDefinition(state.provider);
838
+ 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
839
  }
582
840
  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: [
841
+ 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
842
  {
585
843
  label: "Keep for all sessions until /logout",
586
844
  value: "persist"
@@ -596,36 +854,69 @@ function AuthRetentionBox({ state, onSelect }) {
596
854
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
597
855
  }
598
856
  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) }))] }));
857
+ 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] }));
858
+ }
859
+ function BusyPromptBox({ prompt, onSelect }) {
860
+ 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: [
861
+ {
862
+ label: "Send now (stop current turn)",
863
+ value: "force"
864
+ },
865
+ {
866
+ label: "Send after current turn",
867
+ value: "queue"
868
+ },
869
+ {
870
+ label: "Cancel",
871
+ value: "cancel"
872
+ }
873
+ ], onSelect: (item) => void onSelect(item.value) }) })] }));
874
+ }
875
+ function UpdatePromptBox({ update, onSelect }) {
876
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.accent, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Update available" }), _jsxs(Text, { children: [update.currentVersion, " \u2192 ", update.latestVersion] }), _jsx(Text, { color: UI_COLORS.muted, children: update.installCommand }), _jsx(Box, { marginTop: 1, children: _jsx(SelectInput, { items: [
877
+ {
878
+ label: "Update now",
879
+ value: "update_now"
880
+ },
881
+ {
882
+ label: "Update later",
883
+ value: "update_later"
884
+ }
885
+ ], onSelect: (item) => void onSelect(item.value) }) })] }));
886
+ }
887
+ function UpdateInstallBox({ update }) {
888
+ return (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: UI_COLORS.accent, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: UI_COLORS.accent, children: "Installing update" }), _jsxs(Box, { children: [_jsx(Text, { color: UI_COLORS.accent, children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" Updating to ", update.latestVersion, "..."] })] }), _jsx(Text, { color: UI_COLORS.muted, children: update.installCommand })] }));
600
889
  }
601
890
  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)))] }));
891
+ 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
892
  }
604
893
  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." })] }));
894
+ 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
895
  }
607
896
  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: [
897
+ 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
898
  { label: "Exit", value: "exit" },
610
899
  { label: "Stay", value: "stay" }
611
900
  ], onSelect: (item) => void onSelect(item.value) }) })] }));
612
901
  }
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)] })] }));
902
+ function Footer({ config, queuedPrompt, status, session }) {
903
+ const activeProvider = providerConfigFor(config, session.provider);
904
+ 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
905
  }
616
- function TranscriptCard({ card, assistantBuffer, liveLabel }) {
906
+ function TranscriptTurnCard({ card }) {
617
907
  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] }));
908
+ 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
909
  }
620
910
  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] }));
911
+ 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
912
  }
623
913
  function TranscriptSection({ entry }) {
624
914
  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}`)))] }));
915
+ const labelColor = entryColor(entry.kind);
916
+ 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
917
  }
627
918
  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] })] })] }));
919
+ 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
920
  }
630
921
  function cloneSession(session) {
631
922
  return {
@@ -638,14 +929,15 @@ function cloneSession(session) {
638
929
  messages: [...session.messages],
639
930
  referencedFiles: [...session.referencedFiles],
640
931
  readFiles: [...session.readFiles],
641
- pinnedSkills: [...session.pinnedSkills]
932
+ pinnedSkills: [...session.pinnedSkills],
933
+ edits: session.edits.map((edit) => ({ ...edit }))
642
934
  };
643
935
  }
644
936
  function formatSessionList(sessions) {
645
937
  return sessions.length > 0
646
938
  ? sessions
647
939
  .slice(0, 10)
648
- .map((item) => `${item.id} ${item.updatedAt} ${item.workspaceRoot}`)
940
+ .map((item) => `${item.id} ${item.provider}/${item.model} ${item.updatedAt} ${item.workspaceRoot}`)
649
941
  .join("\n")
650
942
  : "(no sessions)";
651
943
  }
@@ -667,18 +959,23 @@ function describeAuth(config) {
667
959
  return "missing";
668
960
  }
669
961
  }
670
- function formatReasoningEffort(value) {
962
+ function formatReasoningEffort(value, provider) {
963
+ if (!getProviderDefinition(provider).supportsReasoningEffort) {
964
+ return "provider default (model-specific)";
965
+ }
671
966
  return value ?? "(none)";
672
967
  }
673
968
  function describeSkills(pinnedCount) {
674
969
  return pinnedCount > 0 ? `${pinnedCount} pinned` : "none pinned";
675
970
  }
676
971
  function formatModelSetupSummary(state, config, authRetention) {
972
+ const profile = providerConfigFor(config, state.provider);
677
973
  const lines = [
974
+ `Provider: ${providerLabel(state.provider)}`,
678
975
  `Model: ${state.model}`,
679
- `Reasoning effort: ${formatReasoningEffort(state.reasoningEffort)}`,
680
- `Credential: ${describeAuth(config)}`,
681
- `Stored SHA-256: ${config.authFingerprint?.slice(0, 16) ?? "(none)"}`
976
+ `Reasoning effort: ${formatReasoningEffort(state.reasoningEffort, state.provider)}`,
977
+ `Credential: ${describeAuth(profile)}`,
978
+ `Stored SHA-256: ${profile.authFingerprint?.slice(0, 16) ?? "(none)"}`
682
979
  ];
683
980
  if (authRetention === "persist") {
684
981
  lines.push("Raw key is stored locally for all future sessions until /logout.");
@@ -706,6 +1003,20 @@ function summarizeUserPrompt(prompt) {
706
1003
  `Preview: ${preview}${preview.length < prompt.replace(/\s+/g, " ").trim().length ? "..." : ""}`
707
1004
  ].join("\n");
708
1005
  }
1006
+ function renderLiveAssistantBuffer(buffer) {
1007
+ if (!buffer) {
1008
+ return "";
1009
+ }
1010
+ const lines = buffer.split("\n");
1011
+ if (lines.length <= MAX_LIVE_ASSISTANT_LINES) {
1012
+ return buffer;
1013
+ }
1014
+ const hiddenLineCount = lines.length - MAX_LIVE_ASSISTANT_LINES;
1015
+ return [
1016
+ `[${hiddenLineCount} earlier line${hiddenLineCount === 1 ? "" : "s"} hidden while streaming]`,
1017
+ ...lines.slice(-MAX_LIVE_ASSISTANT_LINES)
1018
+ ].join("\n");
1019
+ }
709
1020
  function renderAuthMode(authMode) {
710
1021
  switch (authMode) {
711
1022
  case "bearer":
@@ -732,19 +1043,19 @@ function maskSecretPreview(value) {
732
1043
  function entryColor(kind) {
733
1044
  switch (kind) {
734
1045
  case "assistant":
735
- return "cyan";
1046
+ return UI_COLORS.accent;
736
1047
  case "user":
737
- return "green";
1048
+ return undefined;
738
1049
  case "tool":
739
- return "magenta";
1050
+ return UI_COLORS.accent;
740
1051
  case "activity":
741
- return "gray";
1052
+ return UI_COLORS.muted;
742
1053
  case "info":
743
- return "blue";
1054
+ return UI_COLORS.accent;
744
1055
  case "warn":
745
- return "yellow";
1056
+ return UI_COLORS.warning;
746
1057
  case "error":
747
- return "red";
1058
+ return UI_COLORS.danger;
748
1059
  }
749
1060
  }
750
1061
  function entryLabel(kind) {
@@ -767,23 +1078,20 @@ function entryLabel(kind) {
767
1078
  }
768
1079
  function transcriptCardBorder(entries) {
769
1080
  if (entries.some((entry) => entry.kind === "error")) {
770
- return "red";
1081
+ return UI_COLORS.danger;
771
1082
  }
772
1083
  if (entries.some((entry) => entry.kind === "warn")) {
773
- return "yellow";
1084
+ return UI_COLORS.warning;
774
1085
  }
775
1086
  if (entries.some((entry) => entry.kind === "tool")) {
776
- return "magenta";
777
- }
778
- if (entries.some((entry) => entry.kind === "assistant")) {
779
- return "white";
1087
+ return UI_COLORS.accent;
780
1088
  }
781
1089
  if (entries.some((entry) => entry.kind === "info")) {
782
- return "blue";
1090
+ return UI_COLORS.accent;
783
1091
  }
784
1092
  if (entries.some((entry) => entry.kind === "activity")) {
785
- return "gray";
1093
+ return UI_COLORS.muted;
786
1094
  }
787
- return "white";
1095
+ return UI_COLORS.border;
788
1096
  }
789
1097
  //# sourceMappingURL=repl-app.js.map