codeblog-app 1.2.0 → 1.4.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/bin/codeblog CHANGED
@@ -1,2 +1,65 @@
1
- #!/usr/bin/env bun
2
- import "../src/index.ts"
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require("child_process")
4
+ const fs = require("fs")
5
+ const path = require("path")
6
+ const os = require("os")
7
+
8
+ function run(target) {
9
+ const result = childProcess.spawnSync(target, process.argv.slice(2), {
10
+ stdio: "inherit",
11
+ })
12
+ if (result.error) {
13
+ console.error(result.error.message)
14
+ process.exit(1)
15
+ }
16
+ process.exit(typeof result.status === "number" ? result.status : 0)
17
+ }
18
+
19
+ const scriptDir = path.dirname(fs.realpathSync(__filename))
20
+
21
+ const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" }
22
+ const archMap = { x64: "x64", arm64: "arm64" }
23
+ const platform = platformMap[os.platform()] || os.platform()
24
+ const arch = archMap[os.arch()] || os.arch()
25
+ const base = "codeblog-app-" + platform + "-" + arch
26
+ const binary = platform === "windows" ? "codeblog.exe" : "codeblog"
27
+
28
+ function findBinary(startDir) {
29
+ let current = startDir
30
+ for (;;) {
31
+ const modules = path.join(current, "node_modules")
32
+ if (fs.existsSync(modules)) {
33
+ const candidate = path.join(modules, base, "bin", binary)
34
+ if (fs.existsSync(candidate)) return candidate
35
+ }
36
+ const parent = path.dirname(current)
37
+ if (parent === current) return
38
+ current = parent
39
+ }
40
+ }
41
+
42
+ const resolved = findBinary(scriptDir)
43
+ if (resolved) {
44
+ run(resolved)
45
+ } else {
46
+ // Fallback: run with bun from source
47
+ const bun = process.env.BUN_INSTALL
48
+ ? path.join(process.env.BUN_INSTALL, "bin", "bun")
49
+ : path.join(os.homedir(), ".bun", "bin", "bun")
50
+
51
+ if (fs.existsSync(bun)) {
52
+ const src = path.join(scriptDir, "..", "src", "index.ts")
53
+ const result = childProcess.spawnSync(bun, ["run", src, ...process.argv.slice(2)], {
54
+ stdio: "inherit",
55
+ })
56
+ process.exit(typeof result.status === "number" ? result.status : 0)
57
+ }
58
+
59
+ console.error(
60
+ "Could not find codeblog binary for your platform (" + base + ").\n" +
61
+ "Try: npm install -g codeblog-app@latest\n" +
62
+ "Or install bun: curl -fsSL https://bun.sh/install | bash"
63
+ )
64
+ process.exit(1)
65
+ }
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": "1.2.0",
4
+ "version": "1.4.0",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -55,6 +55,13 @@
55
55
  "drizzle-kit": "1.0.0-beta.12-a5629fb",
56
56
  "typescript": "5.8.2"
57
57
  },
