mini-coder 0.5.13 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +25 -108
  2. package/bin/mc.ts +8 -11
  3. package/bun.lock +79 -269
  4. package/package.json +17 -22
  5. package/src/agent.ts +242 -915
  6. package/src/args.ts +289 -0
  7. package/src/headless.ts +43 -385
  8. package/src/index.ts +29 -836
  9. package/src/oauth.ts +117 -0
  10. package/src/prompt.ts +227 -276
  11. package/src/session.ts +57 -961
  12. package/src/shared.ts +117 -38
  13. package/src/tool-bash.ts +110 -0
  14. package/src/tool-edit.ts +133 -0
  15. package/src/tool-task.ts +114 -0
  16. package/src/tui-components.ts +150 -0
  17. package/src/tui-conversation.ts +262 -0
  18. package/src/tui-editor.ts +29 -0
  19. package/src/tui-overlay.ts +403 -0
  20. package/src/tui.ts +236 -0
  21. package/src/types.ts +160 -0
  22. package/tsconfig.json +17 -0
  23. package/BENCHMARK.md +0 -107
  24. package/LICENSE +0 -9
  25. package/PROGRESS.md +0 -4
  26. package/assets/icon-1-minimal.svg +0 -31
  27. package/assets/icon-2-dark-terminal.svg +0 -48
  28. package/assets/icon-3-gradient-modern.svg +0 -45
  29. package/assets/icon-4-filled-bold.svg +0 -54
  30. package/assets/icon-5-community-badge.svg +0 -63
  31. package/assets/mc-claude-smart.png +0 -0
  32. package/assets/mc-gpt-smart.png +0 -0
  33. package/assets/preview-0-5-0.png +0 -0
  34. package/assets/preview.gif +0 -0
  35. package/benchmark-baseline.sh +0 -15
  36. package/benchmark-loop.sh +0 -19
  37. package/skills-lock.json +0 -15
  38. package/src/cli.ts +0 -134
  39. package/src/errors.ts +0 -15
  40. package/src/git.ts +0 -247
  41. package/src/input.ts +0 -168
  42. package/src/mcp.ts +0 -609
  43. package/src/paths.ts +0 -37
  44. package/src/session-message.ts +0 -393
  45. package/src/settings.ts +0 -449
  46. package/src/skills.ts +0 -271
  47. package/src/submit.ts +0 -371
  48. package/src/text.ts +0 -71
  49. package/src/theme.ts +0 -330
  50. package/src/tool-common.ts +0 -93
  51. package/src/tool-grep.ts +0 -606
  52. package/src/tool-read.ts +0 -313
  53. package/src/tool-shell.ts +0 -1001
  54. package/src/tools.ts +0 -854
  55. package/src/ui/agent.ts +0 -317
  56. package/src/ui/commands.test.ts +0 -913
  57. package/src/ui/commands.ts +0 -834
  58. package/src/ui/conversation.test.ts +0 -585
  59. package/src/ui/conversation.ts +0 -1836
  60. package/src/ui/help.ts +0 -158
  61. package/src/ui/input.test.ts +0 -64
  62. package/src/ui/input.ts +0 -138
  63. package/src/ui/overlay.ts +0 -59
  64. package/src/ui/runtime.ts +0 -69
  65. package/src/ui/status.ts +0 -220
  66. package/src/ui.ts +0 -1190
  67. package/src/version.ts +0 -48
