Skip to content

Commit ca44a5e

Browse files
committed
feat: TUI rewrite — codeblog launches interactive UI by default
- codeblog (no args) now launches full TUI instead of showing help - Home screen: centered Logo, input prompt, login/AI status indicators - Home commands: /login, /config, /scan, /publish, /feed, /search, /models, /help - Chat screen: streaming AI responses, /model, /clear, /help - Auto-navigate to chat when user types text on home screen - Status bar: AI provider + login status + version - First-time user guidance: shows login and AI config hints
1 parent bb26628 commit ca44a5e

5 files changed

Lines changed: 392 additions & 166 deletions

File tree

‎packages/codeblog/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "codeblog-app",
4-
"version": "1.2.0",
4+
"version": "1.3.0",
55
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
66
"type": "module",
77
"license": "MIT",

‎packages/codeblog/src/index.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
3535
import { TagsCommand } from "./cli/cmd/tags"
3636
import { ExploreCommand } from "./cli/cmd/explore"
3737

38-
const VERSION = "1.2.0"
38+
const VERSION = "1.3.0"
3939

4040
process.on("unhandledRejection", (e) => {
4141
Log.Default.error("rejection", {
@@ -129,6 +129,20 @@ const cli = yargs(hideBin(process.argv))
129129
})
130130
.strict()
131131

132+
// If no subcommand given, launch TUI
133+
const args = hideBin(process.argv)
134+
const hasSubcommand = args.length > 0 && !args[0].startsWith("-")
135+
const isHelp = args.includes("--help") || args.includes("-h")
136+
const isVersion = args.includes("--version") || args.includes("-v")
137+
138+
if (!hasSubcommand && !isHelp && !isVersion) {
139+
await Log.init({ print: false })
140+
Log.Default.info("codeblog", { version: VERSION, args: [] })
141+
const { tui } = await import("./tui/app")
142+
await tui({ onExit: async () => {} })
143+
process.exit(0)
144+
}
145+
132146
try {
133147
await cli.parse()
134148
} catch (e) {

‎packages/codeblog/src/tui/app.tsx‎

Lines changed: 66 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
2-
import { Switch, Match, onMount, createSignal } from "solid-js"
2+
import { Switch, Match, onMount, createSignal, Show } from "solid-js"
33
import { RouteProvider, useRoute } from "./context/route"
44
import { ExitProvider, useExit } from "./context/exit"
55
import { Home } from "./routes/home"
66
import { Chat } from "./routes/chat"
7-
import { Trending } from "./routes/trending"
8-
import { Search } from "./routes/search"
9-
import { Post } from "./routes/post"
10-
import { Notifications } from "./routes/notifications"
7+
8+
const VERSION = "1.3.0"
119

1210
export function tui(input: { onExit?: () => Promise<void> }) {
1311
return new Promise<void>(async (resolve) => {
@@ -35,12 +33,36 @@ function App() {
3533
const dimensions = useTerminalDimensions()
3634
const renderer = useRenderer()
3735
const [loggedIn, setLoggedIn] = createSignal(false)
36+
const [username, setUsername] = createSignal("")
37+
const [hasAI, setHasAI] = createSignal(false)
38+
const [aiProvider, setAiProvider] = createSignal("")
3839

3940
onMount(async () => {
4041
renderer.setTerminalTitle("CodeBlog")
42+
43+
// Check auth status
4144
try {
4245
const { Auth } = await import("../auth")
43-
setLoggedIn(await Auth.authenticated())
46+
const authenticated = await Auth.authenticated()
47+
setLoggedIn(authenticated)
48+
if (authenticated) {
49+
const token = await Auth.load()
50+
if (token?.username) setUsername(token.username)
51+
}
52+
} catch {}
53+
54+
// Check AI provider status
55+
try {
56+
const { AIProvider } = await import("../ai/provider")
57+
const has = await AIProvider.hasAnyKey()
58+
setHasAI(has)
59+
if (has) {
60+
const { Config } = await import("../config")
61+
const cfg = await Config.load()
62+
const model = cfg.model || AIProvider.DEFAULT_MODEL
63+
const info = AIProvider.BUILTIN_MODELS[model]
64+
setAiProvider(info?.name || model)
65+
}
4466
} catch {}
4567
})
4668

@@ -51,36 +73,16 @@ function App() {
5173
return
5274
}
5375

54-
if (evt.name === "q" && !evt.ctrl && route.data.type === "home") {
55-
exit()
56-
evt.preventDefault()
57-
return
58-
}
59-
60-
if (evt.name === "c" && route.data.type === "home") {
61-
route.navigate({ type: "chat" })
62-
evt.preventDefault()
63-
return
64-
}
65-
66-
if (evt.name === "t" && route.data.type === "home") {
67-
route.navigate({ type: "trending" })
68-
evt.preventDefault()
69-
return
70-
}
71-
72-
if (evt.name === "s" && route.data.type === "home") {
73-
route.navigate({ type: "search", query: "" })
74-
evt.preventDefault()
75-
return
76-
}
77-
78-
if (evt.name === "n" && route.data.type === "home") {
79-
route.navigate({ type: "notifications" })
80-
evt.preventDefault()
81-
return
76+
// Home screen shortcuts
77+
if (route.data.type === "home") {
78+
if (evt.name === "q" && !evt.ctrl) {
79+
exit()
80+
evt.preventDefault()
81+
return
82+
}
8283
}
8384

85+
// Back navigation
8486
if (evt.name === "escape" && route.data.type !== "home") {
8587
route.navigate({ type: "home" })
8688
evt.preventDefault()
@@ -89,41 +91,53 @@ function App() {
8991
})
9092

9193
return (
92-
<box flexDirection="column" width="100%" height="100%">
94+
<box flexDirection="column" width={dimensions().width} height={dimensions().height}>
9395
<Switch>
9496
<Match when={route.data.type === "home"}>
95-
<Home />
97+
<Home
98+
loggedIn={loggedIn()}
99+
username={username()}
100+
hasAI={hasAI()}
101+
aiProvider={aiProvider()}
102+
onLogin={async () => {
103+
try {
104+
const { OAuth } = await import("../auth/oauth")
105+
await OAuth.login("github")
106+
const { Auth } = await import("../auth")
107+
setLoggedIn(true)
108+
const token = await Auth.load()
109+
if (token?.username) setUsername(token.username)
110+
} catch {}
111+
}}
112+
/>
96113
</Match>
97114
<Match when={route.data.type === "chat"}>
98115
<Chat />
99116
</Match>
100-
<Match when={route.data.type === "trending"}>
101-
<Trending />
102-
</Match>
103-
<Match when={route.data.type === "notifications"}>
104-
<Notifications />
105-
</Match>
106-
<Match when={route.data.type === "search"}>
107-
<Search />
108-
</Match>
109-
<Match when={route.data.type === "post"}>
110-
<Post />
111-
</Match>
112117
</Switch>
113118

114119
{/* Status bar */}
115120
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} flexShrink={0} flexDirection="row">
116121
<text fg="#6a737c">
117122
{route.data.type === "home"
118-
? "c:chat s:search t:trending n:notifs q:quit"
119-
: "esc:back ctrl+c:exit"}
123+
? "type to chat · /help · q:quit"
124+
: "esc:back · ctrl+c:exit"}
120125
</text>
121126
<box flexGrow={1} />
127+
<Show when={hasAI()}>
128+
<text fg="#48a868">{"● "}</text>
129+
<text fg="#6a737c">{aiProvider()}</text>
130+
<text fg="#6a737c">{" "}</text>
131+
</Show>
132+
<Show when={!hasAI()}>
133+
<text fg="#d73a49">{"○ "}</text>
134+
<text fg="#6a737c">{"no AI "}</text>
135+
</Show>
122136
<text fg={loggedIn() ? "#48a868" : "#d73a49"}>
123137
{loggedIn() ? "● " : "○ "}
124138
</text>
125-
<text fg="#6a737c">{loggedIn() ? "logged in" : "not logged in"}</text>
126-
<text fg="#6a737c">{" codeblog v0.4.3"}</text>
139+
<text fg="#6a737c">{loggedIn() ? username() || "logged in" : "not logged in"}</text>
140+
<text fg="#6a737c">{` v${VERSION}`}</text>
127141
</box>
128142
</box>
129143
)

‎packages/codeblog/src/tui/routes/chat.tsx‎

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSignal, For, Show } from "solid-js"
1+
import { createSignal, For, Show, onMount } from "solid-js"
22
import { useKeyboard } from "@opentui/solid"
33
import { useRoute } from "../context/route"
44

@@ -13,18 +13,34 @@ export function Chat() {
1313
const [streaming, setStreaming] = createSignal(false)
1414
const [streamText, setStreamText] = createSignal("")
1515
const [model, setModel] = createSignal("")
16-
17-
// Load configured model on mount
18-
import("../../config").then(({ Config }) =>
19-
Config.load().then((cfg) => {
20-
if (cfg.model) setModel(cfg.model)
21-
}).catch(() => {}),
22-
)
16+
const [modelName, setModelName] = createSignal("")
2317
const [inputBuf, setInputBuf] = createSignal("")
24-
const [inputMode, setInputMode] = createSignal(true)
18+
19+
onMount(async () => {
20+
try {
21+
const { Config } = await import("../../config")
22+
const { AIProvider } = await import("../../ai/provider")
23+
const cfg = await Config.load()
24+
const id = cfg.model || AIProvider.DEFAULT_MODEL
25+
setModel(id)
26+
const info = AIProvider.BUILTIN_MODELS[id]
27+
setModelName(info?.name || id)
28+
} catch {}
29+
30+
// Auto-send initial message from home screen
31+
const data = route.data as any
32+
if (data.sessionMessages?.length > 0) {
33+
for (const msg of data.sessionMessages) {
34+
if (msg.role === "user") {
35+
send(msg.content)
36+
break
37+
}
38+
}
39+
}
40+
})
2541

2642
async function send(text: string) {
27-
if (!text.trim()) return
43+
if (!text.trim() || streaming()) return
2844
const userMsg: Message = { role: "user", content: text.trim() }
2945
const prev = messages()
3046
setMessages([...prev, userMsg])
@@ -53,6 +69,7 @@ export function Chat() {
5369
},
5470
onError: (err) => {
5571
setMessages((p) => [...p, { role: "assistant", content: `Error: ${err.message}` }])
72+
setStreamText("")
5673
setStreaming(false)
5774
},
5875
},
@@ -61,6 +78,7 @@ export function Chat() {
6178
} catch (err) {
6279
const msg = err instanceof Error ? err.message : String(err)
6380
setMessages((p) => [...p, { role: "assistant", content: `Error: ${msg}` }])
81+
setStreamText("")
6482
setStreaming(false)
6583
}
6684
}
@@ -78,10 +96,14 @@ export function Chat() {
7896
if (name === "/model") {
7997
const id = parts[1]
8098
if (!id) {
81-
setMessages((p) => [...p, { role: "assistant", content: `Current model: ${model()}\nUsage: /model <model-id>` }])
99+
setMessages((p) => [...p, { role: "assistant", content: `Current model: ${modelName()} (${model()})\nUsage: /model <model-id>` }])
82100
return
83101
}
84102
setModel(id)
103+
import("../../ai/provider").then(({ AIProvider }) => {
104+
const info = AIProvider.BUILTIN_MODELS[id]
105+
setModelName(info?.name || id)
106+
}).catch(() => setModelName(id))
85107
setMessages((p) => [...p, { role: "assistant", content: `Switched to model: ${id}` }])
86108
return
87109
}
@@ -102,14 +124,13 @@ export function Chat() {
102124
return
103125
}
104126

105-
setMessages((p) => [...p, { role: "assistant", content: `Unknown command: ${name}. Type /help for available commands.` }])
127+
setMessages((p) => [...p, { role: "assistant", content: `Unknown command: ${name}. Type /help` }])
106128
}
107129

108130
useKeyboard((evt) => {
109-
if (!inputMode()) return
110-
111131
if (evt.name === "return" && !evt.shift) {
112132
const text = inputBuf().trim()
133+
if (!text) return
113134
setInputBuf("")
114135
if (text.startsWith("/")) {
115136
handleCommand(text)
@@ -143,12 +164,12 @@ export function Chat() {
143164
<box flexDirection="column" flexGrow={1}>
144165
{/* Header */}
145166
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
146-
<text fg="#d946ef">
167+
<text fg="#0074cc">
147168
<span style={{ bold: true }}>AI Chat</span>
148169
</text>
149-
<text fg="#6a737c">{model()}</text>
170+
<text fg="#6a737c">{modelName()}</text>
150171
<box flexGrow={1} />
151-
<text fg="#6a737c">esc:back /help</text>
172+
<text fg="#6a737c">esc:back · /help · /model · /clear</text>
152173
</box>
153174

154175
{/* Messages */}

0 commit comments

Comments
 (0)