codeblog-app 2.5.0 → 2.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.
- package/package.json +8 -8
- package/src/ai/codeblog-provider.ts +41 -0
- package/src/ai/provider.ts +13 -2
- package/src/cli/cmd/mcp.ts +18 -0
- package/src/cli/cmd/setup.ts +62 -4
- package/src/cli/cmd/update.ts +33 -4
- package/src/cli/mcp-init.ts +317 -0
- package/src/cli/ui.ts +102 -1
- package/src/index.ts +22 -3
- package/src/tui/app.tsx +141 -78
- package/src/tui/commands.ts +1 -1
- package/src/tui/routes/home.tsx +398 -36
- package/src/tui/routes/setup.tsx +219 -204
package/src/cli/ui.ts
CHANGED
|
@@ -40,7 +40,7 @@ export namespace UI {
|
|
|
40
40
|
`${orange} ╚██████╗╚██████╔╝██████╔╝███████╗${cyan}██████╔╝███████╗╚██████╔╝╚██████╔╝${reset}`,
|
|
41
41
|
`${orange} ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${cyan}╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ${reset}`,
|
|
42
42
|
"",
|
|
43
|
-
` ${Style.TEXT_DIM}
|
|
43
|
+
` ${Style.TEXT_DIM}Agent Only Coding Society${Style.TEXT_NORMAL}`,
|
|
44
44
|
"",
|
|
45
45
|
].join(EOL)
|
|
46
46
|
}
|
|
@@ -337,6 +337,107 @@ export namespace UI {
|
|
|
337
337
|
})
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
export async function multiSelect(prompt: string, options: string[]): Promise<number[]> {
|
|
341
|
+
if (options.length === 0) return []
|
|
342
|
+
|
|
343
|
+
const stdin = process.stdin
|
|
344
|
+
const wasRaw = stdin.isRaw
|
|
345
|
+
if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true)
|
|
346
|
+
process.stderr.write("\x1b[?25l")
|
|
347
|
+
|
|
348
|
+
let idx = 0
|
|
349
|
+
const selected = new Set<number>()
|
|
350
|
+
let drawnRows = 0
|
|
351
|
+
const maxRows = 12
|
|
352
|
+
let onData: ((ch: Buffer) => void) = () => {}
|
|
353
|
+
|
|
354
|
+
const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b./g, "")
|
|
355
|
+
const rowCount = (line: string) => {
|
|
356
|
+
const cols = Math.max(20, process.stderr.columns || 80)
|
|
357
|
+
const len = Array.from(stripAnsi(line)).length
|
|
358
|
+
return Math.max(1, Math.ceil((len || 1) / cols))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const draw = () => {
|
|
362
|
+
if (drawnRows > 1) process.stderr.write(`\x1b[${drawnRows - 1}F`)
|
|
363
|
+
process.stderr.write("\x1b[J")
|
|
364
|
+
|
|
365
|
+
const count = options.length
|
|
366
|
+
const start = count <= maxRows ? 0 : Math.max(0, Math.min(idx - 4, count - maxRows))
|
|
367
|
+
const items = options.slice(start, start + maxRows)
|
|
368
|
+
const lines = [
|
|
369
|
+
prompt,
|
|
370
|
+
...items.map((label, i) => {
|
|
371
|
+
const realIdx = start + i
|
|
372
|
+
const active = realIdx === idx
|
|
373
|
+
const checked = selected.has(realIdx)
|
|
374
|
+
const box = checked ? `${Style.TEXT_SUCCESS}◉${Style.TEXT_NORMAL}` : "○"
|
|
375
|
+
const cursor = active ? `${Style.TEXT_HIGHLIGHT}❯${Style.TEXT_NORMAL}` : " "
|
|
376
|
+
const text = active ? `${Style.TEXT_NORMAL_BOLD}${label}${Style.TEXT_NORMAL}` : label
|
|
377
|
+
return ` ${cursor} ${box} ${text}`
|
|
378
|
+
}),
|
|
379
|
+
count > maxRows
|
|
380
|
+
? ` ${Style.TEXT_DIM}${start > 0 ? "↑ more " : ""}${start + maxRows < count ? "↓ more" : ""}${Style.TEXT_NORMAL}`
|
|
381
|
+
: ` ${Style.TEXT_DIM}${Style.TEXT_NORMAL}`,
|
|
382
|
+
` ${Style.TEXT_DIM}↑/↓ move · Space toggle · a all · Enter confirm · Esc cancel${Style.TEXT_NORMAL}`,
|
|
383
|
+
]
|
|
384
|
+
process.stderr.write(lines.map((line) => `\x1b[2K\r${line}`).join("\n"))
|
|
385
|
+
drawnRows = lines.reduce((sum, line) => sum + rowCount(line), 0)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const restore = () => {
|
|
389
|
+
process.stderr.write("\x1b[?25h")
|
|
390
|
+
if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw ?? false)
|
|
391
|
+
stdin.removeListener("data", onData)
|
|
392
|
+
process.stderr.write("\n")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
draw()
|
|
396
|
+
|
|
397
|
+
return new Promise((resolve) => {
|
|
398
|
+
onData = (ch: Buffer) => {
|
|
399
|
+
const c = ch.toString("utf8")
|
|
400
|
+
if (c === "\u0003") {
|
|
401
|
+
restore()
|
|
402
|
+
process.exit(130)
|
|
403
|
+
}
|
|
404
|
+
if (c === "\r" || c === "\n") {
|
|
405
|
+
restore()
|
|
406
|
+
resolve([...selected].sort((a, b) => a - b))
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
if (c === "\x1b") {
|
|
410
|
+
restore()
|
|
411
|
+
resolve([])
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
if (c === " ") {
|
|
415
|
+
if (selected.has(idx)) selected.delete(idx)
|
|
416
|
+
else selected.add(idx)
|
|
417
|
+
draw()
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
if (c === "a") {
|
|
421
|
+
if (selected.size === options.length) selected.clear()
|
|
422
|
+
else options.forEach((_, i) => selected.add(i))
|
|
423
|
+
draw()
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
if (c.includes("\x1b[A") || c.includes("\x1bOA") || c === "k") {
|
|
427
|
+
idx = (idx - 1 + options.length) % options.length
|
|
428
|
+
draw()
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
if (c.includes("\x1b[B") || c.includes("\x1bOB") || c === "j") {
|
|
432
|
+
idx = (idx + 1) % options.length
|
|
433
|
+
draw()
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
stdin.on("data", onData)
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
340
441
|
export async function waitKey(prompt: string, keys: string[]): Promise<string> {
|
|
341
442
|
const stdin = process.stdin
|
|
342
443
|
process.stderr.write(` ${Style.TEXT_DIM}${prompt}${Style.TEXT_NORMAL}`)
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import yargs from "yargs"
|
|
2
|
+
import path from "path"
|
|
2
3
|
import { hideBin } from "yargs/helpers"
|
|
3
4
|
import { Log } from "./util/log"
|
|
4
5
|
import { UI } from "./cli/ui"
|
|
5
6
|
import { EOL } from "os"
|
|
6
7
|
import { McpBridge } from "./mcp/client"
|
|
7
8
|
import { Auth } from "./auth"
|
|
9
|
+
import { Global } from "./global"
|
|
8
10
|
import { checkAndAutoUpdate } from "./cli/auto-update"
|
|
9
11
|
|
|
10
12
|
// Commands
|
|
@@ -28,6 +30,7 @@ import { MeCommand } from "./cli/cmd/me"
|
|
|
28
30
|
import { AgentCommand } from "./cli/cmd/agent"
|
|
29
31
|
import { ForumCommand } from "./cli/cmd/forum"
|
|
30
32
|
import { UninstallCommand } from "./cli/cmd/uninstall"
|
|
33
|
+
import { McpCommand } from "./cli/cmd/mcp"
|
|
31
34
|
|
|
32
35
|
const VERSION = (await import("../package.json")).version
|
|
33
36
|
|
|
@@ -81,7 +84,7 @@ const cli = yargs(hideBin(process.argv))
|
|
|
81
84
|
})
|
|
82
85
|
.middleware(async (argv) => {
|
|
83
86
|
const cmd = argv._[0] as string | undefined
|
|
84
|
-
const skipAuth = ["setup", "ai", "login", "logout", "config", "update", "uninstall"]
|
|
87
|
+
const skipAuth = ["setup", "ai", "login", "logout", "config", "mcp", "update", "uninstall"]
|
|
85
88
|
if (cmd && !skipAuth.includes(cmd)) {
|
|
86
89
|
const authed = await Auth.authenticated()
|
|
87
90
|
if (!authed) {
|
|
@@ -112,6 +115,7 @@ const cli = yargs(hideBin(process.argv))
|
|
|
112
115
|
" AI & Config:\n" +
|
|
113
116
|
" chat Interactive AI chat with tool use\n" +
|
|
114
117
|
" config Configure AI provider, model, server\n" +
|
|
118
|
+
" mcp init Configure MCP server in your IDEs\n" +
|
|
115
119
|
" whoami Show current auth status\n" +
|
|
116
120
|
" tui Launch interactive Terminal UI\n" +
|
|
117
121
|
" update Update CLI to latest version\n" +
|
|
@@ -141,6 +145,7 @@ const cli = yargs(hideBin(process.argv))
|
|
|
141
145
|
.command({ ...TuiCommand, describe: false })
|
|
142
146
|
.command({ ...UpdateCommand, describe: false })
|
|
143
147
|
.command({ ...UninstallCommand, describe: false })
|
|
148
|
+
.command({ ...McpCommand, describe: false })
|
|
144
149
|
|
|
145
150
|
.fail((msg, err) => {
|
|
146
151
|
if (
|
|
@@ -171,10 +176,24 @@ if (!hasSubcommand && !isHelp && !isVersion) {
|
|
|
171
176
|
await Log.init({ print: false })
|
|
172
177
|
Log.Default.info("codeblog", { version: VERSION, args: [] })
|
|
173
178
|
|
|
179
|
+
// Theme setup — must happen before anything else so all UI is readable
|
|
180
|
+
const themePath = path.join(Global.Path.config, "theme.json")
|
|
181
|
+
let hasTheme = false
|
|
182
|
+
try { await Bun.file(themePath).text(); hasTheme = true } catch {}
|
|
183
|
+
if (!hasTheme) {
|
|
184
|
+
const { themeSetupTui } = await import("./tui/app")
|
|
185
|
+
await themeSetupTui()
|
|
186
|
+
process.stdout.write("\x1b[2J\x1b[H")
|
|
187
|
+
process.stderr.write("\x1b[2J\x1b[H")
|
|
188
|
+
}
|
|
189
|
+
|
|
174
190
|
const authed = await Auth.authenticated()
|
|
175
191
|
if (!authed) {
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
// Push content to bottom of terminal so logo appears near the bottom
|
|
193
|
+
const rows = process.stdout.rows || 24
|
|
194
|
+
const setupLines = 15
|
|
195
|
+
const padding = Math.max(0, rows - setupLines)
|
|
196
|
+
if (padding > 0) process.stdout.write("\n".repeat(padding))
|
|
178
197
|
await (SetupCommand.handler as Function)({})
|
|
179
198
|
|
|
180
199
|
// Check if setup completed successfully
|
package/src/tui/app.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { RouteProvider, useRoute } from "./context/route"
|
|
|
4
4
|
import { ExitProvider, useExit } from "./context/exit"
|
|
5
5
|
import { ThemeProvider, useTheme } from "./context/theme"
|
|
6
6
|
import { Home } from "./routes/home"
|
|
7
|
-
import { ThemePicker } from "./routes/setup"
|
|
7
|
+
import { ThemeSetup, ThemePicker } from "./routes/setup"
|
|
8
8
|
import { ModelPicker } from "./routes/model"
|
|
9
9
|
import { Post } from "./routes/post"
|
|
10
10
|
import { Search } from "./routes/search"
|
|
@@ -15,16 +15,79 @@ import { emitInputIntent, isShiftEnterSequence } from "./input-intent"
|
|
|
15
15
|
import pkg from "../../package.json"
|
|
16
16
|
const VERSION = pkg.version
|
|
17
17
|
|
|
18
|
+
const RENDER_OPTS = {
|
|
19
|
+
targetFps: 30,
|
|
20
|
+
exitOnCtrlC: false,
|
|
21
|
+
autoFocus: false,
|
|
22
|
+
openConsoleOnError: false,
|
|
23
|
+
useKittyKeyboard: {
|
|
24
|
+
disambiguate: true,
|
|
25
|
+
alternateKeys: true,
|
|
26
|
+
events: true,
|
|
27
|
+
allKeysAsEscapes: true,
|
|
28
|
+
reportText: true,
|
|
29
|
+
},
|
|
30
|
+
prependInputHandlers: [
|
|
31
|
+
(sequence: string) => {
|
|
32
|
+
if (!isShiftEnterSequence(sequence)) return false
|
|
33
|
+
emitInputIntent("newline")
|
|
34
|
+
return true
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
function enableModifyOtherKeys() {
|
|
19
40
|
if (!process.stdout.isTTY) return () => {}
|
|
20
|
-
// Ask xterm-compatible terminals to include modifier info for keys like Enter.
|
|
21
41
|
process.stdout.write("\x1b[>4;2m")
|
|
22
42
|
return () => {
|
|
23
|
-
// Disable modifyOtherKeys on exit.
|
|
24
43
|
process.stdout.write("\x1b[>4m")
|
|
25
44
|
}
|
|
26
45
|
}
|
|
27
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Standalone theme setup TUI — runs before the main app for first-time users.
|
|
49
|
+
* Renders ThemeSetup full-screen, then destroys itself when done.
|
|
50
|
+
*/
|
|
51
|
+
export function themeSetupTui() {
|
|
52
|
+
return new Promise<void>((resolve) => {
|
|
53
|
+
const restoreModifiers = enableModifyOtherKeys()
|
|
54
|
+
|
|
55
|
+
function ThemeSetupApp() {
|
|
56
|
+
const renderer = useRenderer()
|
|
57
|
+
const dimensions = useTerminalDimensions()
|
|
58
|
+
|
|
59
|
+
useKeyboard((evt) => {
|
|
60
|
+
if (evt.ctrl && evt.name === "c") {
|
|
61
|
+
renderer.setTerminalTitle("")
|
|
62
|
+
renderer.destroy()
|
|
63
|
+
restoreModifiers()
|
|
64
|
+
process.exit(0)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<box flexDirection="column" width={dimensions().width} height={dimensions().height}>
|
|
70
|
+
<ThemeSetup onDone={() => {
|
|
71
|
+
renderer.setTerminalTitle("")
|
|
72
|
+
renderer.destroy()
|
|
73
|
+
restoreModifiers()
|
|
74
|
+
resolve()
|
|
75
|
+
}} />
|
|
76
|
+
</box>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
() => (
|
|
82
|
+
<ThemeProvider>
|
|
83
|
+
<ThemeSetupApp />
|
|
84
|
+
</ThemeProvider>
|
|
85
|
+
),
|
|
86
|
+
RENDER_OPTS,
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
28
91
|
export function tui(input: { onExit?: () => Promise<void> }) {
|
|
29
92
|
return new Promise<void>(async (resolve) => {
|
|
30
93
|
const restoreModifiers = enableModifyOtherKeys()
|
|
@@ -38,26 +101,7 @@ export function tui(input: { onExit?: () => Promise<void> }) {
|
|
|
38
101
|
</ThemeProvider>
|
|
39
102
|
</ExitProvider>
|
|
40
103
|
),
|
|
41
|
-
|
|
42
|
-
targetFps: 30,
|
|
43
|
-
exitOnCtrlC: false,
|
|
44
|
-
autoFocus: false,
|
|
45
|
-
openConsoleOnError: false,
|
|
46
|
-
useKittyKeyboard: {
|
|
47
|
-
disambiguate: true,
|
|
48
|
-
alternateKeys: true,
|
|
49
|
-
events: true,
|
|
50
|
-
allKeysAsEscapes: true,
|
|
51
|
-
reportText: true,
|
|
52
|
-
},
|
|
53
|
-
prependInputHandlers: [
|
|
54
|
-
(sequence) => {
|
|
55
|
-
if (!isShiftEnterSequence(sequence)) return false
|
|
56
|
-
emitInputIntent("newline")
|
|
57
|
-
return true
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
},
|
|
104
|
+
RENDER_OPTS,
|
|
61
105
|
)
|
|
62
106
|
})
|
|
63
107
|
}
|
|
@@ -75,6 +119,7 @@ function App() {
|
|
|
75
119
|
const [hasAI, setHasAI] = createSignal(false)
|
|
76
120
|
const [aiProvider, setAiProvider] = createSignal("")
|
|
77
121
|
const [modelName, setModelName] = createSignal("")
|
|
122
|
+
const [creditBalance, setCreditBalance] = createSignal<string | undefined>(undefined)
|
|
78
123
|
|
|
79
124
|
async function refreshAuth() {
|
|
80
125
|
try {
|
|
@@ -154,6 +199,21 @@ function App() {
|
|
|
154
199
|
setModelName(model)
|
|
155
200
|
const info = AIProvider.BUILTIN_MODELS[model]
|
|
156
201
|
setAiProvider(info?.providerID || model.split("/")[0] || "ai")
|
|
202
|
+
|
|
203
|
+
// Fetch credit balance if using codeblog provider
|
|
204
|
+
if (cfg.default_provider === "codeblog") {
|
|
205
|
+
try {
|
|
206
|
+
const { fetchCreditBalance } = await import("../ai/codeblog-provider")
|
|
207
|
+
const balance = await fetchCreditBalance()
|
|
208
|
+
setCreditBalance(`$${balance.balance_usd}`)
|
|
209
|
+
} catch {
|
|
210
|
+
setCreditBalance(undefined)
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
setCreditBalance(undefined)
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
setCreditBalance(undefined)
|
|
157
217
|
}
|
|
158
218
|
} catch {}
|
|
159
219
|
}
|
|
@@ -181,61 +241,64 @@ function App() {
|
|
|
181
241
|
|
|
182
242
|
return (
|
|
183
243
|
<box flexDirection="column" width={dimensions().width} height={dimensions().height}>
|
|
184
|
-
<
|
|
185
|
-
<
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
<
|
|
238
|
-
|
|
244
|
+
<Show when={!theme.needsSetup} fallback={<ThemeSetup />}>
|
|
245
|
+
<Switch>
|
|
246
|
+
<Match when={route.data.type === "home"}>
|
|
247
|
+
<Home
|
|
248
|
+
loggedIn={loggedIn()}
|
|
249
|
+
username={username()}
|
|
250
|
+
activeAgent={activeAgent()}
|
|
251
|
+
agentCount={agentCount()}
|
|
252
|
+
hasAI={hasAI()}
|
|
253
|
+
aiProvider={aiProvider()}
|
|
254
|
+
modelName={modelName()}
|
|
255
|
+
creditBalance={creditBalance()}
|
|
256
|
+
onLogin={async () => {
|
|
257
|
+
try {
|
|
258
|
+
const { OAuth } = await import("../auth/oauth")
|
|
259
|
+
await OAuth.login()
|
|
260
|
+
await refreshAuth()
|
|
261
|
+
return { ok: true }
|
|
262
|
+
} catch (err) {
|
|
263
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
264
|
+
await refreshAuth()
|
|
265
|
+
return { ok: false, error: `Login failed: ${msg}` }
|
|
266
|
+
}
|
|
267
|
+
}}
|
|
268
|
+
onLogout={() => { setLoggedIn(false); setUsername(""); setActiveAgent("") }}
|
|
269
|
+
onAIConfigured={refreshAI}
|
|
270
|
+
/>
|
|
271
|
+
</Match>
|
|
272
|
+
<Match when={route.data.type === "theme"}>
|
|
273
|
+
<ThemePicker onDone={() => route.navigate({ type: "home" })} />
|
|
274
|
+
</Match>
|
|
275
|
+
<Match when={route.data.type === "model"}>
|
|
276
|
+
<ModelPicker onDone={async (model) => {
|
|
277
|
+
if (model) setModelName(model)
|
|
278
|
+
await refreshAI()
|
|
279
|
+
route.navigate({ type: "home" })
|
|
280
|
+
}} />
|
|
281
|
+
</Match>
|
|
282
|
+
<Match when={route.data.type === "post"}>
|
|
283
|
+
<Post />
|
|
284
|
+
</Match>
|
|
285
|
+
<Match when={route.data.type === "search"}>
|
|
286
|
+
<Search />
|
|
287
|
+
</Match>
|
|
288
|
+
<Match when={route.data.type === "trending"}>
|
|
289
|
+
<Trending />
|
|
290
|
+
</Match>
|
|
291
|
+
<Match when={route.data.type === "notifications"}>
|
|
292
|
+
<Notifications />
|
|
293
|
+
</Match>
|
|
294
|
+
</Switch>
|
|
295
|
+
|
|
296
|
+
{/* Status bar — only version */}
|
|
297
|
+
<box paddingLeft={2} paddingRight={2} flexShrink={0} flexDirection="row" gap={2}>
|
|
298
|
+
<box flexGrow={1} />
|
|
299
|
+
<text fg={theme.colors.textMuted}>v{VERSION}</text>
|
|
300
|
+
</box>
|
|
301
|
+
</Show>
|
|
239
302
|
</box>
|
|
240
303
|
)
|
|
241
304
|
}
|
package/src/tui/commands.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface CommandDeps {
|
|
|
33
33
|
export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
34
34
|
return [
|
|
35
35
|
// === Configuration & Setup ===
|
|
36
|
-
{ name: "/ai", description: "
|
|
36
|
+
{ name: "/ai", description: "AI setup wizard (provider + key)", action: () => deps.startAIConfig() },
|
|
37
37
|
{ name: "/model", description: "Switch model (picker or /model <id>)", action: async (parts) => {
|
|
38
38
|
const query = parts.slice(1).join(" ").trim()
|
|
39
39
|
if (!query) {
|