codeblog-app 1.5.2 → 1.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/src/tui/app.tsx CHANGED
@@ -4,8 +4,8 @@ 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 { Chat } from "./routes/chat"
8
- import { ThemeSetup, ThemePicker } from "./routes/setup"
7
+ import { ThemePicker } from "./routes/setup"
8
+ import { ModelPicker } from "./routes/model"
9
9
 
10
10
  import pkg from "../../package.json"
11
11
  const VERSION = pkg.version
@@ -42,6 +42,23 @@ function App() {
42
42
  const [username, setUsername] = createSignal("")
43
43
  const [hasAI, setHasAI] = createSignal(false)
44
44
  const [aiProvider, setAiProvider] = createSignal("")
45
+ const [modelName, setModelName] = createSignal("")
46
+
47
+ async function refreshAI() {
48
+ try {
49
+ const { AIProvider } = await import("../ai/provider")
50
+ const has = await AIProvider.hasAnyKey()
51
+ setHasAI(has)
52
+ if (has) {
53
+ const { Config } = await import("../config")
54
+ const cfg = await Config.load()
55
+ const model = cfg.model || AIProvider.DEFAULT_MODEL
56
+ setModelName(model)
57
+ const info = AIProvider.BUILTIN_MODELS[model]
58
+ setAiProvider(info?.providerID || model.split("/")[0] || "ai")
59
+ }
60
+ } catch {}
61
+ }
45
62
 
46
63
  onMount(async () => {
47
64
  renderer.setTerminalTitle("CodeBlog")
@@ -52,24 +69,12 @@ function App() {
52
69
  const authenticated = await Auth.authenticated()
53
70
  setLoggedIn(authenticated)
54
71
  if (authenticated) {
55
- const token = await Auth.load()
72
+ const token = await Auth.get()
56
73
  if (token?.username) setUsername(token.username)
57
74
  }
58
75
  } catch {}
59
76
 
60
- // Check AI provider status
61
- try {
62
- const { AIProvider } = await import("../ai/provider")
63
- const has = await AIProvider.hasAnyKey()
64
- setHasAI(has)
65
- if (has) {
66
- const { Config } = await import("../config")
67
- const cfg = await Config.load()
68
- const model = cfg.model || AIProvider.DEFAULT_MODEL
69
- const info = AIProvider.BUILTIN_MODELS[model]
70
- setAiProvider(info?.name || model)
71
- }
72
- } catch {}
77
+ await refreshAI()
73
78
  })
74
79
 
75
80
  useKeyboard((evt) => {
@@ -79,16 +84,7 @@ function App() {
79
84
  return
80
85
  }
81
86
 
82
- // Home screen shortcuts
83
- if (route.data.type === "home") {
84
- if (evt.name === "q" && !evt.ctrl) {
85
- exit()
86
- evt.preventDefault()
87
- return
88
- }
89
- }
90
-
91
- // Back navigation
87
+ // Back navigation from sub-pages
92
88
  if (evt.name === "escape" && route.data.type !== "home") {
93
89
  route.navigate({ type: "home" })
94
90
  evt.preventDefault()
@@ -99,57 +95,68 @@ function App() {
99
95
  return (
100
96
  <box flexDirection="column" width={dimensions().width} height={dimensions().height}>
101
97
  <Switch>
102
- <Match when={theme.needsSetup}>
103
- <ThemeSetup />
104
- </Match>
105
98
  <Match when={route.data.type === "home"}>
106
99
  <Home
107
100
  loggedIn={loggedIn()}
108
101
  username={username()}
109
102
  hasAI={hasAI()}
110
103
  aiProvider={aiProvider()}
104
+ modelName={modelName()}
111
105
  onLogin={async () => {
112
106
  try {
113
107
  const { OAuth } = await import("../auth/oauth")
114
108
  await OAuth.login("github")
115
109
  const { Auth } = await import("../auth")
116
110
  setLoggedIn(true)
117
- const token = await Auth.load()
111
+ const token = await Auth.get()
118
112
  if (token?.username) setUsername(token.username)
119
113
  } catch {}
120
114
  }}
115
+ onLogout={() => { setLoggedIn(false); setUsername("") }}
116
+ onAIConfigured={refreshAI}
121
117
  />
122
118
  </Match>
123
- <Match when={route.data.type === "chat"}>
124
- <Chat />
125
- </Match>
126
119
  <Match when={route.data.type === "theme"}>
127
120
  <ThemePicker onDone={() => route.navigate({ type: "home" })} />
128
121
  </Match>
122
+ <Match when={route.data.type === "model"}>
123
+ <ModelPicker onDone={async (model) => {
124
+ if (model) setModelName(model)
125
+ await refreshAI()
126
+ route.navigate({ type: "home" })
127
+ }} />
128
+ </Match>
129
129
  </Switch>
130
130
 
131
- {/* Status bar */}
132
- <box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} flexShrink={0} flexDirection="row">
133
- <text fg={theme.colors.textMuted}>
134
- {route.data.type === "home"
135
- ? "type to chat · /help · /theme · q:quit"
136
- : "esc:back · ctrl+c:exit"}
137
- </text>
131
+ {/* Status bar — like OpenCode */}
132
+ <box paddingLeft={2} paddingRight={2} flexShrink={0} flexDirection="row" gap={2}>
133
+ <text fg={theme.colors.textMuted}>{process.cwd()}</text>
138
134
  <box flexGrow={1} />
139
135
  <Show when={hasAI()}>
140
- <text fg={theme.colors.success}>{"● "}</text>
141
- <text fg={theme.colors.textMuted}>{aiProvider()}</text>
142
- <text fg={theme.colors.textMuted}>{" "}</text>
136
+ <text fg={theme.colors.text}>
137
+ <span style={{ fg: theme.colors.success }}>● </span>
138
+ {modelName()}
139
+ </text>
143
140
  </Show>
144
141
  <Show when={!hasAI()}>
145
- <text fg={theme.colors.error}>{"○ "}</text>
146
- <text fg={theme.colors.textMuted}>{"no AI "}</text>
142
+ <text fg={theme.colors.text}>
143
+ <span style={{ fg: theme.colors.error }}>○ </span>
144
+ no AI <span style={{ fg: theme.colors.textMuted }}>/ai</span>
145
+ </text>
146
+ </Show>
147
+ <Show when={loggedIn()}>
148
+ <text fg={theme.colors.text}>
149
+ <span style={{ fg: theme.colors.success }}>● </span>
150
+ {username() || "logged in"}
151
+ </text>
152
+ </Show>
153
+ <Show when={!loggedIn()}>
154
+ <text fg={theme.colors.text}>
155
+ <span style={{ fg: theme.colors.error }}>○ </span>
156
+ <span style={{ fg: theme.colors.textMuted }}>/login</span>
157
+ </text>
147
158
  </Show>
148
- <text fg={loggedIn() ? theme.colors.success : theme.colors.error}>
149
- {loggedIn() ? "● " : "○ "}
150
- </text>
151
- <text fg={theme.colors.textMuted}>{loggedIn() ? username() || "logged in" : "not logged in"}</text>
152
- <text fg={theme.colors.textMuted}>{` v${VERSION}`}</text>
159
+ <text fg={theme.colors.textMuted}>v{VERSION}</text>
153
160
  </box>
154
161
  </box>
155
162
  )
@@ -0,0 +1,126 @@
1
+ // Slash command definitions for the TUI home screen
2
+
3
+ export interface CmdDef {
4
+ name: string
5
+ description: string
6
+ action: (parts: string[]) => void | Promise<void>
7
+ }
8
+
9
+ export interface CommandDeps {
10
+ showMsg: (text: string, color?: string) => void
11
+ navigate: (route: any) => void
12
+ exit: () => void
13
+ onLogin: () => Promise<void>
14
+ onLogout: () => void
15
+ clearChat: () => void
16
+ startAIConfig: () => void
17
+ setMode: (mode: "dark" | "light") => void
18
+ colors: {
19
+ primary: string
20
+ success: string
21
+ warning: string
22
+ error: string
23
+ text: string
24
+ }
25
+ }
26
+
27
+ export function createCommands(deps: CommandDeps): CmdDef[] {
28
+ return [
29
+ { name: "/ai", description: "Configure AI provider (paste URL + key)", action: () => deps.startAIConfig() },
30
+ { name: "/model", description: "Choose AI model", action: () => deps.navigate({ type: "model" }) },
31
+ { name: "/clear", description: "Clear conversation", action: () => deps.clearChat() },
32
+ { name: "/new", description: "New conversation", action: () => deps.clearChat() },
33
+ { name: "/login", description: "Sign in to CodeBlog", action: async () => {
34
+ deps.showMsg("Opening browser for login...", deps.colors.primary)
35
+ await deps.onLogin()
36
+ deps.showMsg("Logged in!", deps.colors.success)
37
+ }},
38
+ { name: "/logout", description: "Sign out of CodeBlog", action: async () => {
39
+ try {
40
+ const { Auth } = await import("../auth")
41
+ await Auth.remove()
42
+ deps.showMsg("Logged out.", deps.colors.text)
43
+ deps.onLogout()
44
+ } catch (err) { deps.showMsg(`Logout failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
45
+ }},
46
+ { name: "/theme", description: "Change color theme", action: () => deps.navigate({ type: "theme" }) },
47
+ { name: "/dark", description: "Switch to dark mode", action: () => { deps.setMode("dark"); deps.showMsg("Dark mode", deps.colors.text) } },
48
+ { name: "/light", description: "Switch to light mode", action: () => { deps.setMode("light"); deps.showMsg("Light mode", deps.colors.text) } },
49
+ { name: "/scan", description: "Scan IDE coding sessions", action: async () => {
50
+ deps.showMsg("Scanning IDE sessions...", deps.colors.primary)
51
+ try {
52
+ const { registerAllScanners, scanAll } = await import("../scanner")
53
+ registerAllScanners()
54
+ const sessions = scanAll(10)
55
+ if (sessions.length === 0) deps.showMsg("No IDE sessions found.", deps.colors.warning)
56
+ else deps.showMsg(`Found ${sessions.length} sessions: ${sessions.slice(0, 3).map((s) => `[${s.source}] ${s.project}`).join(" | ")}`, deps.colors.success)
57
+ } catch (err) { deps.showMsg(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
58
+ }},
59
+ { name: "/publish", description: "Publish sessions as blog posts", action: async () => {
60
+ deps.showMsg("Publishing sessions...", deps.colors.primary)
61
+ try {
62
+ const { Publisher } = await import("../publisher")
63
+ const results = await Publisher.scanAndPublish({ limit: 1 })
64
+ const ok = results.filter((r) => r.postId)
65
+ deps.showMsg(ok.length > 0 ? `Published ${ok.length} post(s)!` : "No sessions to publish.", ok.length > 0 ? deps.colors.success : deps.colors.warning)
66
+ } catch (err) { deps.showMsg(`Publish failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
67
+ }},
68
+ { name: "/feed", description: "Browse recent posts", action: async () => {
69
+ deps.showMsg("Loading feed...", deps.colors.primary)
70
+ try {
71
+ const { Feed } = await import("../api/feed")
72
+ const result = await Feed.list()
73
+ const posts = (result as any).posts || []
74
+ if (posts.length === 0) deps.showMsg("No posts yet.", deps.colors.warning)
75
+ else deps.showMsg(`${posts.length} posts: ${posts.slice(0, 3).map((p: any) => p.title?.slice(0, 40)).join(" | ")}`, deps.colors.text)
76
+ } catch (err) { deps.showMsg(`Feed failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
77
+ }},
78
+ { name: "/search", description: "Search posts", action: async (parts) => {
79
+ const query = parts.slice(1).join(" ")
80
+ if (!query) { deps.showMsg("Usage: /search <query>", deps.colors.warning); return }
81
+ try {
82
+ const { Search } = await import("../api/search")
83
+ const result = await Search.query(query)
84
+ const count = result.counts?.posts || 0
85
+ const posts = result.posts || []
86
+ deps.showMsg(count > 0 ? `${count} results for "${query}": ${posts.slice(0, 3).map((p: any) => p.title?.slice(0, 30)).join(" | ")}` : `No results for "${query}"`, count > 0 ? deps.colors.success : deps.colors.warning)
87
+ } catch (err) { deps.showMsg(`Search failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
88
+ }},
89
+ { name: "/config", description: "Show current configuration", action: async () => {
90
+ try {
91
+ const { Config } = await import("../config")
92
+ const cfg = await Config.load()
93
+ const providers = cfg.providers || {}
94
+ const keys = Object.keys(providers)
95
+ const model = cfg.model || "claude-sonnet-4-20250514"
96
+ deps.showMsg(`Model: ${model} | Providers: ${keys.length > 0 ? keys.join(", ") : "none"}`, deps.colors.text)
97
+ } catch { deps.showMsg("Failed to load config", deps.colors.error) }
98
+ }},
99
+ { name: "/help", description: "Show all commands", action: () => {
100
+ deps.showMsg("Commands: /ai /model /scan /publish /feed /search /config /clear /theme /login /logout /exit", deps.colors.text)
101
+ }},
102
+ { name: "/exit", description: "Exit CodeBlog", action: () => deps.exit() },
103
+ ]
104
+ }
105
+
106
+ export const TIPS = [
107
+ "Type /ai to configure your AI provider with a URL and API key",
108
+ "Type /model to switch between available AI models",
109
+ "Use /scan to discover IDE coding sessions from Cursor, Windsurf, etc.",
110
+ "Use /publish to share your coding sessions as blog posts",
111
+ "Type /feed to browse recent posts from the community",
112
+ "Type /theme to switch between color themes",
113
+ "Press Ctrl+C to exit at any time",
114
+ "Type / to see all available commands with autocomplete",
115
+ "Just start typing to chat with AI — no command needed!",
116
+ "Use /clear to reset the conversation",
117
+ ]
118
+
119
+ export const LOGO = [
120
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
121
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d ",
122
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557",
123
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255d \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
124
+ " \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d",
125
+ " \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d ",
126
+ ]
@@ -2,15 +2,15 @@ import { createStore } from "solid-js/store"
2
2
  import { createSimpleContext } from "./helper"
3
3
 
4
4
  export type HomeRoute = { type: "home" }
5
- export type ChatRoute = { type: "chat"; sessionMessages?: Array<{ role: string; content: string }> }
6
5
  export type PostRoute = { type: "post"; postId: string }
7
6
  export type SearchRoute = { type: "search"; query: string }
8
7
  export type TrendingRoute = { type: "trending" }
9
8
  export type NotificationsRoute = { type: "notifications" }
10
9
 
11
10
  export type ThemeRoute = { type: "theme" }
11
+ export type ModelRoute = { type: "model" }
12
12
 
13
- export type Route = HomeRoute | ChatRoute | PostRoute | SearchRoute | TrendingRoute | NotificationsRoute | ThemeRoute
13
+ export type Route = HomeRoute | PostRoute | SearchRoute | TrendingRoute | NotificationsRoute | ThemeRoute | ModelRoute
14
14
 
15
15
  export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
16
16
  name: "Route",
@@ -440,7 +440,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
440
440
  const saved = load()
441
441
  const [store, setStore] = createStore({
442
442
  name: saved?.name || "codeblog",
443
- mode: (saved?.mode || "dark") as "dark" | "light",
443
+ mode: (saved?.mode || "light") as "dark" | "light",
444
444
  needsSetup: !saved,
445
445
  })
446
446