@@ -1,834 +0,0 @@
1
- /**
2
- * Slash-command and overlay controllers for the terminal UI.
3
- *
4
- * Owns Select-overlay command flows (`/model`, `/session`, `/login`, etc.)
5
- * and stateful command mutations that operate on the current {@link AppState}.
6
- * Runtime-specific concerns such as rendering, overlay storage, and input draft
7
- * ownership are injected from `ui.ts` so this module can stay independent of
8
- * module-scoped UI state.
9
- *
10
- * @module
11
- */
12
-
13
- import { Select } from "@cel-tui/components";
14
- import type { Model, ThinkingLevel } from "@mariozechner/pi-ai";
15
- import type { OAuthProviderInterface } from "@mariozechner/pi-ai/oauth";
16
- import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
17
- import { getErrorMessage } from "../errors.ts";
18
- import type { AppState } from "../index.ts";
19
- import { getAvailableModels, saveOAuthCredentials } from "../index.ts";
20
- import { COMMANDS, SKILL_COMMAND } from "../input.ts";
21
- import { connectMcpServer, disconnectMcpServer } from "../mcp.ts";
22
- import {
23
- clearConversationState,
24
- forkSession,
25
- listPromptHistory,
26
- listSessions,
27
- loadMessages,
28
- replaceConversationState,
29
- type SessionListEntry,
30
- type UiInfoFormat,
31
- undoLastTurn,
32
- } from "../session.ts";
33
- import { updateSettings } from "../settings.ts";
34
- import { clearQueuedUserMessages } from "../submit.ts";
35
- import { collapseWhitespace, truncateText } from "../text.ts";
36
- import { getTodoItems } from "../tools.ts";
37
- import {
38
- buildHelpText,
39
- COMMAND_DESCRIPTIONS,
40
- SKILL_REFERENCE_DESCRIPTION,
41
- SKILL_REFERENCE_LABEL,
42
- } from "./help.ts";
43
- import { type ActiveOverlay, OVERLAY_MAX_VISIBLE } from "./overlay.ts";
44
- import type { UiRenderPriority } from "./runtime.ts";
45
- import { abbreviatePath } from "./status.ts";
46
-
47
- /** Effort levels available for selection. */
48
- const EFFORT_LEVELS: { label: string; value: ThinkingLevel }[] = [
49
- { label: "low", value: "low" },
50
- { label: "medium", value: "medium" },
51
- { label: "high", value: "high" },
52
- { label: "xhigh", value: "xhigh" },
53
- ];
54
-
55
- /** Runtime hooks injected from the stateful UI module. */
56
- interface UiCommandRuntime {
57
- /** Open an overlay. */
58
- openOverlay: (overlay: ActiveOverlay) => void;
59
- /** Dismiss the active overlay. */
60
- dismissOverlay: () => void;
61
- /** Update the current input draft. */
62
- setInputValue: (value: string) => void;
63
- /** Append a UI-only info message to the conversation log. */
64
- appendInfoMessage: (
65
- text: string,
66
- state: AppState,
67
- format?: UiInfoFormat,
68
- ) => void;
69
- /** Append a UI-only todo snapshot to the conversation log. */
70
- appendTodoMessage: (
71
- todos: ReturnType<typeof getTodoItems>,
72
- state: AppState,
73
- ) => void;
74
- /** Re-enable stick-to-bottom behavior for the conversation log. */
75
- scrollConversationToBottom: () => void;
76
- /** Schedule a UI re-render. */
77
- requestRender: (priority?: UiRenderPriority) => void;
78
- /** Reload prompt/session context at a boundary like `/new`. */
79
- reloadPromptContext: (state: AppState) => Promise<void>;
80
- /** Open a URL in the user's default browser. */
81
- openInBrowser: (url: string) => void;
82
- }
83
-
84
- interface UiCommandDeps {
85
- /** Connect a configured MCP server on demand. */
86
- connectMcpServer: (
87
- server: AppState["mcpServers"][number],
88
- ) => Promise<string[]>;
89
- /** Disconnect an MCP server and clear its imported tools. */
90
- disconnectMcpServer: (
91
- server: AppState["mcpServers"][number],
92
- ) => Promise<void>;
93
- }
94
-
95
- const defaultUiCommandDeps: UiCommandDeps = {
96
- connectMcpServer,
97
- disconnectMcpServer,
98
- };
99
-
100
- /** Public command actions consumed by `ui.ts` and unit tests. */
101
- interface UiCommandController {
102
- /** Apply a model selection and persist it to settings. */
103
- applyModelSelection: (
104
- state: AppState,
105
- model: AppState["model"] & NonNullable<AppState["model"]>,
106
- ) => void;
107
- /** Apply an effort selection and persist it to settings. */
108
- applyEffortSelection: (state: AppState, effort: ThinkingLevel) => void;
109
- /** Open the slash-command autocomplete overlay. */
110
- showCommandAutocomplete: (state: AppState) => void;
111
- /** Open the raw input-history overlay. */
112
- showInputHistoryOverlay: (state: AppState) => void;
113
- /** Dispatch a parsed command and report whether it was handled. */
114
- handleCommand: (command: string, state: AppState) => boolean;
115
- }
116
-
117
- /**
118
- * Apply a model selection to the active state and persisted settings.
119
- *
120
- * @param state - Application state.
121
- * @param model - Selected model.
122
- */
123
- function applyModelSelection(state: AppState, model: Model<string>): void {
124
- state.model = model;
125
- state.settings = updateSettings(state.settingsPath, {
126
- defaultModel: `${model.provider}/${model.id}`,
127
- });
128
- }
129
-
130
- /**
131
- * Apply an effort selection to the active state and persisted settings.
132
- *
133
- * @param state - Application state.
134
- * @param effort - Selected reasoning effort.
135
- */
136
- function applyEffortSelection(state: AppState, effort: ThinkingLevel): void {
137
- state.effort = effort;
138
- state.settings = updateSettings(state.settingsPath, {
139
- defaultEffort: effort,
140
- });
141
- }
142
-
143
- /**
144
- * Format a recent timestamp for display.
145
- *
146
- * @param date - Timestamp to format.
147
- * @param now - Reference time used to compute the relative label.
148
- * @returns Relative time text suitable for Select labels.
149
- */
150
- export function formatRelativeDate(date: Date, now = new Date()): string {
151
- const diffMs = now.getTime() - date.getTime();
152
- const diffMins = Math.floor(diffMs / 60_000);
153
- const diffHours = Math.floor(diffMs / 3_600_000);
154
- const diffDays = Math.floor(diffMs / 86_400_000);
155
-
156
- if (diffMins < 1) return "just now";
157
- if (diffMins < 60) return `${diffMins}m ago`;
158
- if (diffHours < 24) return `${diffHours}h ago`;
159
- if (diffDays < 7) return `${diffDays}d ago`;
160
- return date.toLocaleDateString();
161
- }
162
-
163
- /**
164
- * Collapse a raw prompt into a one-line preview for the Select overlay.
165
- *
166
- * @param text - Raw prompt text.
167
- * @returns A single-line preview string.
168
- */
169
- export function formatPromptHistoryPreview(text: string): string {
170
- return collapseWhitespace(text);
171
- }
172
-
173
- const HISTORY_PREVIEW_MAX_CHARS = 32;
174
- const HISTORY_CWD_MAX_CHARS = 18;
175
- const SESSION_PREVIEW_MAX_CHARS = 27;
176
- const SESSION_MODEL_MAX_CHARS = 17;
177
-
178
- /**
179
- * Format a prompt-history row for the Select overlay.
180
- *
181
- * @param text - Raw prompt text.
182
- * @param cwd - Prompt working directory.
183
- * @param date - Relative date label.
184
- * @returns A single-line prompt-history label with stable metadata suffixes.
185
- */
186
- export function formatPromptHistoryLabel(
187
- text: string,
188
- cwd: string,
189
- date: string,
190
- ): string {
191
- const preview = truncateText(
192
- formatPromptHistoryPreview(text),
193
- HISTORY_PREVIEW_MAX_CHARS,
194
- );
195
- const displayCwd = truncateText(
196
- abbreviatePath(cwd),
197
- HISTORY_CWD_MAX_CHARS,
198
- "start",
199
- );
200
- return `${preview} · ${displayCwd} · ${date}`;
201
- }
202
-
203
- /**
204
- * Format a session-picker row for the Select overlay.
205
- *
206
- * @param session - Session metadata and first-user preview.
207
- * @param date - Relative date label.
208
- * @param isCurrent - Whether this is the active session.
209
- * @returns A single-line session label with readable preview and metadata.
210
- */
211
- export function formatSessionLabel(
212
- session: Pick<SessionListEntry, "model" | "firstUserPreview">,
213
- date: string,
214
- isCurrent: boolean,
215
- ): string {
216
- const preview = truncateText(
217
- session.firstUserPreview ?? "No messages yet",
218
- SESSION_PREVIEW_MAX_CHARS,
219
- );
220
- const model = truncateText(
221
- session.model ?? "no model",
222
- SESSION_MODEL_MAX_CHARS,
223
- );
224
- const current = isCurrent ? " · current" : "";
225
- return `${preview} · ${model} · ${date}${current}`;
226
- }
227
-
228
- /** Lightweight item shape used by the shared Select-overlay helper. */
229
- interface OverlayItem {
230
- /** Label shown in the Select list. */
231
- label: string;
232
- /** Stable item value. */
233
- value: string;
234
- /** Search text used by Select filtering. */
235
- filterText: string;
236
- }
237
-
238
- function formatSkillLabel(skill: AppState["skills"][number]): string {
239
- return skill.description
240
- ? `${skill.name} · ${skill.description}`
241
- : skill.name;
242
- }
243
-
244
- function formatToolCount(count: number): string {
245
- return `${count} tool${count === 1 ? "" : "s"}`;
246
- }
247
-
248
- function formatMcpServerLabel(server: AppState["mcpServers"][number]): string {
249
- return `${server.name} · ${server.enabled ? "on" : "off"} · ${formatToolCount(server.tools.length)}`;
250
- }
251
-
252
- function formatMcpToggleMessage(
253
- server: AppState["mcpServers"][number],
254
- toolCount: number,
255
- ): string {
256
- const action = server.enabled ? "Enabled" : "Disabled";
257
- const delta = `${server.enabled ? "+" : "-"}${toolCount}`;
258
- const toolSuffix = toolCount === 1 ? "tool" : "tools";
259
- return `${action} MCP server "${server.name}" (${delta} ${toolSuffix}).`;
260
- }
261
-
262
- function isRepoLocalMcpServer(state: AppState, serverName: string): boolean {
263
- return (
264
- state.repoSettings.mcp?.servers?.some(
265
- (entry) => entry.name === serverName,
266
- ) ?? false
267
- );
268
- }
269
-
270
- function persistMcpServerSettings(
271
- state: AppState,
272
- server: AppState["mcpServers"][number],
273
- ): void {
274
- if (isRepoLocalMcpServer(state, server.name)) {
275
- return;
276
- }
277
-
278
- state.settings = updateSettings(state.settingsPath, {
279
- mcp: {
280
- servers: [
281
- {
282
- name: server.name,
283
- url: server.url,
284
- enabled: server.enabled,
285
- },
286
- ],
287
- },
288
- });
289
- }
290
-
291
- async function toggleMcpServer(
292
- server: AppState["mcpServers"][number],
293
- state: AppState,
294
- runtime: UiCommandRuntime,
295
- deps: UiCommandDeps,
296
- ): Promise<void> {
297
- try {
298
- if (server.enabled) {
299
- const toolCount = server.tools.length;
300
- await deps.disconnectMcpServer(server);
301
- server.enabled = false;
302
- persistMcpServerSettings(state, server);
303
- runtime.appendInfoMessage(
304
- formatMcpToggleMessage(server, toolCount),
305
- state,
306
- );
307
- runtime.requestRender("normal");
308
- return;
309
- }
310
-
311
- const warnings = await deps.connectMcpServer(server);
312
- if (!server.connected) {
313
- for (const warning of warnings) {
314
- runtime.appendInfoMessage(warning, state);
315
- }
316
- runtime.requestRender("normal");
317
- return;
318
- }
319
-
320
- server.enabled = true;
321
- persistMcpServerSettings(state, server);
322
- runtime.appendInfoMessage(
323
- formatMcpToggleMessage(server, server.tools.length),
324
- state,
325
- );
326
- for (const warning of warnings) {
327
- runtime.appendInfoMessage(warning, state);
328
- }
329
- runtime.requestRender("normal");
330
- } catch (error) {
331
- runtime.appendInfoMessage(
332
- `MCP server "${server.name}": ${getErrorMessage(error)}`,
333
- state,
334
- );
335
- runtime.requestRender("normal");
336
- }
337
- }
338
-
339
- /**
340
- * Create the UI command controller bound to the current UI runtime hooks.
341
- *
342
- * @param runtime - State mutation and rendering hooks owned by `ui.ts`.
343
- * @param deps - Internal MCP command dependencies, mainly for tests.
344
- * @returns Command actions for slash commands and overlays.
345
- */
346
- export function createCommandController(
347
- runtime: UiCommandRuntime,
348
- deps: UiCommandDeps = defaultUiCommandDeps,
349
- ): UiCommandController {
350
- const openSelectOverlay = (
351
- state: AppState,
352
- title: string,
353
- items: OverlayItem[],
354
- placeholder: string,
355
- onSelect: (value: string) => void,
356
- ): void => {
357
- const select = Select({
358
- items,
359
- maxVisible: OVERLAY_MAX_VISIBLE,
360
- placeholder,
361
- focused: true,
362
- highlightColor: state.theme.accentText,
363
- onSelect,
364
- onKeyPress: (key) => {
365
- if (key === "escape") {
366
- runtime.dismissOverlay();
367
- return;
368
- }
369
- return false;
370
- },
371
- onBlur: runtime.dismissOverlay,
372
- });
373
-
374
- runtime.openOverlay({ select, title });
375
- };
376
-
377
- const handleSkillCommand = (state: AppState): void => {
378
- const items = state.skills.map((skill) => ({
379
- label: formatSkillLabel(skill),
380
- value: skill.name,
381
- filterText: `${skill.name} ${skill.description ?? ""}`,
382
- }));
383
-
384
- openSelectOverlay(
385
- state,
386
- "Select a skill",
387
- items,
388
- "type to filter skills...",
389
- (skillName) => {
390
- runtime.setInputValue(`/${SKILL_COMMAND}:${skillName}`);
391
- runtime.dismissOverlay();
392
- },
393
- );
394
- };
395
-
396
- const showCommandAutocomplete = (state: AppState): void => {
397
- const items: OverlayItem[] = [
398
- ...COMMANDS.map((command) => ({
399
- label: `/${command} ${COMMAND_DESCRIPTIONS[command] ?? ""}`,
400
- value: command,
401
- filterText: command,
402
- })),
403
- {
404
- label: `${SKILL_REFERENCE_LABEL} ${SKILL_REFERENCE_DESCRIPTION}`,
405
- value: SKILL_COMMAND,
406
- filterText: `${SKILL_COMMAND} ${SKILL_REFERENCE_LABEL}`,
407
- },
408
- ];
409
-
410
- openSelectOverlay(
411
- state,
412
- "Commands",
413
- items,
414
- "type to filter commands...",
415
- (value) => {
416
- runtime.dismissOverlay();
417
- if (value === SKILL_COMMAND) {
418
- handleSkillCommand(state);
419
- return;
420
- }
421
-
422
- runtime.setInputValue("");
423
- handleCommand(value, state);
424
- },
425
- );
426
- };
427
-
428
- const showInputHistoryOverlay = (state: AppState): void => {
429
- const history = listPromptHistory(state.db);
430
- if (history.length === 0) {
431
- return;
432
- }
433
-
434
- const items = history.map((entry) => {
435
- const date = formatRelativeDate(new Date(entry.createdAt));
436
- return {
437
- label: formatPromptHistoryLabel(entry.text, entry.cwd, date),
438
- value: String(entry.id),
439
- filterText: `${entry.text} ${entry.cwd}`,
440
- };
441
- });
442
-
443
- openSelectOverlay(
444
- state,
445
- "Input history",
446
- items,
447
- "type to filter history...",
448
- (value) => {
449
- const picked = history.find((entry) => String(entry.id) === value);
450
- if (picked) {
451
- runtime.setInputValue(picked.text);
452
- }
453
- runtime.dismissOverlay();
454
- },
455
- );
456
- };
457
-
458
- const handleModelCommand = (state: AppState): void => {
459
- const models = getAvailableModels(state);
460
- if (models.length === 0) {
461
- return;
462
- }
463
-
464
- const currentValue = state.model
465
- ? `${state.model.provider}/${state.model.id}`
466
- : null;
467
- const items = models.map((model) => {
468
- const value = `${model.provider}/${model.id}`;
469
- const current = value === currentValue ? " (current)" : "";
470
- return {
471
- label: `${model.provider}/${model.id}${current}`,
472
- value,
473
- filterText: `${model.provider} ${model.id}`,
474
- };
475
- });
476
-
477
- openSelectOverlay(
478
- state,
479
- "Select a model",
480
- items,
481
- "type to filter models...",
482
- (value) => {
483
- const picked = models.find(
484
- (model) => `${model.provider}/${model.id}` === value,
485
- );
486
- if (picked) {
487
- applyModelSelection(state, picked);
488
- }
489
- runtime.dismissOverlay();
490
- },
491
- );
492
- };
493
-
494
- const handleEffortCommand = (state: AppState): void => {
495
- const items = EFFORT_LEVELS.map((effort) => ({
496
- label:
497
- effort.value === state.effort
498
- ? `${effort.label} (current)`
499
- : effort.label,
500
- value: effort.value,
501
- filterText: effort.label,
502
- }));
503
-
504
- openSelectOverlay(
505
- state,
506
- "Select effort level",
507
- items,
508
- "type to filter...",
509
- (value) => {
510
- applyEffortSelection(state, value as ThinkingLevel);
511
- runtime.dismissOverlay();
512
- },
513
- );
514
- };
515
-
516
- const handleSessionCommand = (state: AppState): void => {
517
- const sessions = listSessions(state.db, state.canonicalCwd);
518
- if (sessions.length === 0) {
519
- return;
520
- }
521
-
522
- const currentSessionId = state.session?.id ?? null;
523
- const items = sessions.map((session) => {
524
- const dateStr = formatRelativeDate(new Date(session.updatedAt));
525
- const model = session.model ?? "no model";
526
- return {
527
- label: formatSessionLabel(
528
- session,
529
- dateStr,
530
- session.id === currentSessionId,
531
- ),
532
- value: session.id,
533
- filterText: `${session.firstUserPreview ?? ""} ${model} ${dateStr}`,
534
- };
535
- });
536
-
537
- openSelectOverlay(
538
- state,
539
- "Resume a session",
540
- items,
541
- "type to filter sessions...",
542
- (sessionId) => {
543
- clearQueuedUserMessages(state);
544
- if (sessionId !== currentSessionId) {
545
- const picked = sessions.find((session) => session.id === sessionId);
546
- if (picked) {
547
- state.session = picked;
548
- replaceConversationState(state, loadMessages(state.db, picked.id));
549
- runtime.scrollConversationToBottom();
550
- }
551
- }
552
- runtime.dismissOverlay();
553
- runtime.requestRender("normal");
554
- },
555
- );
556
- };
557
-
558
- const handleNewCommand = async (state: AppState): Promise<void> => {
559
- if (state.running) {
560
- return;
561
- }
562
- clearQueuedUserMessages(state);
563
- state.session = null;
564
- clearConversationState(state);
565
- await runtime.reloadPromptContext(state);
566
- runtime.scrollConversationToBottom();
567
- runtime.requestRender("normal");
568
- };
569
-
570
- const handleForkCommand = (state: AppState): void => {
571
- if (state.running || !state.session) {
572
- return;
573
- }
574
- const forked = forkSession(state.db, state.session.id);
575
- state.session = forked;
576
- replaceConversationState(state, loadMessages(state.db, forked.id));
577
- runtime.appendInfoMessage("Forked session.", state);
578
- };
579
-
580
- const handleUndoCommand = async (state: AppState): Promise<void> => {
581
- clearQueuedUserMessages(state);
582
-
583
- if (state.running && state.abortController) {
584
- state.abortController.abort();
585
- }
586
-
587
- if (state.activeTurnPromise) {
588
- await state.activeTurnPromise;
589
- }
590
-
591
- if (!state.session) {
592
- return;
593
- }
594
- const removed = undoLastTurn(state.db, state.session.id);
595
- if (removed) {
596
- replaceConversationState(state, loadMessages(state.db, state.session.id));
597
- runtime.scrollConversationToBottom();
598
- runtime.requestRender("normal");
599
- }
600
- };
601
-
602
- const handleReasoningCommand = (state: AppState): void => {
603
- state.showReasoning = !state.showReasoning;
604
- state.settings = updateSettings(state.settingsPath, {
605
- showReasoning: state.showReasoning,
606
- });
607
- };
608
-
609
- const handleVerboseCommand = (state: AppState): void => {
610
- state.verbose = !state.verbose;
611
- state.settings = updateSettings(state.settingsPath, {
612
- verbose: state.verbose,
613
- });
614
- };
615
-
616
- const handleMcpCommand = (state: AppState): void => {
617
- if (state.mcpServers.length === 0) {
618
- return;
619
- }
620
-
621
- const items = state.mcpServers.map((server) => ({
622
- label: formatMcpServerLabel(server),
623
- value: server.name,
624
- filterText: `${server.name} ${server.url} ${server.enabled ? "on" : "off"}`,
625
- }));
626
-
627
- openSelectOverlay(
628
- state,
629
- "Toggle MCP servers",
630
- items,
631
- "type to filter MCP servers...",
632
- (serverName) => {
633
- const server = state.mcpServers.find(
634
- (entry) => entry.name === serverName,
635
- );
636
- runtime.dismissOverlay();
637
- if (server) {
638
- void toggleMcpServer(server, state, runtime, deps);
639
- }
640
- },
641
- );
642
- };
643
-
644
- const performLogin = async (
645
- provider: OAuthProviderInterface,
646
- state: AppState,
647
- ): Promise<void> => {
648
- runtime.appendInfoMessage(`Logging in to ${provider.name}...`, state);
649
-
650
- const credentials = await provider.login({
651
- onAuth: (info) => {
652
- runtime.openInBrowser(info.url);
653
- runtime.appendInfoMessage(
654
- info.instructions ?? "Opening browser for login...",
655
- state,
656
- );
657
- },
658
- onPrompt: () => {
659
- return Promise.reject(new Error("Manual code input is not supported."));
660
- },
661
- onProgress: (message) => {
662
- runtime.appendInfoMessage(message, state);
663
- },
664
- });
665
-
666
- state.oauthCredentials[provider.id] = credentials;
667
- saveOAuthCredentials(state.oauthCredentials);
668
-
669
- const apiKey = provider.getApiKey(credentials);
670
- state.providers.set(provider.id, apiKey);
671
-
672
- if (!state.model) {
673
- const models = getAvailableModels(state);
674
- if (models.length > 0) {
675
- state.model = models[0]!;
676
- }
677
- }
678
-
679
- runtime.appendInfoMessage(`Logged in to ${provider.name}.`, state);
680
- };
681
-
682
- const handleLoginCommand = (state: AppState): void => {
683
- const oauthProviders = getOAuthProviders();
684
- if (oauthProviders.length === 0) {
685
- return;
686
- }
687
-
688
- const items = oauthProviders.map((provider) => {
689
- const loggedIn = state.oauthCredentials[provider.id] != null;
690
- const status = loggedIn ? " (logged in)" : "";
691
- return {
692
- label: `${provider.name}${status}`,
693
- value: provider.id,
694
- filterText: provider.name,
695
- };
696
- });
697
-
698
- openSelectOverlay(
699
- state,
700
- "Login to a provider",
701
- items,
702
- "type to filter providers...",
703
- (providerId) => {
704
- runtime.dismissOverlay();
705
- const provider = oauthProviders.find(
706
- (entry) => entry.id === providerId,
707
- );
708
- if (provider) {
709
- performLogin(provider, state).catch((err) => {
710
- runtime.appendInfoMessage(
711
- `Login failed: ${getErrorMessage(err)}`,
712
- state,
713
- );
714
- });
715
- }
716
- },
717
- );
718
- };
719
-
720
- const handleLogoutCommand = (state: AppState): void => {
721
- const loggedInProviders = getOAuthProviders().filter(
722
- (provider) => state.oauthCredentials[provider.id] != null,
723
- );
724
- if (loggedInProviders.length === 0) {
725
- return;
726
- }
727
-
728
- const items = loggedInProviders.map((provider) => ({
729
- label: provider.name,
730
- value: provider.id,
731
- filterText: provider.name,
732
- }));
733
-
734
- openSelectOverlay(
735
- state,
736
- "Logout from a provider",
737
- items,
738
- "type to filter providers...",
739
- (providerId) => {
740
- delete state.oauthCredentials[providerId];
741
- saveOAuthCredentials(state.oauthCredentials);
742
- state.providers.delete(providerId);
743
-
744
- if (state.model && state.model.provider === providerId) {
745
- state.model = null;
746
- }
747
-
748
- const provider = loggedInProviders.find(
749
- (entry) => entry.id === providerId,
750
- );
751
- runtime.dismissOverlay();
752
- runtime.appendInfoMessage(
753
- `Logged out of ${provider?.name ?? providerId}.`,
754
- state,
755
- );
756
- },
757
- );
758
- };
759
-
760
- const handleHelpCommand = (state: AppState): void => {
761
- runtime.appendInfoMessage(buildHelpText(state), state, "markdown");
762
- };
763
-
764
- const handleTodoCommand = (state: AppState): void => {
765
- runtime.appendTodoMessage(getTodoItems(state.messages), state);
766
- };
767
-
768
- const handleCommand = (command: string, state: AppState): boolean => {
769
- switch (command) {
770
- case "model":
771
- handleModelCommand(state);
772
- return true;
773
- case "effort":
774
- handleEffortCommand(state);
775
- return true;
776
- case "session":
777
- handleSessionCommand(state);
778
- return true;
779
- case "login":
780
- handleLoginCommand(state);
781
- return true;
782
- case "logout":
783
- handleLogoutCommand(state);
784
- return true;
785
- case "new":
786
- handleNewCommand(state).catch((error) => {
787
- runtime.appendInfoMessage(
788
- `New session failed: ${getErrorMessage(error)}`,
789
- state,
790
- );
791
- });
792
- return true;
793
- case "fork":
794
- handleForkCommand(state);
795
- return true;
796
- case "undo":
797
- handleUndoCommand(state).catch((error) => {
798
- runtime.appendInfoMessage(
799
- `Undo failed: ${getErrorMessage(error)}`,
800
- state,
801
- );
802
- });
803
- return true;
804
- case "reasoning":
805
- handleReasoningCommand(state);
806
- return true;
807
- case "verbose":
808
- handleVerboseCommand(state);
809
- return true;
810
- case "mcp":
811
- handleMcpCommand(state);
812
- return true;
813
- case "todo":
814
- handleTodoCommand(state);
815
- return true;
816
- case "help":
817
- handleHelpCommand(state);
818
- return true;
819
- case SKILL_COMMAND:
820
- handleSkillCommand(state);
821
- return true;
822
- default:
823
- return false;
824
- }
825
- };
826
-
827
- return {
828
- applyModelSelection,
829
- applyEffortSelection,
830
- showCommandAutocomplete,
831
- showInputHistoryOverlay,
832
- handleCommand,
833
- };
834
- }