58
+ "optionalDependencies": {
59
+ "codeblog-app-darwin-arm64": "1.4.0",
60
+ "codeblog-app-darwin-x64": "1.4.0",
61
+ "codeblog-app-linux-arm64": "1.4.0",
62
+ "codeblog-app-linux-x64": "1.4.0",
63
+ "codeblog-app-windows-x64": "1.4.0"
64
+ },
58
65
  "dependencies": {
59
66
  "@ai-sdk/amazon-bedrock": "^4.0.60",
60
67
  "@ai-sdk/anthropic": "^3.0.44",
package/src/index.ts CHANGED
@@ -35,7 +35,7 @@ import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
35
35
  import { TagsCommand } from "./cli/cmd/tags"
36
36
  import { ExploreCommand } from "./cli/cmd/explore"
37
37
 
38
- const VERSION = "1.2.0"
38
+ const VERSION = "1.4.0"
39
39
 
40
40
  process.on("unhandledRejection", (e) => {
41
41
  Log.Default.error("rejection", {
@@ -129,6 +129,20 @@ const cli = yargs(hideBin(process.argv))
129
129
  })
130
130
  .strict()
131
131
 
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
+
132
146
  try {
133
147
  await cli.parse()
134
148
  } catch (e) {
package/src/tui/app.tsx CHANGED
@@ -1,13 +1,11 @@
1
1
  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"
3
3
  import { RouteProvider, useRoute } from "./context/route"
4
4
  import { ExitProvider, useExit } from "./context/exit"
5
5
  import { Home } from "./routes/home"
6
6
  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"
11
9
 
12
10
  export function tui(input: { onExit?: () => Promise<void> }) {
13
11
  return new Promise<void>(async (resolve) => {
@@ -35,12 +33,36 @@ function App() {
35
33
  const dimensions = useTerminalDimensions()
36
34
  const renderer = useRenderer()
37
35
  const [loggedIn, setLoggedIn] = createSignal(false)
36
+ const [username, setUsername] = createSignal("")
37
+ const [hasAI, setHasAI] = createSignal(false)
38
+ const [aiProvider, setAiProvider] = createSignal("")
38
39
 
39
40
  onMount(async () => {
40
41
  renderer.setTerminalTitle("CodeBlog")
42
+
43
+ // Check auth status
41
44
  try {
42
45
  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
+ }
44
66
  } catch {}
45
67
  })
46
68
 
@@ -51,36 +73,16 @@ function App() {
51
73
  return
52
74
  }
53
75
 
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
+ }
82
83
  }
83
84
 
85
+ // Back navigation
84
86
  if (evt.name === "escape" && route.data.type !== "home") {
85
87
  route.navigate({ type: "home" })
86
88
  evt.preventDefault()
@@ -89,41 +91,53 @@ function App() {
89
91
  })
90
92
 
91
93
  return (
92
- <box flexDirection="column" width="100%" height="100%">
94
+ <box flexDirection="column" width={dimensions().width} height={dimensions().height}>
93
95
  <Switch>
94
96
  <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
+ />
96
113
  </Match>
97
114
  <Match when={route.data.type === "chat"}>
98
115
  <Chat />
99
116
  </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>
112
117
  </Switch>
113
118
 
114
119
  {/* Status bar */}
115
120
  <box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} flexShrink={0} flexDirection="row">
116
121
  <text fg="#6a737c">
117
122
  {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"}
120
125
  </text>
121
126
  <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>
122
136
  <text fg={loggedIn() ? "#48a868" : "#d73a49"}>
123
137
  {loggedIn() ? "● " : "○ "}
124
138
  </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>
127
141
  </box>
128
142
  </box>
129
143
  )
@@ -1,4 +1,4 @@
1
- import { createSignal, For, Show } from "solid-js"
1
+ import { createSignal, For, Show, onMount } from "solid-js"
2
2
  import { useKeyboard } from "@opentui/solid"
3
3
  import { useRoute } from "../context/route"
4
4
 
@@ -13,18 +13,34 @@ export function Chat() {
13
13
  const [streaming, setStreaming] = createSignal(false)
14
14
  const [streamText, setStreamText] = createSignal("")
15
15
  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("")
23
17
  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
+ })
25
41
 
26
42
  async function send(text: string) {
27
- if (!text.trim()) return
43
+ if (!text.trim() || streaming()) return
28
44
  const userMsg: Message = { role: "user", content: text.trim() }
29
45
  const prev = messages()
30
46
  setMessages([...prev, userMsg])
@@ -53,6 +69,7 @@ export function Chat() {
53
69
  },
54
70
  onError: (err) => {
55
71
  setMessages((p) => [...p, { role: "assistant", content: `Error: ${err.message}` }])
72
+ setStreamText("")
56
73
  setStreaming(false)
57
74
  },
58
75
  },
@@ -61,6 +78,7 @@ export function Chat() {
61
78
  } catch (err) {
62
79
  const msg = err instanceof Error ? err.message : String(err)
63
80
  setMessages((p) => [...p, { role: "assistant", content: `Error: ${msg}` }])
81
+ setStreamText("")
64
82
  setStreaming(false)
65
83
  }
66
84
  }
@@ -78,10 +96,14 @@ export function Chat() {
78
96
  if (name === "/model") {
79
97
  const id = parts[1]
80
98
  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>` }])
82
100
  return
83
101
  }
84
102
  setModel(id)
103
+ import("../../ai/provider").then(({ AIProvider }) => {
104
+ const info = AIProvider.BUILTIN_MODELS[id]
105
+ setModelName(info?.name || id)
106
+ }).catch(() => setModelName(id))
85
107
  setMessages((p) => [...p, { role: "assistant", content: `Switched to model: ${id}` }])
86
108
  return
87
109
  }
@@ -102,14 +124,13 @@ export function Chat() {
102
124
  return
103
125
  }
104
126
 
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` }])
106
128
  }
107
129
 
108
130
  useKeyboard((evt) => {
109
- if (!inputMode()) return
110
-
111
131
  if (evt.name === "return" && !evt.shift) {
112
132
  const text = inputBuf().trim()
133
+ if (!text) return
113
134
  setInputBuf("")
114
135
  if (text.startsWith("/")) {
115
136
  handleCommand(text)
@@ -143,12 +164,12 @@ export function Chat() {
143
164
  <box flexDirection="column" flexGrow={1}>
144
165
  {/* Header */}
145
166
  <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
146
- <text fg="#d946ef">
167
+ <text fg="#0074cc">
147
168
  <span style={{ bold: true }}>AI Chat</span>
148
169
  </text>
149
- <text fg="#6a737c">{model()}</text>
170
+ <text fg="#6a737c">{modelName()}</text>
150
171
  <box flexGrow={1} />
151
- <text fg="#6a737c">esc:back /help</text>
172
+ <text fg="#6a737c">esc:back · /help · /model · /clear</text>
152
173
  </box>
153
174
 
154
175
  {/* Messages */}
@@ -1,125 +1,302 @@
1
- import { createSignal, onMount, For, Show } from "solid-js"
1
+ import { createSignal, Show } from "solid-js"
2
2
  import { useKeyboard } from "@opentui/solid"
3
- import { useRoute, type Route } from "../context/route"
4
-
5
- interface FeedPost {
6
- id: string
7
- title: string
8
- upvotes: number
9
- downvotes: number
10
- comment_count: number
11
- views: number
12
- tags: string[]
13
- agent: string
14
- created_at: string
15
- }
3
+ import { useRoute } from "../context/route"
4
+ import { useExit } from "../context/exit"
5
+
6
+ const LOGO = [
7
+ " ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ ",
8
+ " ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██║ ██╔═══██╗██╔════╝ ",
9
+ " ██║ ██║ ██║██║ ██║█████╗ ██████╔╝██║ ██║ ██║██║ ███╗",
10
+ " ██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██║ ██║ ██║██║ ██║",
11
+ " ╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝███████╗╚██████╔╝╚██████╔╝",
12
+ " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ",
13
+ ]
14
+
15
+ const HELP_TEXT = [
16
+ "Commands:",
17
+ " /login Log in with GitHub or Google",
18
+ " /config Show current configuration",
19
+ " /config ai Configure AI provider (interactive)",
20
+ " /scan Scan IDE sessions",
21
+ " /publish Publish scanned sessions",
22
+ " /ai-publish AI writes a post from your session",
23
+ " /feed Browse recent posts",
24
+ " /search <query> Search posts",
25
+ " /trending View trending topics",
26
+ " /notifications View notifications",
27
+ " /dashboard Your stats",
28
+ " /models List available AI models",
29
+ " /help Show this help",
30
+ " /exit Exit",
31
+ "",
32
+ "Or just type anything to chat with AI.",
33
+ ]
16
34
 
17
- export function Home() {
35
+ export function Home(props: {
36
+ loggedIn: boolean
37
+ username: string
38
+ hasAI: boolean
39
+ aiProvider: string
40
+ onLogin: () => Promise<void>
41
+ }) {
18
42
  const route = useRoute()
19
- const [posts, setPosts] = createSignal<FeedPost[]>([])
20
- const [loading, setLoading] = createSignal(true)
21
- const [selected, setSelected] = createSignal(0)
22
-
23
- const [error, setError] = createSignal("")
24
-
25
- onMount(async () => {
26
- try {
27
- const { Feed } = await import("../../api/feed")
28
- const result = await Feed.list()
29
- setPosts(result.posts as unknown as FeedPost[])
30
- } catch (err) {
31
- const msg = err instanceof Error ? err.message : String(err)
32
- setError(msg.includes("401") ? "Not logged in. Run: codeblog login" : msg)
33
- setPosts([])
43
+ const exit = useExit()
44
+ const [input, setInput] = createSignal("")
45
+ const [message, setMessage] = createSignal("")
46
+ const [messageColor, setMessageColor] = createSignal("#6a737c")
47
+ const [showHelp, setShowHelp] = createSignal(false)
48
+
49
+ function showMsg(text: string, color = "#6a737c") {
50
+ setMessage(text)
51
+ setMessageColor(color)
52
+ }
53
+
54
+ async function handleSubmit() {
55
+ const text = input().trim()
56
+ setInput("")
57
+ if (!text) return
58
+
59
+ // Handle commands
60
+ if (text.startsWith("/")) {
61
+ const parts = text.split(/\s+/)
62
+ const cmd = parts[0]
63
+
64
+ if (cmd === "/help") {
65
+ setShowHelp(!showHelp())
66
+ return
67
+ }
68
+
69
+ if (cmd === "/exit" || cmd === "/quit" || cmd === "/q") {
70
+ exit()
71
+ return
72
+ }
73
+
74
+ if (cmd === "/login") {
75
+ showMsg("Opening browser for login...", "#0074cc")
76
+ await props.onLogin()
77
+ showMsg("Logged in!", "#48a868")
78
+ return
79
+ }
80
+
81
+ if (cmd === "/config") {
82
+ if (parts[1] === "ai") {
83
+ showMsg("Use CLI: codeblog config --provider anthropic --api-key sk-...", "#f48225")
84
+ return
85
+ }
86
+ try {
87
+ const { Config } = await import("../../config")
88
+ const cfg = await Config.load()
89
+ const providers = cfg.providers || {}
90
+ const keys = Object.keys(providers)
91
+ const model = cfg.model || "claude-sonnet-4-20250514"
92
+ showMsg(`Model: ${model} | Providers: ${keys.length > 0 ? keys.join(", ") : "none"} | URL: ${cfg.api_url || "https://codeblog.ai"}`, "#e7e9eb")
93
+ } catch {
94
+ showMsg("Failed to load config", "#d73a49")
95
+ }
96
+ return
97
+ }
98
+
99
+ if (cmd === "/scan") {
100
+ showMsg("Scanning IDE sessions...", "#0074cc")
101
+ try {
102
+ const { registerAllScanners, scanAll } = await import("../../scanner")
103
+ registerAllScanners()
104
+ const sessions = scanAll(10)
105
+ if (sessions.length === 0) {
106
+ showMsg("No IDE sessions found.", "#f48225")
107
+ } else {
108
+ const summary = sessions.slice(0, 3).map((s) => `[${s.source}] ${s.project}`).join(" | ")
109
+ showMsg(`Found ${sessions.length} sessions: ${summary}`, "#48a868")
110
+ }
111
+ } catch (err) {
112
+ showMsg(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
113
+ }
114
+ return
115
+ }
116
+
117
+ if (cmd === "/publish") {
118
+ showMsg("Publishing sessions...", "#0074cc")
119
+ try {
120
+ const { Publisher } = await import("../../publisher")
121
+ const results = await Publisher.scanAndPublish({ limit: 1 })
122
+ const ok = results.filter((r) => r.postId)
123
+ if (ok.length > 0) {
124
+ showMsg(`Published ${ok.length} post(s)!`, "#48a868")
125
+ } else {
126
+ showMsg("No sessions to publish.", "#f48225")
127
+ }
128
+ } catch (err) {
129
+ showMsg(`Publish failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
130
+ }
131
+ return
132
+ }
133
+
134
+ if (cmd === "/ai-publish") {
135
+ if (!props.hasAI) {
136
+ showMsg("No AI configured. Use: /config ai", "#d73a49")
137
+ return
138
+ }
139
+ showMsg("AI is writing a post from your session...", "#0074cc")
140
+ // Delegate to CLI command for now
141
+ showMsg("Use CLI: codeblog ai-publish", "#f48225")
142
+ return
143
+ }
144
+
145
+ if (cmd === "/feed") {
146
+ showMsg("Loading feed...", "#0074cc")
147
+ try {
148
+ const { Feed } = await import("../../api/feed")
149
+ const result = await Feed.list()
150
+ const posts = (result as any).posts || []
151
+ if (posts.length === 0) {
152
+ showMsg("No posts yet.", "#f48225")
153
+ } else {
154
+ const summary = posts.slice(0, 3).map((p: any) => p.title?.slice(0, 40)).join(" | ")
155
+ showMsg(`${posts.length} posts: ${summary}`, "#e7e9eb")
156
+ }
157
+ } catch (err) {
158
+ showMsg(`Feed failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
159
+ }
160
+ return
161
+ }
162
+
163
+ if (cmd === "/models") {
164
+ try {
165
+ const { AIProvider } = await import("../../ai/provider")
166
+ const models = await AIProvider.available()
167
+ const configured = models.filter((m) => m.hasKey)
168
+ const names = configured.map((m) => m.model.name).join(", ")
169
+ showMsg(configured.length > 0 ? `Available: ${names}` : "No models configured. Use: codeblog config --provider anthropic --api-key sk-...", configured.length > 0 ? "#48a868" : "#f48225")
170
+ } catch (err) {
171
+ showMsg(`Failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
172
+ }
173
+ return
174
+ }
175
+
176
+ if (cmd === "/search") {
177
+ const query = parts.slice(1).join(" ")
178
+ if (!query) {
179
+ showMsg("Usage: /search <query>", "#f48225")
180
+ return
181
+ }
182
+ try {
183
+ const { Posts } = await import("../../api/posts")
184
+ const result = await Posts.search(query)
185
+ const posts = (result as any).posts || []
186
+ showMsg(posts.length > 0 ? `${posts.length} results for "${query}"` : `No results for "${query}"`, posts.length > 0 ? "#48a868" : "#f48225")
187
+ } catch (err) {
188
+ showMsg(`Search failed: ${err instanceof Error ? err.message : String(err)}`, "#d73a49")
189
+ }
190
+ return
191
+ }
192
+
193
+ if (cmd === "/trending" || cmd === "/notifications" || cmd === "/dashboard") {
194
+ showMsg(`Use CLI: codeblog ${cmd.slice(1)}`, "#f48225")
195
+ return
196
+ }
197
+
198
+ showMsg(`Unknown command: ${cmd}. Type /help`, "#d73a49")
199
+ return
34
200
  }
35
- setLoading(false)
36
- })
201
+
202
+ // Regular text → start AI chat
203
+ if (!props.hasAI) {
204
+ showMsg("No AI provider configured. Run: /config ai", "#d73a49")
205
+ return
206
+ }
207
+
208
+ route.navigate({ type: "chat", sessionMessages: [{ role: "user", content: text }] })
209
+ }
37
210
 
38
211
  useKeyboard((evt) => {
39
- const p = posts()
40
- if (evt.name === "up" || evt.name === "k") {
41
- setSelected((s) => Math.max(0, s - 1))
212
+ if (evt.name === "return" && !evt.shift) {
213
+ handleSubmit()
42
214
  evt.preventDefault()
215
+ return
43
216
  }
44
- if (evt.name === "down" || evt.name === "j") {
45
- setSelected((s) => Math.min(p.length - 1, s + 1))
217
+
218
+ if (evt.name === "backspace") {
219
+ setInput((s) => s.slice(0, -1))
46
220
  evt.preventDefault()
221
+ return
47
222
  }
48
- if (evt.name === "return" && p.length > 0) {
49
- const post = p[selected()]
50
- if (post) route.navigate({ type: "post", postId: post.id })
223
+
224
+ if (evt.sequence && evt.sequence.length === 1 && !evt.ctrl && !evt.meta) {
225
+ setInput((s) => s + evt.sequence)
51
226
  evt.preventDefault()
227
+ return
228
+ }
229
+
230
+ if (evt.name === "space") {
231
+ setInput((s) => s + " ")
232
+ evt.preventDefault()
233
+ return
52
234
  }
53
235
  })
54
236
 
55
237
  return (
56
- <box flexDirection="column" flexGrow={1}>
57
- {/* Header */}
58
- <box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
59
- <text fg="#0074cc">
60
- <span style={{ bold: true }}>CodeBlog</span>
61
- </text>
62
- <text fg="#6a737c"> AI Forum</text>
238
+ <box flexDirection="column" flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
239
+ {/* Top spacer */}
240
+ <box flexGrow={1} minHeight={0} />
241
+
242
+ {/* Logo */}
243
+ <box flexShrink={0} flexDirection="column">
244
+ {LOGO.map((line, i) => (
245
+ <text fg={i < 4 ? "#f48225" : "#0074cc"}>{line}</text>
246
+ ))}
63
247
  </box>
64
248
 
65
- {/* Section title */}
66
- <box paddingLeft={2} paddingTop={1} flexShrink={0}>
67
- <text fg="#f48225">
68
- <span style={{ bold: true }}>Recent Posts</span>
69
- </text>
70
- <text fg="#6a737c">{` (${posts().length})`}</text>
249
+ <box height={1} flexShrink={0}>
250
+ <text fg="#6a737c">The AI-powered coding forum</text>
71
251
  </box>
72
252
 
73
- <Show when={loading()}>
74
- <box paddingLeft={4} paddingTop={1}>
75
- <text fg="#6a737c">Loading feed...</text>
253
+ {/* Status indicators */}
254
+ <box height={2} flexShrink={0} flexDirection="column" paddingTop={1}>
255
+ <box flexDirection="row" gap={2}>
256
+ <Show when={!props.loggedIn}>
257
+ <text fg="#d73a49">○ Not logged in — type /login</text>
258
+ </Show>
259
+ <Show when={props.loggedIn}>
260
+ <text fg="#48a868">● {props.username || "Logged in"}</text>
261
+ </Show>
262
+ <Show when={!props.hasAI}>
263
+ <text fg="#d73a49">○ No AI — type /config ai</text>
264
+ </Show>
265
+ <Show when={props.hasAI}>
266
+ <text fg="#48a868">● {props.aiProvider}</text>
267
+ </Show>
76
268
  </box>
77
- </Show>
269
+ </box>
78
270
 
79
- <Show when={error()}>
80
- <box paddingLeft={4} paddingTop={1}>
81
- <text fg="#d73a49">{error()}</text>
271
+ {/* Input prompt */}
272
+ <box width="100%" maxWidth={75} flexShrink={0} paddingTop={1}>
273
+ <box flexDirection="row" width="100%">
274
+ <text fg="#0074cc">
275
+ <span style={{ bold: true }}>{"❯ "}</span>
276
+ </text>
277
+ <text fg="#e7e9eb">{input()}</text>
278
+ <text fg="#6a737c">{"█"}</text>
279
+ </box>
280
+ </box>
281
+
282
+ {/* Message area */}
283
+ <Show when={message()}>
284
+ <box width="100%" maxWidth={75} paddingTop={1} flexShrink={0}>
285
+ <text fg={messageColor()}>{message()}</text>
82
286
  </box>
83
287
  </Show>
84
288
 
85
- <Show when={!loading() && !error() && posts().length === 0}>
86
- <box paddingLeft={4} paddingTop={1}>
87
- <text fg="#6a737c">No posts yet. Press c to start an AI chat.</text>
289
+ {/* Help text */}
290
+ <Show when={showHelp()}>
291
+ <box width="100%" maxWidth={75} paddingTop={1} flexShrink={0} flexDirection="column">
292
+ {HELP_TEXT.map((line) => (
293
+ <text fg={line.startsWith(" /") ? "#0074cc" : "#6a737c"}>{line}</text>
294
+ ))}
88
295
  </box>
89
296
  </Show>
90
297
 
91
- {/* Post list */}
92
- <box flexDirection="column" paddingTop={1} flexGrow={1}>
93
- <For each={posts()}>
94
- {(post, i) => {
95
- const score = post.upvotes - post.downvotes
96
- const isSelected = () => i() === selected()
97
- return (
98
- <box flexDirection="row" paddingLeft={2} paddingRight={2}>
99
- {/* Score */}
100
- <box width={6} justifyContent="flex-end" marginRight={1}>
101
- <text fg={score > 0 ? "#48a868" : score < 0 ? "#d73a49" : "#6a737c"}>
102
- {score > 0 ? `+${score}` : `${score}`}
103
- </text>
104
- </box>
105
- {/* Content */}
106
- <box flexDirection="column" flexGrow={1}>
107
- <text fg={isSelected() ? "#0074cc" : "#e7e9eb"}>
108
- <span style={{ bold: isSelected() }}>{isSelected() ? "▸ " : " "}{post.title}</span>
109
- </text>
110
- <box flexDirection="row" gap={1}>
111
- <text fg="#6a737c">{`💬${post.comment_count} 👁${post.views}`}</text>
112
- <For each={(post.tags || []).slice(0, 3)}>
113
- {(tag) => <text fg="#39739d">{`#${tag}`}</text>}
114
- </For>
115
- <text fg="#838c95">{`by ${post.agent || "anon"}`}</text>
116
- </box>
117
- </box>
118
- </box>
119
- )
120
- }}
121
- </For>
122
- </box>
298
+ {/* Bottom spacer */}
299
+ <box flexGrow={1} minHeight={0} />
123
300
  </box>
124
301
  )
125
302
  }