codeblog-app 2.5.0 → 2.5.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.
- package/package.json +7 -7
- package/src/cli/cmd/setup.ts +2 -2
- package/src/index.ts +14 -1
- package/src/tui/app.tsx +124 -78
- package/src/tui/routes/home.tsx +1 -1
- package/src/tui/routes/setup.tsx +219 -204
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "codeblog-app",
|
|
4
|
-
"version": "2.5.
|
|
4
|
+
"version": "2.5.1",
|
|
5
5
|
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"typescript": "5.8.2"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"codeblog-app-darwin-arm64": "2.5.
|
|
62
|
-
"codeblog-app-darwin-x64": "2.5.
|
|
63
|
-
"codeblog-app-linux-arm64": "2.5.
|
|
64
|
-
"codeblog-app-linux-x64": "2.5.
|
|
65
|
-
"codeblog-app-windows-x64": "2.5.
|
|
61
|
+
"codeblog-app-darwin-arm64": "2.5.1",
|
|
62
|
+
"codeblog-app-darwin-x64": "2.5.1",
|
|
63
|
+
"codeblog-app-linux-arm64": "2.5.1",
|
|
64
|
+
"codeblog-app-linux-x64": "2.5.1",
|
|
65
|
+
"codeblog-app-windows-x64": "2.5.1"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@ai-sdk/anthropic": "^3.0.44",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@opentui/core": "^0.1.79",
|
|
74
74
|
"@opentui/solid": "^0.1.79",
|
|
75
75
|
"ai": "^6.0.86",
|
|
76
|
-
"codeblog-mcp": "2.5.
|
|
76
|
+
"codeblog-mcp": "2.5.1",
|
|
77
77
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
78
78
|
"fuzzysort": "^3.1.0",
|
|
79
79
|
"hono": "4.10.7",
|
package/src/cli/cmd/setup.ts
CHANGED
|
@@ -831,10 +831,10 @@ export const SetupCommand: CommandModule = {
|
|
|
831
831
|
describe: "First-time setup wizard: authenticate, scan, publish, configure AI",
|
|
832
832
|
handler: async () => {
|
|
833
833
|
// Phase 1: Welcome
|
|
834
|
-
|
|
834
|
+
Bun.stderr.write(UI.logo() + "\n")
|
|
835
835
|
await UI.typeText("Welcome to CodeBlog!", { charDelay: 20 })
|
|
836
836
|
await UI.typeText("The AI-powered coding forum in your terminal.", { charDelay: 15 })
|
|
837
|
-
|
|
837
|
+
Bun.stderr.write("\n")
|
|
838
838
|
|
|
839
839
|
// Phase 2: Authentication
|
|
840
840
|
const alreadyAuthed = await Auth.authenticated()
|
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
|
|
@@ -171,10 +173,21 @@ if (!hasSubcommand && !isHelp && !isVersion) {
|
|
|
171
173
|
await Log.init({ print: false })
|
|
172
174
|
Log.Default.info("codeblog", { version: VERSION, args: [] })
|
|
173
175
|
|
|
176
|
+
// Theme setup — must happen before anything else so all UI is readable
|
|
177
|
+
const themePath = path.join(Global.Path.config, "theme.json")
|
|
178
|
+
let hasTheme = false
|
|
179
|
+
try { await Bun.file(themePath).text(); hasTheme = true } catch {}
|
|
180
|
+
if (!hasTheme) {
|
|
181
|
+
const { themeSetupTui } = await import("./tui/app")
|
|
182
|
+
await themeSetupTui()
|
|
183
|
+
// Clear screen on both stdout and stderr to remove renderer cleanup artifacts
|
|
184
|
+
process.stdout.write("\x1b[2J\x1b[H")
|
|
185
|
+
process.stderr.write("\x1b[2J\x1b[H")
|
|
186
|
+
}
|
|
187
|
+
|
|
174
188
|
const authed = await Auth.authenticated()
|
|
175
189
|
if (!authed) {
|
|
176
190
|
console.log("")
|
|
177
|
-
// Use the statically imported SetupCommand
|
|
178
191
|
await (SetupCommand.handler as Function)({})
|
|
179
192
|
|
|
180
193
|
// 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
|
}
|
|
@@ -181,61 +225,63 @@ function App() {
|
|
|
181
225
|
|
|
182
226
|
return (
|
|
183
227
|
<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
|
-
<box
|
|
237
|
-
|
|
238
|
-
|
|
228
|
+
<Show when={!theme.needsSetup} fallback={<ThemeSetup />}>
|
|
229
|
+
<Switch>
|
|
230
|
+
<Match when={route.data.type === "home"}>
|
|
231
|
+
<Home
|
|
232
|
+
loggedIn={loggedIn()}
|
|
233
|
+
username={username()}
|
|
234
|
+
activeAgent={activeAgent()}
|
|
235
|
+
agentCount={agentCount()}
|
|
236
|
+
hasAI={hasAI()}
|
|
237
|
+
aiProvider={aiProvider()}
|
|
238
|
+
modelName={modelName()}
|
|
239
|
+
onLogin={async () => {
|
|
240
|
+
try {
|
|
241
|
+
const { OAuth } = await import("../auth/oauth")
|
|
242
|
+
await OAuth.login()
|
|
243
|
+
await refreshAuth()
|
|
244
|
+
return { ok: true }
|
|
245
|
+
} catch (err) {
|
|
246
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
247
|
+
await refreshAuth()
|
|
248
|
+
return { ok: false, error: `Login failed: ${msg}` }
|
|
249
|
+
}
|
|
250
|
+
}}
|
|
251
|
+
onLogout={() => { setLoggedIn(false); setUsername(""); setActiveAgent("") }}
|
|
252
|
+
onAIConfigured={refreshAI}
|
|
253
|
+
/>
|
|
254
|
+
</Match>
|
|
255
|
+
<Match when={route.data.type === "theme"}>
|
|
256
|
+
<ThemePicker onDone={() => route.navigate({ type: "home" })} />
|
|
257
|
+
</Match>
|
|
258
|
+
<Match when={route.data.type === "model"}>
|
|
259
|
+
<ModelPicker onDone={async (model) => {
|
|
260
|
+
if (model) setModelName(model)
|
|
261
|
+
await refreshAI()
|
|
262
|
+
route.navigate({ type: "home" })
|
|
263
|
+
}} />
|
|
264
|
+
</Match>
|
|
265
|
+
<Match when={route.data.type === "post"}>
|
|
266
|
+
<Post />
|
|
267
|
+
</Match>
|
|
268
|
+
<Match when={route.data.type === "search"}>
|
|
269
|
+
<Search />
|
|
270
|
+
</Match>
|
|
271
|
+
<Match when={route.data.type === "trending"}>
|
|
272
|
+
<Trending />
|
|
273
|
+
</Match>
|
|
274
|
+
<Match when={route.data.type === "notifications"}>
|
|
275
|
+
<Notifications />
|
|
276
|
+
</Match>
|
|
277
|
+
</Switch>
|
|
278
|
+
|
|
279
|
+
{/* Status bar — only version */}
|
|
280
|
+
<box paddingLeft={2} paddingRight={2} flexShrink={0} flexDirection="row" gap={2}>
|
|
281
|
+
<box flexGrow={1} />
|
|
282
|
+
<text fg={theme.colors.textMuted}>v{VERSION}</text>
|
|
283
|
+
</box>
|
|
284
|
+
</Show>
|
|
239
285
|
</box>
|
|
240
286
|
)
|
|
241
287
|
}
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -157,7 +157,7 @@ export function Home(props: {
|
|
|
157
157
|
return "info"
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
function showMsg(text: string, color =
|
|
160
|
+
function showMsg(text: string, color = theme.colors.textMuted) {
|
|
161
161
|
ensureSession()
|
|
162
162
|
setMessages((p) => [...p, { role: "system", content: text, tone: tone(color) }])
|
|
163
163
|
}
|
package/src/tui/routes/setup.tsx
CHANGED
|
@@ -1,200 +1,223 @@
|
|
|
1
|
-
import { createSignal } from "solid-js"
|
|
2
|
-
import { useKeyboard } from "@opentui/solid"
|
|
3
|
-
import { useTheme, THEME_NAMES, THEMES } from "../context/theme"
|
|
1
|
+
import { createSignal, createMemo } from "solid-js"
|
|
2
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
|
3
|
+
import { useTheme, THEME_NAMES, THEMES, type ThemeColors } from "../context/theme"
|
|
4
4
|
|
|
5
|
-
// High-contrast colors that are visible on ANY terminal background
|
|
6
5
|
const HC = {
|
|
7
6
|
title: "#ff6600",
|
|
8
|
-
text: "#
|
|
9
|
-
|
|
10
|
-
dim: "#999999",
|
|
7
|
+
text: "#aaaaaa",
|
|
8
|
+
dim: "#aaaaaa",
|
|
11
9
|
}
|
|
12
10
|
|
|
11
|
+
const LOGO_ORANGE = "#f48225"
|
|
12
|
+
const LOGO_CYAN = "#00c8ff"
|
|
13
|
+
|
|
14
|
+
const LOGO = [
|
|
15
|
+
" ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ ",
|
|
16
|
+
"██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██║ ██╔═══██╗██╔════╝ ",
|
|
17
|
+
"██║ ██║ ██║██║ ██║█████╗ ██████╔╝██║ ██║ ██║██║ ███╗",
|
|
18
|
+
"██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██║ ██║ ██║██║ ██║",
|
|
19
|
+
"╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝███████╗╚██████╔╝╚██████╔╝",
|
|
20
|
+
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ",
|
|
21
|
+
]
|
|
22
|
+
|
|
13
23
|
function resolveThemeDef(name: string) {
|
|
14
24
|
const fallback = THEMES.codeblog ?? Object.values(THEMES).find(Boolean)
|
|
15
|
-
if (!fallback)
|
|
16
|
-
throw new Error("No themes available")
|
|
17
|
-
}
|
|
25
|
+
if (!fallback) throw new Error("No themes available")
|
|
18
26
|
return THEMES[name] ?? fallback
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
type ThemeOption = { name: string; mode: "dark" | "light"; label: string; colors: ThemeColors }
|
|
30
|
+
|
|
31
|
+
const SETUP_OPTIONS: ThemeOption[] = [
|
|
32
|
+
{ name: "codeblog", mode: "dark", label: "Dark mode", colors: resolveThemeDef("codeblog").dark },
|
|
33
|
+
{ name: "codeblog", mode: "light", label: "Light mode", colors: resolveThemeDef("codeblog").light },
|
|
34
|
+
{ name: "dracula", mode: "dark", label: "Dark — Dracula", colors: resolveThemeDef("dracula").dark },
|
|
35
|
+
{ name: "tokyonight", mode: "dark", label: "Dark — Tokyo Night", colors: resolveThemeDef("tokyonight").dark },
|
|
36
|
+
{ name: "catppuccin", mode: "dark", label: "Dark — Catppuccin", colors: resolveThemeDef("catppuccin").dark },
|
|
37
|
+
{ name: "github", mode: "dark", label: "Dark — GitHub", colors: resolveThemeDef("github").dark },
|
|
38
|
+
{ name: "gruvbox", mode: "dark", label: "Dark — Gruvbox", colors: resolveThemeDef("gruvbox").dark },
|
|
39
|
+
{ name: "github", mode: "light", label: "Light — GitHub", colors: resolveThemeDef("github").light },
|
|
40
|
+
{ name: "catppuccin", mode: "light", label: "Light — Catppuccin", colors: resolveThemeDef("catppuccin").light },
|
|
41
|
+
{ name: "solarized", mode: "light", label: "Light — Solarized", colors: resolveThemeDef("solarized").light },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export function ThemeSetup(props: { onDone?: () => void }) {
|
|
22
45
|
const theme = useTheme()
|
|
23
|
-
const
|
|
24
|
-
const [
|
|
25
|
-
|
|
26
|
-
const [
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
const dimensions = useTerminalDimensions()
|
|
47
|
+
const [idx, setIdx] = createSignal(0)
|
|
48
|
+
|
|
49
|
+
const current = createMemo(() => SETUP_OPTIONS[idx()] ?? SETUP_OPTIONS[0]!)
|
|
50
|
+
|
|
51
|
+
function apply(i: number) {
|
|
52
|
+
const opt = SETUP_OPTIONS[i]
|
|
53
|
+
if (!opt) return
|
|
54
|
+
theme.set(opt.name)
|
|
55
|
+
theme.setMode(opt.mode)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
apply(0)
|
|
29
59
|
|
|
30
60
|
useKeyboard((evt) => {
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (evt.name === "down" || evt.name === "j") {
|
|
38
|
-
setModeIdx((i) => (i + 1) % modes.length)
|
|
39
|
-
evt.preventDefault()
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
if (evt.name === "return") {
|
|
43
|
-
theme.setMode(modes[modeIdx()] ?? "dark")
|
|
44
|
-
setStep("theme")
|
|
45
|
-
evt.preventDefault()
|
|
46
|
-
return
|
|
47
|
-
}
|
|
61
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
62
|
+
const next = (idx() - 1 + SETUP_OPTIONS.length) % SETUP_OPTIONS.length
|
|
63
|
+
setIdx(next)
|
|
64
|
+
apply(next)
|
|
65
|
+
evt.preventDefault()
|
|
66
|
+
return
|
|
48
67
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
theme.set(getThemeName(next))
|
|
62
|
-
evt.preventDefault()
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
if (evt.name === "return") {
|
|
66
|
-
theme.finishSetup()
|
|
67
|
-
evt.preventDefault()
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
if (evt.name === "escape") {
|
|
71
|
-
setStep("mode")
|
|
72
|
-
evt.preventDefault()
|
|
73
|
-
return
|
|
74
|
-
}
|
|
68
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
69
|
+
const next = (idx() + 1) % SETUP_OPTIONS.length
|
|
70
|
+
setIdx(next)
|
|
71
|
+
apply(next)
|
|
72
|
+
evt.preventDefault()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (evt.name === "return") {
|
|
76
|
+
theme.finishSetup()
|
|
77
|
+
props.onDone?.()
|
|
78
|
+
evt.preventDefault()
|
|
79
|
+
return
|
|
75
80
|
}
|
|
76
81
|
})
|
|
77
82
|
|
|
83
|
+
const wide = createMemo(() => (dimensions().width ?? 80) >= 90)
|
|
84
|
+
const c = createMemo(() => current().colors)
|
|
85
|
+
|
|
78
86
|
return (
|
|
79
|
-
<box flexDirection="column" flexGrow={1}
|
|
87
|
+
<box flexDirection="column" flexGrow={1} paddingLeft={2} paddingRight={2}>
|
|
80
88
|
<box flexGrow={1} minHeight={0} />
|
|
81
89
|
|
|
90
|
+
{/* Logo */}
|
|
91
|
+
<box flexShrink={0} flexDirection="column" alignItems="center">
|
|
92
|
+
{LOGO.map((line, i) => (
|
|
93
|
+
<text fg={i < 3 ? LOGO_ORANGE : LOGO_CYAN}>{line}</text>
|
|
94
|
+
))}
|
|
95
|
+
<box height={1} />
|
|
96
|
+
<text fg={HC.text}>{"The AI-powered coding forum in your terminal"}</text>
|
|
97
|
+
<box height={1} />
|
|
98
|
+
</box>
|
|
99
|
+
|
|
100
|
+
{/* Main content */}
|
|
82
101
|
<box flexShrink={0} flexDirection="column" alignItems="center">
|
|
83
102
|
<text fg={HC.title}>
|
|
84
|
-
<span style={{ bold: true }}>{"
|
|
103
|
+
<span style={{ bold: true }}>{"Choose the text style that looks best with your terminal:"}</span>
|
|
85
104
|
</text>
|
|
105
|
+
<text fg={HC.dim}>{"To change this later, run /theme"}</text>
|
|
86
106
|
<box height={1} />
|
|
87
|
-
</box>
|
|
88
107
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<box flexDirection="row" paddingLeft={2}>
|
|
97
|
-
<text fg={modeIdx() === i ? HC.selected : HC.dim}>
|
|
98
|
-
{modeIdx() === i ? "❯ " : " "}
|
|
99
|
-
</text>
|
|
100
|
-
<text fg={modeIdx() === i ? HC.selected : HC.dim}>
|
|
101
|
-
<span style={{ bold: modeIdx() === i }}>
|
|
102
|
-
{m === "dark" ? "Dark background (black/dark terminal)" : "Light background (white/light terminal)"}
|
|
103
|
-
</span>
|
|
104
|
-
</text>
|
|
105
|
-
</box>
|
|
106
|
-
))}
|
|
107
|
-
<box height={1} />
|
|
108
|
-
<text fg={HC.text}>{"↑↓ select · Enter confirm"}</text>
|
|
109
|
-
</box>
|
|
110
|
-
) : (
|
|
111
|
-
<box flexShrink={0} flexDirection="column" width="100%" maxWidth={60}>
|
|
112
|
-
<text fg={theme.colors.text}>
|
|
113
|
-
<span style={{ bold: true }}>{"Choose a color theme:"}</span>
|
|
114
|
-
</text>
|
|
115
|
-
<box height={1} />
|
|
116
|
-
{THEME_NAMES.map((name, i) => {
|
|
117
|
-
const c = getThemeColors(name)
|
|
118
|
-
return (
|
|
119
|
-
<box flexDirection="row" paddingLeft={2}>
|
|
120
|
-
<text fg={themeIdx() === i ? c.primary : theme.colors.textMuted}>
|
|
121
|
-
{themeIdx() === i ? "❯ " : " "}
|
|
108
|
+
<box flexDirection="row" justifyContent="center" gap={wide() ? 6 : 3}>
|
|
109
|
+
{/* Options list */}
|
|
110
|
+
<box flexDirection="column" width={wide() ? 28 : 26}>
|
|
111
|
+
{SETUP_OPTIONS.map((opt, i) => (
|
|
112
|
+
<box flexDirection="row">
|
|
113
|
+
<text fg={idx() === i ? opt.colors.primary : HC.dim}>
|
|
114
|
+
{idx() === i ? "❯ " : " "}
|
|
122
115
|
</text>
|
|
123
|
-
<text fg={
|
|
124
|
-
<span style={{ bold:
|
|
125
|
-
{
|
|
116
|
+
<text fg={idx() === i ? opt.colors.text : HC.dim}>
|
|
117
|
+
<span style={{ bold: idx() === i }}>
|
|
118
|
+
{`${(i + 1).toString().padStart(2)}. ${opt.label}`}
|
|
126
119
|
</span>
|
|
127
120
|
</text>
|
|
128
|
-
<text fg={
|
|
129
|
-
<text fg={c.logo2}>{"●"}</text>
|
|
130
|
-
<text fg={c.primary}>{"●"}</text>
|
|
131
|
-
<text fg={c.accent}>{"●"}</text>
|
|
132
|
-
<text fg={c.success}>{"●"}</text>
|
|
133
|
-
<text fg={c.error}>{"●"}</text>
|
|
121
|
+
{idx() === i && <text fg={opt.colors.success}>{"✓"}</text>}
|
|
134
122
|
</box>
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
))}
|
|
124
|
+
</box>
|
|
125
|
+
|
|
126
|
+
{/* Live preview */}
|
|
127
|
+
<box flexDirection="column" width={wide() ? 44 : 38}>
|
|
128
|
+
<text fg={c().text}><span style={{ bold: true }}>{"Preview"}</span></text>
|
|
129
|
+
<box height={1} />
|
|
130
|
+
<box flexDirection="column" paddingLeft={2}>
|
|
131
|
+
<text fg={c().textMuted}>{"// A coding conversation"}</text>
|
|
132
|
+
<box height={1} />
|
|
133
|
+
<box flexDirection="row">
|
|
134
|
+
<text fg={c().primary}><span style={{ bold: true }}>{"You: "}</span></text>
|
|
135
|
+
<text fg={c().text}>{"Refactor the auth module"}</text>
|
|
136
|
+
</box>
|
|
137
|
+
<box flexDirection="row">
|
|
138
|
+
<text fg={c().accent}><span style={{ bold: true }}>{"AI: "}</span></text>
|
|
139
|
+
<text fg={c().text}>{"I'll update 3 files..."}</text>
|
|
140
|
+
</box>
|
|
141
|
+
<box height={1} />
|
|
142
|
+
<text fg={c().textMuted}>{" src/auth.ts"}</text>
|
|
143
|
+
<box flexDirection="row">
|
|
144
|
+
<text fg={c().error}>{" - "}</text>
|
|
145
|
+
<text fg={c().error}>{"const token = getOld()"}</text>
|
|
146
|
+
</box>
|
|
147
|
+
<box flexDirection="row">
|
|
148
|
+
<text fg={c().success}>{" + "}</text>
|
|
149
|
+
<text fg={c().success}>{"const token = getNew()"}</text>
|
|
150
|
+
</box>
|
|
151
|
+
<box height={1} />
|
|
152
|
+
<box flexDirection="row">
|
|
153
|
+
<text fg={c().success}>{"✓ "}</text>
|
|
154
|
+
<text fg={c().text}>{"Changes applied"}</text>
|
|
155
|
+
</box>
|
|
156
|
+
<box flexDirection="row">
|
|
157
|
+
<text fg={c().warning}>{"⚠ "}</text>
|
|
158
|
+
<text fg={c().textMuted}>{"3 tests need updating"}</text>
|
|
159
|
+
</box>
|
|
160
|
+
</box>
|
|
161
|
+
</box>
|
|
139
162
|
</box>
|
|
140
|
-
|
|
163
|
+
|
|
164
|
+
<box height={1} />
|
|
165
|
+
<text fg={HC.text}>{"↑↓ select · Enter confirm"}</text>
|
|
166
|
+
</box>
|
|
141
167
|
|
|
142
168
|
<box flexGrow={1} minHeight={0} />
|
|
143
169
|
</box>
|
|
144
170
|
)
|
|
145
171
|
}
|
|
146
172
|
|
|
173
|
+
// Full theme picker (all themes × dark/light) for /theme command in main TUI
|
|
174
|
+
function buildAllOptions(): ThemeOption[] {
|
|
175
|
+
const out: ThemeOption[] = []
|
|
176
|
+
for (const name of THEME_NAMES) {
|
|
177
|
+
const def = resolveThemeDef(name)
|
|
178
|
+
out.push({ name, mode: "dark", label: `${name} — dark`, colors: def.dark })
|
|
179
|
+
out.push({ name, mode: "light", label: `${name} — light`, colors: def.light })
|
|
180
|
+
}
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ALL_OPTIONS = buildAllOptions()
|
|
185
|
+
|
|
147
186
|
export function ThemePicker(props: { onDone: () => void }) {
|
|
148
187
|
const theme = useTheme()
|
|
149
|
-
const [idx, setIdx] = createSignal(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
188
|
+
const [idx, setIdx] = createSignal(
|
|
189
|
+
Math.max(0, ALL_OPTIONS.findIndex((o) => o.name === theme.name && o.mode === theme.mode))
|
|
190
|
+
)
|
|
191
|
+
const current = createMemo(() => ALL_OPTIONS[idx()] ?? ALL_OPTIONS[0]!)
|
|
192
|
+
|
|
193
|
+
function apply(i: number) {
|
|
194
|
+
const opt = ALL_OPTIONS[i]
|
|
195
|
+
if (!opt) return
|
|
196
|
+
theme.set(opt.name)
|
|
197
|
+
theme.setMode(opt.mode)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const c = createMemo(() => current().colors)
|
|
153
201
|
|
|
154
202
|
useKeyboard((evt) => {
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
if (evt.name === "down" || evt.name === "j") {
|
|
164
|
-
const next = (idx() + 1) % THEME_NAMES.length
|
|
165
|
-
setIdx(next)
|
|
166
|
-
theme.set(getThemeName(next))
|
|
167
|
-
evt.preventDefault()
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
if (evt.name === "tab") {
|
|
171
|
-
setTab("mode")
|
|
172
|
-
evt.preventDefault()
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
if (evt.name === "return" || evt.name === "escape") {
|
|
176
|
-
props.onDone()
|
|
177
|
-
evt.preventDefault()
|
|
178
|
-
return
|
|
179
|
-
}
|
|
203
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
204
|
+
const next = (idx() - 1 + ALL_OPTIONS.length) % ALL_OPTIONS.length
|
|
205
|
+
setIdx(next)
|
|
206
|
+
apply(next)
|
|
207
|
+
evt.preventDefault()
|
|
208
|
+
return
|
|
180
209
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
if (evt.name === "return" || evt.name === "escape") {
|
|
194
|
-
props.onDone()
|
|
195
|
-
evt.preventDefault()
|
|
196
|
-
return
|
|
197
|
-
}
|
|
210
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
211
|
+
const next = (idx() + 1) % ALL_OPTIONS.length
|
|
212
|
+
setIdx(next)
|
|
213
|
+
apply(next)
|
|
214
|
+
evt.preventDefault()
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if (evt.name === "return" || evt.name === "escape") {
|
|
218
|
+
props.onDone()
|
|
219
|
+
evt.preventDefault()
|
|
220
|
+
return
|
|
198
221
|
}
|
|
199
222
|
})
|
|
200
223
|
|
|
@@ -205,60 +228,52 @@ export function ThemePicker(props: { onDone: () => void }) {
|
|
|
205
228
|
<span style={{ bold: true }}>Theme Settings</span>
|
|
206
229
|
</text>
|
|
207
230
|
<box flexGrow={1} />
|
|
208
|
-
<text fg={theme.colors.textMuted}>{"
|
|
231
|
+
<text fg={theme.colors.textMuted}>{"Enter/Esc: done"}</text>
|
|
209
232
|
</box>
|
|
210
233
|
|
|
211
234
|
<box flexDirection="row" flexGrow={1} paddingTop={1} gap={4}>
|
|
212
|
-
{/*
|
|
213
|
-
<box flexDirection="column" width={
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<span style={{ bold: idx() === i }}>
|
|
227
|
-
{name.padEnd(14)}
|
|
228
|
-
</span>
|
|
229
|
-
</text>
|
|
230
|
-
<text fg={c.logo1}>{" ●"}</text>
|
|
231
|
-
<text fg={c.logo2}>{"●"}</text>
|
|
232
|
-
<text fg={c.primary}>{"●"}</text>
|
|
233
|
-
<text fg={c.accent}>{"●"}</text>
|
|
234
|
-
<text fg={c.success}>{"●"}</text>
|
|
235
|
-
<text fg={c.error}>{"●"}</text>
|
|
236
|
-
</box>
|
|
237
|
-
)
|
|
238
|
-
})}
|
|
235
|
+
{/* Options list */}
|
|
236
|
+
<box flexDirection="column" width={30}>
|
|
237
|
+
{ALL_OPTIONS.map((opt, i) => (
|
|
238
|
+
<box flexDirection="row">
|
|
239
|
+
<text fg={idx() === i ? opt.colors.primary : theme.colors.textMuted}>
|
|
240
|
+
{idx() === i ? "❯ " : " "}
|
|
241
|
+
</text>
|
|
242
|
+
<text fg={idx() === i ? opt.colors.text : theme.colors.textMuted}>
|
|
243
|
+
<span style={{ bold: idx() === i }}>
|
|
244
|
+
{opt.label}
|
|
245
|
+
</span>
|
|
246
|
+
</text>
|
|
247
|
+
</box>
|
|
248
|
+
))}
|
|
239
249
|
</box>
|
|
240
250
|
|
|
241
|
-
{/*
|
|
242
|
-
<box flexDirection="column" width={
|
|
243
|
-
<text fg={
|
|
244
|
-
<span style={{ bold: true }}>{"Background Mode"}</span>
|
|
245
|
-
</text>
|
|
251
|
+
{/* Preview */}
|
|
252
|
+
<box flexDirection="column" width={40}>
|
|
253
|
+
<text fg={c().text}><span style={{ bold: true }}>{"Preview"}</span></text>
|
|
246
254
|
<box height={1} />
|
|
247
|
-
<box flexDirection="
|
|
248
|
-
<
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
255
|
+
<box flexDirection="column" paddingLeft={2}>
|
|
256
|
+
<box flexDirection="row">
|
|
257
|
+
<text fg={c().primary}><span style={{ bold: true }}>{"You: "}</span></text>
|
|
258
|
+
<text fg={c().text}>{"Show me trending posts"}</text>
|
|
259
|
+
</box>
|
|
260
|
+
<box flexDirection="row">
|
|
261
|
+
<text fg={c().accent}><span style={{ bold: true }}>{"AI: "}</span></text>
|
|
262
|
+
<text fg={c().text}>{"Here are today's top..."}</text>
|
|
263
|
+
</box>
|
|
264
|
+
<box height={1} />
|
|
265
|
+
<box flexDirection="row">
|
|
266
|
+
<text fg={c().success}>{"✓ "}</text>
|
|
267
|
+
<text fg={c().text}>{"Published successfully"}</text>
|
|
268
|
+
</box>
|
|
269
|
+
<box flexDirection="row">
|
|
270
|
+
<text fg={c().warning}>{"⚠ "}</text>
|
|
271
|
+
<text fg={c().textMuted}>{"Rate limit reached"}</text>
|
|
272
|
+
</box>
|
|
273
|
+
<box flexDirection="row">
|
|
274
|
+
<text fg={c().error}>{"✗ "}</text>
|
|
275
|
+
<text fg={c().textMuted}>{"Connection failed"}</text>
|
|
276
|
+
</box>
|
|
262
277
|
</box>
|
|
263
278
|
</box>
|
|
264
279
|
</box>
|