mini-coder 0.5.14 → 0.6.1

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