mini-coder 0.4.1 → 0.5.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.
- package/README.md +87 -48
- package/assets/icon-1-minimal.svg +31 -0
- package/assets/icon-2-dark-terminal.svg +48 -0
- package/assets/icon-3-gradient-modern.svg +45 -0
- package/assets/icon-4-filled-bold.svg +54 -0
- package/assets/icon-5-community-badge.svg +63 -0
- package/assets/preview-0-5-0.png +0 -0
- package/assets/preview.gif +0 -0
- package/bin/mc.ts +14 -0
- package/bun.lock +438 -0
- package/package.json +12 -29
- package/src/agent.ts +592 -0
- package/src/cli.ts +124 -0
- package/src/git.ts +164 -0
- package/src/headless.ts +140 -0
- package/src/index.ts +645 -0
- package/src/input.ts +155 -0
- package/src/paths.ts +37 -0
- package/src/plugins.ts +183 -0
- package/src/prompt.ts +294 -0
- package/src/session.ts +838 -0
- package/src/settings.ts +184 -0
- package/src/skills.ts +258 -0
- package/src/submit.ts +323 -0
- package/src/theme.ts +147 -0
- package/src/tools.ts +636 -0
- package/src/ui/agent.test.ts +49 -0
- package/src/ui/agent.ts +210 -0
- package/src/ui/commands.test.ts +610 -0
- package/src/ui/commands.ts +638 -0
- package/src/ui/conversation.test.ts +892 -0
- package/src/ui/conversation.ts +926 -0
- package/src/ui/help.test.ts +26 -0
- package/src/ui/help.ts +119 -0
- package/src/ui/input.test.ts +74 -0
- package/src/ui/input.ts +138 -0
- package/src/ui/overlay.test.ts +42 -0
- package/src/ui/overlay.ts +59 -0
- package/src/ui/status.test.ts +450 -0
- package/src/ui/status.ts +357 -0
- package/src/ui.ts +615 -0
- package/.claude/settings.local.json +0 -54
- package/.prettierignore +0 -7
- package/dist/mc-edit.js +0 -275
- package/dist/mc.js +0 -7355
- package/docs/KNOWN_ISSUES.md +0 -13
- package/docs/design-decisions.md +0 -31
- package/docs/mini-coder.1.md +0 -227
- package/docs/superpowers/plans/2026-03-30-anthropic-oauth-removal.md +0 -61
- package/docs/superpowers/specs/2026-03-30-anthropic-oauth-removal-design.md +0 -47
- package/lefthook.yml +0 -4
|
@@ -0,0 +1,638 @@
|
|
|
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 type { AppState } from "../index.ts";
|
|
18
|
+
import { getAvailableModels, saveOAuthCredentials } from "../index.ts";
|
|
19
|
+
import { COMMANDS } from "../input.ts";
|
|
20
|
+
import {
|
|
21
|
+
computeStats,
|
|
22
|
+
forkSession,
|
|
23
|
+
listPromptHistory,
|
|
24
|
+
listSessions,
|
|
25
|
+
loadMessages,
|
|
26
|
+
type SessionListEntry,
|
|
27
|
+
undoLastTurn,
|
|
28
|
+
} from "../session.ts";
|
|
29
|
+
import { updateSettings } from "../settings.ts";
|
|
30
|
+
import { buildHelpText, COMMAND_DESCRIPTIONS } from "./help.ts";
|
|
31
|
+
import { type ActiveOverlay, OVERLAY_MAX_VISIBLE } from "./overlay.ts";
|
|
32
|
+
import { abbreviatePath } from "./status.ts";
|
|
33
|
+
|
|
34
|
+
/** Effort levels available for selection. */
|
|
35
|
+
const EFFORT_LEVELS: { label: string; value: ThinkingLevel }[] = [
|
|
36
|
+
{ label: "low", value: "low" },
|
|
37
|
+
{ label: "medium", value: "medium" },
|
|
38
|
+
{ label: "high", value: "high" },
|
|
39
|
+
{ label: "xhigh", value: "xhigh" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function getErrorMessage(error: unknown): string {
|
|
43
|
+
return error instanceof Error ? error.message : String(error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Runtime hooks injected from the stateful UI module. */
|
|
47
|
+
interface UiCommandRuntime {
|
|
48
|
+
/** Open an overlay and trigger a re-render. */
|
|
49
|
+
openOverlay: (overlay: ActiveOverlay) => void;
|
|
50
|
+
/** Dismiss the active overlay and trigger a re-render. */
|
|
51
|
+
dismissOverlay: () => void;
|
|
52
|
+
/** Update the current input draft. */
|
|
53
|
+
setInputValue: (value: string) => void;
|
|
54
|
+
/** Append a UI-only info message to the conversation log. */
|
|
55
|
+
appendInfoMessage: (text: string, state: AppState) => void;
|
|
56
|
+
/** Re-enable stick-to-bottom behavior for the conversation log. */
|
|
57
|
+
scrollConversationToBottom: () => void;
|
|
58
|
+
/** Trigger a UI re-render. */
|
|
59
|
+
render: () => void;
|
|
60
|
+
/** Reload prompt/session context at a boundary like `/new`. */
|
|
61
|
+
reloadPromptContext: (state: AppState) => Promise<void>;
|
|
62
|
+
/** Open a URL in the user's default browser. */
|
|
63
|
+
openInBrowser: (url: string) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Public command actions consumed by `ui.ts` and unit tests. */
|
|
67
|
+
interface UiCommandController {
|
|
68
|
+
/** Apply a model selection and persist it to settings. */
|
|
69
|
+
applyModelSelection: (
|
|
70
|
+
state: AppState,
|
|
71
|
+
model: AppState["model"] & NonNullable<AppState["model"]>,
|
|
72
|
+
) => void;
|
|
73
|
+
/** Apply an effort selection and persist it to settings. */
|
|
74
|
+
applyEffortSelection: (state: AppState, effort: ThinkingLevel) => void;
|
|
75
|
+
/** Open the slash-command autocomplete overlay. */
|
|
76
|
+
showCommandAutocomplete: (state: AppState) => void;
|
|
77
|
+
/** Open the raw input-history overlay. */
|
|
78
|
+
showInputHistoryOverlay: (state: AppState) => void;
|
|
79
|
+
/** Dispatch a parsed command and report whether it was handled. */
|
|
80
|
+
handleCommand: (command: string, state: AppState) => boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Apply a model selection to the active state and persisted settings.
|
|
85
|
+
*
|
|
86
|
+
* @param state - Application state.
|
|
87
|
+
* @param model - Selected model.
|
|
88
|
+
*/
|
|
89
|
+
function applyModelSelection(state: AppState, model: Model<string>): void {
|
|
90
|
+
state.model = model;
|
|
91
|
+
state.settings = updateSettings(state.settingsPath, {
|
|
92
|
+
defaultModel: `${model.provider}/${model.id}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Apply an effort selection to the active state and persisted settings.
|
|
98
|
+
*
|
|
99
|
+
* @param state - Application state.
|
|
100
|
+
* @param effort - Selected reasoning effort.
|
|
101
|
+
*/
|
|
102
|
+
function applyEffortSelection(state: AppState, effort: ThinkingLevel): void {
|
|
103
|
+
state.effort = effort;
|
|
104
|
+
state.settings = updateSettings(state.settingsPath, {
|
|
105
|
+
defaultEffort: effort,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Format a recent timestamp for display.
|
|
111
|
+
*
|
|
112
|
+
* @param date - Timestamp to format.
|
|
113
|
+
* @param now - Reference time used to compute the relative label.
|
|
114
|
+
* @returns Relative time text suitable for Select labels.
|
|
115
|
+
*/
|
|
116
|
+
export function formatRelativeDate(date: Date, now = new Date()): string {
|
|
117
|
+
const diffMs = now.getTime() - date.getTime();
|
|
118
|
+
const diffMins = Math.floor(diffMs / 60_000);
|
|
119
|
+
const diffHours = Math.floor(diffMs / 3_600_000);
|
|
120
|
+
const diffDays = Math.floor(diffMs / 86_400_000);
|
|
121
|
+
|
|
122
|
+
if (diffMins < 1) return "just now";
|
|
123
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
124
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
125
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
126
|
+
return date.toLocaleDateString();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Collapse a raw prompt into a one-line preview for the Select overlay.
|
|
131
|
+
*
|
|
132
|
+
* @param text - Raw prompt text.
|
|
133
|
+
* @returns A single-line preview string.
|
|
134
|
+
*/
|
|
135
|
+
export function formatPromptHistoryPreview(text: string): string {
|
|
136
|
+
return text.replace(/\s+/g, " ").trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const HISTORY_PREVIEW_MAX_CHARS = 32;
|
|
140
|
+
const HISTORY_CWD_MAX_CHARS = 18;
|
|
141
|
+
const SESSION_PREVIEW_MAX_CHARS = 27;
|
|
142
|
+
const SESSION_MODEL_MAX_CHARS = 17;
|
|
143
|
+
|
|
144
|
+
function truncateTrailingText(text: string, maxChars: number): string {
|
|
145
|
+
if (text.length <= maxChars) {
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
if (maxChars <= 1) {
|
|
149
|
+
return "…";
|
|
150
|
+
}
|
|
151
|
+
return `${text.slice(0, maxChars - 1)}…`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function truncateLeadingText(text: string, maxChars: number): string {
|
|
155
|
+
if (text.length <= maxChars) {
|
|
156
|
+
return text;
|
|
157
|
+
}
|
|
158
|
+
if (maxChars <= 1) {
|
|
159
|
+
return "…";
|
|
160
|
+
}
|
|
161
|
+
return `…${text.slice(text.length - (maxChars - 1))}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Format a prompt-history row for the Select overlay.
|
|
166
|
+
*
|
|
167
|
+
* @param text - Raw prompt text.
|
|
168
|
+
* @param cwd - Prompt working directory.
|
|
169
|
+
* @param date - Relative date label.
|
|
170
|
+
* @returns A single-line prompt-history label with stable metadata suffixes.
|
|
171
|
+
*/
|
|
172
|
+
export function formatPromptHistoryLabel(
|
|
173
|
+
text: string,
|
|
174
|
+
cwd: string,
|
|
175
|
+
date: string,
|
|
176
|
+
): string {
|
|
177
|
+
const preview = truncateTrailingText(
|
|
178
|
+
formatPromptHistoryPreview(text),
|
|
179
|
+
HISTORY_PREVIEW_MAX_CHARS,
|
|
180
|
+
);
|
|
181
|
+
const displayCwd = truncateLeadingText(
|
|
182
|
+
abbreviatePath(cwd),
|
|
183
|
+
HISTORY_CWD_MAX_CHARS,
|
|
184
|
+
);
|
|
185
|
+
return `${preview} · ${displayCwd} · ${date}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Format a session-picker row for the Select overlay.
|
|
190
|
+
*
|
|
191
|
+
* @param session - Session metadata and first-user preview.
|
|
192
|
+
* @param date - Relative date label.
|
|
193
|
+
* @param isCurrent - Whether this is the active session.
|
|
194
|
+
* @returns A single-line session label with readable preview and metadata.
|
|
195
|
+
*/
|
|
196
|
+
export function formatSessionLabel(
|
|
197
|
+
session: Pick<SessionListEntry, "model" | "firstUserPreview">,
|
|
198
|
+
date: string,
|
|
199
|
+
isCurrent: boolean,
|
|
200
|
+
): string {
|
|
201
|
+
const preview = truncateTrailingText(
|
|
202
|
+
session.firstUserPreview ?? "No messages yet",
|
|
203
|
+
SESSION_PREVIEW_MAX_CHARS,
|
|
204
|
+
);
|
|
205
|
+
const model = truncateTrailingText(
|
|
206
|
+
session.model ?? "no model",
|
|
207
|
+
SESSION_MODEL_MAX_CHARS,
|
|
208
|
+
);
|
|
209
|
+
const current = isCurrent ? " · current" : "";
|
|
210
|
+
return `${preview} · ${model} · ${date}${current}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Lightweight item shape used by the shared Select-overlay helper. */
|
|
214
|
+
interface OverlayItem {
|
|
215
|
+
/** Label shown in the Select list. */
|
|
216
|
+
label: string;
|
|
217
|
+
/** Stable item value. */
|
|
218
|
+
value: string;
|
|
219
|
+
/** Search text used by Select filtering. */
|
|
220
|
+
filterText: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create the UI command controller bound to the current UI runtime hooks.
|
|
225
|
+
*
|
|
226
|
+
* @param runtime - State mutation and rendering hooks owned by `ui.ts`.
|
|
227
|
+
* @returns Command actions for slash commands and overlays.
|
|
228
|
+
*/
|
|
229
|
+
export function createCommandController(
|
|
230
|
+
runtime: UiCommandRuntime,
|
|
231
|
+
): UiCommandController {
|
|
232
|
+
const openSelectOverlay = (
|
|
233
|
+
state: AppState,
|
|
234
|
+
title: string,
|
|
235
|
+
items: OverlayItem[],
|
|
236
|
+
placeholder: string,
|
|
237
|
+
onSelect: (value: string) => void,
|
|
238
|
+
): void => {
|
|
239
|
+
const select = Select({
|
|
240
|
+
items,
|
|
241
|
+
maxVisible: OVERLAY_MAX_VISIBLE,
|
|
242
|
+
placeholder,
|
|
243
|
+
focused: true,
|
|
244
|
+
highlightColor: state.theme.accentText,
|
|
245
|
+
onSelect,
|
|
246
|
+
onBlur: runtime.dismissOverlay,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
runtime.openOverlay({ select, title });
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const showCommandAutocomplete = (state: AppState): void => {
|
|
253
|
+
const items = COMMANDS.map((command) => ({
|
|
254
|
+
label: `/${command} ${COMMAND_DESCRIPTIONS[command] ?? ""}`,
|
|
255
|
+
value: command,
|
|
256
|
+
filterText: command,
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
runtime.setInputValue("");
|
|
260
|
+
openSelectOverlay(
|
|
261
|
+
state,
|
|
262
|
+
"Commands",
|
|
263
|
+
items,
|
|
264
|
+
"type to filter commands...",
|
|
265
|
+
(value) => {
|
|
266
|
+
runtime.dismissOverlay();
|
|
267
|
+
handleCommand(value, state);
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const showInputHistoryOverlay = (state: AppState): void => {
|
|
273
|
+
const history = listPromptHistory(state.db);
|
|
274
|
+
if (history.length === 0) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const items = history.map((entry) => {
|
|
279
|
+
const date = formatRelativeDate(new Date(entry.createdAt));
|
|
280
|
+
return {
|
|
281
|
+
label: formatPromptHistoryLabel(entry.text, entry.cwd, date),
|
|
282
|
+
value: String(entry.id),
|
|
283
|
+
filterText: `${entry.text} ${entry.cwd}`,
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
openSelectOverlay(
|
|
288
|
+
state,
|
|
289
|
+
"Input history",
|
|
290
|
+
items,
|
|
291
|
+
"type to filter history...",
|
|
292
|
+
(value) => {
|
|
293
|
+
const picked = history.find((entry) => String(entry.id) === value);
|
|
294
|
+
if (picked) {
|
|
295
|
+
runtime.setInputValue(picked.text);
|
|
296
|
+
}
|
|
297
|
+
runtime.dismissOverlay();
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const handleModelCommand = (state: AppState): void => {
|
|
303
|
+
const models = getAvailableModels(state);
|
|
304
|
+
if (models.length === 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const currentValue = state.model
|
|
309
|
+
? `${state.model.provider}/${state.model.id}`
|
|
310
|
+
: null;
|
|
311
|
+
const items = models.map((model) => {
|
|
312
|
+
const value = `${model.provider}/${model.id}`;
|
|
313
|
+
const current = value === currentValue ? " (current)" : "";
|
|
314
|
+
return {
|
|
315
|
+
label: `${model.provider}/${model.id}${current}`,
|
|
316
|
+
value,
|
|
317
|
+
filterText: `${model.provider} ${model.id}`,
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
openSelectOverlay(
|
|
322
|
+
state,
|
|
323
|
+
"Select a model",
|
|
324
|
+
items,
|
|
325
|
+
"type to filter models...",
|
|
326
|
+
(value) => {
|
|
327
|
+
const picked = models.find(
|
|
328
|
+
(model) => `${model.provider}/${model.id}` === value,
|
|
329
|
+
);
|
|
330
|
+
if (picked) {
|
|
331
|
+
applyModelSelection(state, picked);
|
|
332
|
+
}
|
|
333
|
+
runtime.dismissOverlay();
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const handleEffortCommand = (state: AppState): void => {
|
|
339
|
+
const items = EFFORT_LEVELS.map((effort) => ({
|
|
340
|
+
label:
|
|
341
|
+
effort.value === state.effort
|
|
342
|
+
? `${effort.label} (current)`
|
|
343
|
+
: effort.label,
|
|
344
|
+
value: effort.value,
|
|
345
|
+
filterText: effort.label,
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
openSelectOverlay(
|
|
349
|
+
state,
|
|
350
|
+
"Select effort level",
|
|
351
|
+
items,
|
|
352
|
+
"type to filter...",
|
|
353
|
+
(value) => {
|
|
354
|
+
applyEffortSelection(state, value as ThinkingLevel);
|
|
355
|
+
runtime.dismissOverlay();
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const handleSessionCommand = (state: AppState): void => {
|
|
361
|
+
const sessions = listSessions(state.db, state.canonicalCwd);
|
|
362
|
+
if (sessions.length === 0) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const currentSessionId = state.session?.id ?? null;
|
|
367
|
+
const items = sessions.map((session) => {
|
|
368
|
+
const dateStr = formatRelativeDate(new Date(session.updatedAt));
|
|
369
|
+
const model = session.model ?? "no model";
|
|
370
|
+
return {
|
|
371
|
+
label: formatSessionLabel(
|
|
372
|
+
session,
|
|
373
|
+
dateStr,
|
|
374
|
+
session.id === currentSessionId,
|
|
375
|
+
),
|
|
376
|
+
value: session.id,
|
|
377
|
+
filterText: `${session.firstUserPreview ?? ""} ${model} ${dateStr}`,
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
openSelectOverlay(
|
|
382
|
+
state,
|
|
383
|
+
"Resume a session",
|
|
384
|
+
items,
|
|
385
|
+
"type to filter sessions...",
|
|
386
|
+
(sessionId) => {
|
|
387
|
+
if (sessionId !== currentSessionId) {
|
|
388
|
+
const picked = sessions.find((session) => session.id === sessionId);
|
|
389
|
+
if (picked) {
|
|
390
|
+
state.session = picked;
|
|
391
|
+
state.messages = loadMessages(state.db, picked.id);
|
|
392
|
+
state.stats = computeStats(state.messages);
|
|
393
|
+
runtime.scrollConversationToBottom();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
runtime.dismissOverlay();
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const handleNewCommand = async (state: AppState): Promise<void> => {
|
|
402
|
+
if (state.running) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
state.session = null;
|
|
406
|
+
state.messages = [];
|
|
407
|
+
state.stats = { totalInput: 0, totalOutput: 0, totalCost: 0 };
|
|
408
|
+
await runtime.reloadPromptContext(state);
|
|
409
|
+
runtime.scrollConversationToBottom();
|
|
410
|
+
runtime.render();
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const handleForkCommand = (state: AppState): void => {
|
|
414
|
+
if (state.running || !state.session) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const forked = forkSession(state.db, state.session.id);
|
|
418
|
+
state.session = forked;
|
|
419
|
+
state.messages = loadMessages(state.db, forked.id);
|
|
420
|
+
state.stats = computeStats(state.messages);
|
|
421
|
+
runtime.appendInfoMessage("Forked session.", state);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const handleUndoCommand = async (state: AppState): Promise<void> => {
|
|
425
|
+
if (state.running && state.abortController) {
|
|
426
|
+
state.abortController.abort();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (state.activeTurnPromise) {
|
|
430
|
+
await state.activeTurnPromise;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!state.session) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const removed = undoLastTurn(state.db, state.session.id);
|
|
437
|
+
if (removed) {
|
|
438
|
+
state.messages = loadMessages(state.db, state.session.id);
|
|
439
|
+
state.stats = computeStats(state.messages);
|
|
440
|
+
runtime.scrollConversationToBottom();
|
|
441
|
+
runtime.render();
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const handleReasoningCommand = (state: AppState): void => {
|
|
446
|
+
state.showReasoning = !state.showReasoning;
|
|
447
|
+
state.settings = updateSettings(state.settingsPath, {
|
|
448
|
+
showReasoning: state.showReasoning,
|
|
449
|
+
});
|
|
450
|
+
runtime.render();
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const handleVerboseCommand = (state: AppState): void => {
|
|
454
|
+
state.verbose = !state.verbose;
|
|
455
|
+
state.settings = updateSettings(state.settingsPath, {
|
|
456
|
+
verbose: state.verbose,
|
|
457
|
+
});
|
|
458
|
+
runtime.render();
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const performLogin = async (
|
|
462
|
+
provider: OAuthProviderInterface,
|
|
463
|
+
state: AppState,
|
|
464
|
+
): Promise<void> => {
|
|
465
|
+
runtime.appendInfoMessage(`Logging in to ${provider.name}...`, state);
|
|
466
|
+
|
|
467
|
+
const credentials = await provider.login({
|
|
468
|
+
onAuth: (info) => {
|
|
469
|
+
runtime.openInBrowser(info.url);
|
|
470
|
+
runtime.appendInfoMessage(
|
|
471
|
+
info.instructions ?? "Opening browser for login...",
|
|
472
|
+
state,
|
|
473
|
+
);
|
|
474
|
+
},
|
|
475
|
+
onPrompt: () => {
|
|
476
|
+
return Promise.reject(new Error("Manual code input is not supported."));
|
|
477
|
+
},
|
|
478
|
+
onProgress: (message) => {
|
|
479
|
+
runtime.appendInfoMessage(message, state);
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
state.oauthCredentials[provider.id] = credentials;
|
|
484
|
+
saveOAuthCredentials(state.oauthCredentials);
|
|
485
|
+
|
|
486
|
+
const apiKey = provider.getApiKey(credentials);
|
|
487
|
+
state.providers.set(provider.id, apiKey);
|
|
488
|
+
|
|
489
|
+
if (!state.model) {
|
|
490
|
+
const models = getAvailableModels(state);
|
|
491
|
+
if (models.length > 0) {
|
|
492
|
+
state.model = models[0]!;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
runtime.appendInfoMessage(`Logged in to ${provider.name}.`, state);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const handleLoginCommand = (state: AppState): void => {
|
|
500
|
+
const oauthProviders = getOAuthProviders();
|
|
501
|
+
if (oauthProviders.length === 0) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const items = oauthProviders.map((provider) => {
|
|
506
|
+
const loggedIn = state.oauthCredentials[provider.id] != null;
|
|
507
|
+
const status = loggedIn ? " (logged in)" : "";
|
|
508
|
+
return {
|
|
509
|
+
label: `${provider.name}${status}`,
|
|
510
|
+
value: provider.id,
|
|
511
|
+
filterText: provider.name,
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
openSelectOverlay(
|
|
516
|
+
state,
|
|
517
|
+
"Login to a provider",
|
|
518
|
+
items,
|
|
519
|
+
"type to filter providers...",
|
|
520
|
+
(providerId) => {
|
|
521
|
+
runtime.dismissOverlay();
|
|
522
|
+
const provider = oauthProviders.find(
|
|
523
|
+
(entry) => entry.id === providerId,
|
|
524
|
+
);
|
|
525
|
+
if (provider) {
|
|
526
|
+
performLogin(provider, state).catch((err) => {
|
|
527
|
+
runtime.appendInfoMessage(
|
|
528
|
+
`Login failed: ${getErrorMessage(err)}`,
|
|
529
|
+
state,
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const handleLogoutCommand = (state: AppState): void => {
|
|
538
|
+
const loggedInProviders = getOAuthProviders().filter(
|
|
539
|
+
(provider) => state.oauthCredentials[provider.id] != null,
|
|
540
|
+
);
|
|
541
|
+
if (loggedInProviders.length === 0) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const items = loggedInProviders.map((provider) => ({
|
|
546
|
+
label: provider.name,
|
|
547
|
+
value: provider.id,
|
|
548
|
+
filterText: provider.name,
|
|
549
|
+
}));
|
|
550
|
+
|
|
551
|
+
openSelectOverlay(
|
|
552
|
+
state,
|
|
553
|
+
"Logout from a provider",
|
|
554
|
+
items,
|
|
555
|
+
"type to filter providers...",
|
|
556
|
+
(providerId) => {
|
|
557
|
+
delete state.oauthCredentials[providerId];
|
|
558
|
+
saveOAuthCredentials(state.oauthCredentials);
|
|
559
|
+
state.providers.delete(providerId);
|
|
560
|
+
|
|
561
|
+
if (state.model && state.model.provider === providerId) {
|
|
562
|
+
state.model = null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const provider = loggedInProviders.find(
|
|
566
|
+
(entry) => entry.id === providerId,
|
|
567
|
+
);
|
|
568
|
+
runtime.dismissOverlay();
|
|
569
|
+
runtime.appendInfoMessage(
|
|
570
|
+
`Logged out of ${provider?.name ?? providerId}.`,
|
|
571
|
+
state,
|
|
572
|
+
);
|
|
573
|
+
},
|
|
574
|
+
);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const handleHelpCommand = (state: AppState): void => {
|
|
578
|
+
runtime.appendInfoMessage(buildHelpText(state), state);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const handleCommand = (command: string, state: AppState): boolean => {
|
|
582
|
+
switch (command) {
|
|
583
|
+
case "model":
|
|
584
|
+
handleModelCommand(state);
|
|
585
|
+
return true;
|
|
586
|
+
case "effort":
|
|
587
|
+
handleEffortCommand(state);
|
|
588
|
+
return true;
|
|
589
|
+
case "session":
|
|
590
|
+
handleSessionCommand(state);
|
|
591
|
+
return true;
|
|
592
|
+
case "login":
|
|
593
|
+
handleLoginCommand(state);
|
|
594
|
+
return true;
|
|
595
|
+
case "logout":
|
|
596
|
+
handleLogoutCommand(state);
|
|
597
|
+
return true;
|
|
598
|
+
case "new":
|
|
599
|
+
handleNewCommand(state).catch((error) => {
|
|
600
|
+
runtime.appendInfoMessage(
|
|
601
|
+
`New session failed: ${getErrorMessage(error)}`,
|
|
602
|
+
state,
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
return true;
|
|
606
|
+
case "fork":
|
|
607
|
+
handleForkCommand(state);
|
|
608
|
+
return true;
|
|
609
|
+
case "undo":
|
|
610
|
+
handleUndoCommand(state).catch((error) => {
|
|
611
|
+
runtime.appendInfoMessage(
|
|
612
|
+
`Undo failed: ${getErrorMessage(error)}`,
|
|
613
|
+
state,
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
return true;
|
|
617
|
+
case "reasoning":
|
|
618
|
+
handleReasoningCommand(state);
|
|
619
|
+
return true;
|
|
620
|
+
case "verbose":
|
|
621
|
+
handleVerboseCommand(state);
|
|
622
|
+
return true;
|
|
623
|
+
case "help":
|
|
624
|
+
handleHelpCommand(state);
|
|
625
|
+
return true;
|
|
626
|
+
default:
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
applyModelSelection,
|
|
633
|
+
applyEffortSelection,
|
|
634
|
+
showCommandAutocomplete,
|
|
635
|
+
showInputHistoryOverlay,
|
|
636
|
+
handleCommand,
|
|
637
|
+
};
|
|
638
|
+
}
|