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 +65 -2
- package/package.json +8 -1
- package/src/index.ts +15 -1
- package/src/tui/app.tsx +66 -52
- package/src/tui/routes/chat.tsx +38 -17
- package/src/tui/routes/home.tsx +272 -95
package/bin/codeblog
CHANGED
|
@@ -1,2 +1,65 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
evt.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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=
|
|
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
|
-
? "
|
|
119
|
-
: "esc:back
|
|
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">{
|
|
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
|
)
|
package/src/tui/routes/chat.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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="#
|
|
167
|
+
<text fg="#0074cc">
|
|
147
168
|
<span style={{ bold: true }}>AI Chat</span>
|
|
148
169
|
</text>
|
|
149
|
-
<text fg="#6a737c">{
|
|
170
|
+
<text fg="#6a737c">{modelName()}</text>
|
|
150
171
|
<box flexGrow={1} />
|
|
151
|
-
<text fg="#6a737c">esc:back
|
|
172
|
+
<text fg="#6a737c">esc:back · /help · /model · /clear</text>
|
|
152
173
|
</box>
|
|
153
174
|
|
|
154
175
|
{/* Messages */}
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -1,125 +1,302 @@
|
|
|
1
|
-
import { createSignal,
|
|
1
|
+
import { createSignal, Show } from "solid-js"
|
|
2
2
|
import { useKeyboard } from "@opentui/solid"
|
|
3
|
-
import { useRoute
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
20
|
-
const [
|
|
21
|
-
const [
|
|
22
|
-
|
|
23
|
-
const [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
217
|
+
|
|
218
|
+
if (evt.name === "backspace") {
|
|
219
|
+
setInput((s) => s.slice(0, -1))
|
|
46
220
|
evt.preventDefault()
|
|
221
|
+
return
|
|
47
222
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
{/*
|
|
58
|
-
<box
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
{
|
|
66
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
</
|
|
269
|
+
</box>
|
|
78
270
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
{/*
|
|
92
|
-
<box
|
|
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
|
}
|