codeblog-app 1.4.0 → 1.5.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/package.json +6 -6
- package/src/auth/index.ts +1 -0
- package/src/auth/oauth.ts +3 -2
- package/src/cli/cmd/update.ts +75 -0
- package/src/index.ts +4 -1
- package/src/tui/app.tsx +19 -14
- package/src/tui/context/theme.tsx +274 -0
- package/src/tui/routes/chat.tsx +12 -10
- package/src/tui/routes/home.tsx +69 -39
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.5.0",
|
|
5
5
|
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"typescript": "5.8.2"
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
|
-
"codeblog-app-darwin-arm64": "1.
|
|
60
|
-
"codeblog-app-darwin-x64": "1.
|
|
61
|
-
"codeblog-app-linux-arm64": "1.
|
|
62
|
-
"codeblog-app-linux-x64": "1.
|
|
63
|
-
"codeblog-app-windows-x64": "1.
|
|
59
|
+
"codeblog-app-darwin-arm64": "1.5.0",
|
|
60
|
+
"codeblog-app-darwin-x64": "1.5.0",
|
|
61
|
+
"codeblog-app-linux-arm64": "1.5.0",
|
|
62
|
+
"codeblog-app-linux-x64": "1.5.0",
|
|
63
|
+
"codeblog-app-windows-x64": "1.5.0"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@ai-sdk/amazon-bedrock": "^4.0.60",
|
package/src/auth/index.ts
CHANGED
package/src/auth/oauth.ts
CHANGED
|
@@ -13,12 +13,13 @@ export namespace OAuth {
|
|
|
13
13
|
const { app, port } = Server.createCallbackServer(async (params) => {
|
|
14
14
|
const token = params.get("token")
|
|
15
15
|
const key = params.get("api_key")
|
|
16
|
+
const username = params.get("username") || undefined
|
|
16
17
|
|
|
17
18
|
if (key) {
|
|
18
|
-
await Auth.set({ type: "apikey", value: key })
|
|
19
|
+
await Auth.set({ type: "apikey", value: key, username })
|
|
19
20
|
log.info("authenticated with api key")
|
|
20
21
|
} else if (token) {
|
|
21
|
-
await Auth.set({ type: "jwt", value: token })
|
|
22
|
+
await Auth.set({ type: "jwt", value: token, username })
|
|
22
23
|
log.info("authenticated with jwt")
|
|
23
24
|
} else {
|
|
24
25
|
Server.stop()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { UI } from "../ui"
|
|
3
|
+
|
|
4
|
+
export const UpdateCommand: CommandModule = {
|
|
5
|
+
command: "update",
|
|
6
|
+
describe: "Update codeblog CLI to the latest version",
|
|
7
|
+
builder: (yargs) =>
|
|
8
|
+
yargs.option("force", {
|
|
9
|
+
describe: "Force update even if already on latest",
|
|
10
|
+
type: "boolean",
|
|
11
|
+
default: false,
|
|
12
|
+
}),
|
|
13
|
+
handler: async (args) => {
|
|
14
|
+
const pkg = await import("../../../package.json")
|
|
15
|
+
const current = pkg.version
|
|
16
|
+
|
|
17
|
+
UI.info(`Current version: v${current}`)
|
|
18
|
+
UI.info("Checking for updates...")
|
|
19
|
+
|
|
20
|
+
const res = await fetch("https://registry.npmjs.org/codeblog-app/latest")
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
UI.error("Failed to check for updates")
|
|
23
|
+
process.exitCode = 1
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data = await res.json() as { version: string }
|
|
28
|
+
const latest = data.version
|
|
29
|
+
|
|
30
|
+
if (current === latest && !args.force) {
|
|
31
|
+
UI.success(`Already on latest version v${current}`)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
UI.info(`Updating v${current} → v${latest}...`)
|
|
36
|
+
|
|
37
|
+
const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux"
|
|
38
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64"
|
|
39
|
+
const platform = `${os}-${arch}`
|
|
40
|
+
const pkg_name = `codeblog-app-${platform}`
|
|
41
|
+
const url = `https://registry.npmjs.org/${pkg_name}/-/${pkg_name}-${latest}.tgz`
|
|
42
|
+
|
|
43
|
+
const tmpdir = (await import("os")).tmpdir()
|
|
44
|
+
const path = await import("path")
|
|
45
|
+
const fs = await import("fs/promises")
|
|
46
|
+
const tmp = path.join(tmpdir, `codeblog-update-${Date.now()}`)
|
|
47
|
+
await fs.mkdir(tmp, { recursive: true })
|
|
48
|
+
|
|
49
|
+
const tgz = path.join(tmp, "pkg.tgz")
|
|
50
|
+
const dlRes = await fetch(url)
|
|
51
|
+
if (!dlRes.ok) {
|
|
52
|
+
UI.error(`Failed to download update for ${platform}`)
|
|
53
|
+
process.exitCode = 1
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await Bun.write(tgz, dlRes)
|
|
58
|
+
|
|
59
|
+
const proc = Bun.spawn(["tar", "-xzf", tgz, "-C", tmp], { stdout: "ignore", stderr: "ignore" })
|
|
60
|
+
await proc.exited
|
|
61
|
+
|
|
62
|
+
const bin = process.execPath
|
|
63
|
+
const ext = os === "windows" ? ".exe" : ""
|
|
64
|
+
const src = path.join(tmp, "package", "bin", `codeblog${ext}`)
|
|
65
|
+
|
|
66
|
+
await fs.copyFile(src, bin)
|
|
67
|
+
if (os !== "windows") {
|
|
68
|
+
await fs.chmod(bin, 0o755)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await fs.rm(tmp, { recursive: true, force: true })
|
|
72
|
+
|
|
73
|
+
UI.success(`Updated to v${latest}!`)
|
|
74
|
+
},
|
|
75
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -34,8 +34,9 @@ import { TuiCommand } from "./cli/cmd/tui"
|
|
|
34
34
|
import { WeeklyDigestCommand } from "./cli/cmd/weekly-digest"
|
|
35
35
|
import { TagsCommand } from "./cli/cmd/tags"
|
|
36
36
|
import { ExploreCommand } from "./cli/cmd/explore"
|
|
37
|
+
import { UpdateCommand } from "./cli/cmd/update"
|
|
37
38
|
|
|
38
|
-
const VERSION = "
|
|
39
|
+
const VERSION = (await import("../package.json")).version
|
|
39
40
|
|
|
40
41
|
process.on("unhandledRejection", (e) => {
|
|
41
42
|
Log.Default.error("rejection", {
|
|
@@ -115,6 +116,8 @@ const cli = yargs(hideBin(process.argv))
|
|
|
115
116
|
.command(DashboardCommand)
|
|
116
117
|
.command(AgentsCommand)
|
|
117
118
|
.command(MyPostsCommand)
|
|
119
|
+
// Update
|
|
120
|
+
.command(UpdateCommand)
|
|
118
121
|
.fail((msg, err) => {
|
|
119
122
|
if (
|
|
120
123
|
msg?.startsWith("Unknown argument") ||
|
package/src/tui/app.tsx
CHANGED
|
@@ -2,19 +2,23 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu
|
|
|
2
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
|
+
import { ThemeProvider, useTheme } from "./context/theme"
|
|
5
6
|
import { Home } from "./routes/home"
|
|
6
7
|
import { Chat } from "./routes/chat"
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
import pkg from "../../package.json"
|
|
10
|
+
const VERSION = pkg.version
|
|
9
11
|
|
|
10
12
|
export function tui(input: { onExit?: () => Promise<void> }) {
|
|
11
13
|
return new Promise<void>(async (resolve) => {
|
|
12
14
|
render(
|
|
13
15
|
() => (
|
|
14
16
|
<ExitProvider onExit={async () => { await input.onExit?.(); resolve() }}>
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
17
|
+
<ThemeProvider>
|
|
18
|
+
<RouteProvider>
|
|
19
|
+
<App />
|
|
20
|
+
</RouteProvider>
|
|
21
|
+
</ThemeProvider>
|
|
18
22
|
</ExitProvider>
|
|
19
23
|
),
|
|
20
24
|
{
|
|
@@ -30,6 +34,7 @@ export function tui(input: { onExit?: () => Promise<void> }) {
|
|
|
30
34
|
function App() {
|
|
31
35
|
const route = useRoute()
|
|
32
36
|
const exit = useExit()
|
|
37
|
+
const theme = useTheme()
|
|
33
38
|
const dimensions = useTerminalDimensions()
|
|
34
39
|
const renderer = useRenderer()
|
|
35
40
|
const [loggedIn, setLoggedIn] = createSignal(false)
|
|
@@ -118,26 +123,26 @@ function App() {
|
|
|
118
123
|
|
|
119
124
|
{/* Status bar */}
|
|
120
125
|
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} flexShrink={0} flexDirection="row">
|
|
121
|
-
<text fg=
|
|
126
|
+
<text fg={theme.colors.textMuted}>
|
|
122
127
|
{route.data.type === "home"
|
|
123
|
-
? "type to chat · /help · q:quit"
|
|
128
|
+
? "type to chat · /help · /theme · q:quit"
|
|
124
129
|
: "esc:back · ctrl+c:exit"}
|
|
125
130
|
</text>
|
|
126
131
|
<box flexGrow={1} />
|
|
127
132
|
<Show when={hasAI()}>
|
|
128
|
-
<text fg=
|
|
129
|
-
<text fg=
|
|
130
|
-
<text fg=
|
|
133
|
+
<text fg={theme.colors.success}>{"● "}</text>
|
|
134
|
+
<text fg={theme.colors.textMuted}>{aiProvider()}</text>
|
|
135
|
+
<text fg={theme.colors.textMuted}>{" "}</text>
|
|
131
136
|
</Show>
|
|
132
137
|
<Show when={!hasAI()}>
|
|
133
|
-
<text fg=
|
|
134
|
-
<text fg=
|
|
138
|
+
<text fg={theme.colors.error}>{"○ "}</text>
|
|
139
|
+
<text fg={theme.colors.textMuted}>{"no AI "}</text>
|
|
135
140
|
</Show>
|
|
136
|
-
<text fg={loggedIn() ?
|
|
141
|
+
<text fg={loggedIn() ? theme.colors.success : theme.colors.error}>
|
|
137
142
|
{loggedIn() ? "● " : "○ "}
|
|
138
143
|
</text>
|
|
139
|
-
<text fg=
|
|
140
|
-
<text fg=
|
|
144
|
+
<text fg={theme.colors.textMuted}>{loggedIn() ? username() || "logged in" : "not logged in"}</text>
|
|
145
|
+
<text fg={theme.colors.textMuted}>{` v${VERSION}`}</text>
|
|
141
146
|
</box>
|
|
142
147
|
</box>
|
|
143
148
|
)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { createStore } from "solid-js/store"
|
|
2
|
+
import { createSimpleContext } from "./helper"
|
|
3
|
+
|
|
4
|
+
export type ThemeColors = {
|
|
5
|
+
text: string
|
|
6
|
+
textMuted: string
|
|
7
|
+
primary: string
|
|
8
|
+
accent: string
|
|
9
|
+
success: string
|
|
10
|
+
error: string
|
|
11
|
+
warning: string
|
|
12
|
+
input: string
|
|
13
|
+
cursor: string
|
|
14
|
+
logo1: string
|
|
15
|
+
logo2: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ThemeDef = {
|
|
19
|
+
dark: ThemeColors
|
|
20
|
+
light: ThemeColors
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const codeblog: ThemeDef = {
|
|
24
|
+
dark: {
|
|
25
|
+
text: "#e7e9eb",
|
|
26
|
+
textMuted: "#6a737c",
|
|
27
|
+
primary: "#0074cc",
|
|
28
|
+
accent: "#f48225",
|
|
29
|
+
success: "#48a868",
|
|
30
|
+
error: "#d73a49",
|
|
31
|
+
warning: "#f48225",
|
|
32
|
+
input: "#e7e9eb",
|
|
33
|
+
cursor: "#6a737c",
|
|
34
|
+
logo1: "#f48225",
|
|
35
|
+
logo2: "#0074cc",
|
|
36
|
+
},
|
|
37
|
+
light: {
|
|
38
|
+
text: "#232629",
|
|
39
|
+
textMuted: "#6a737c",
|
|
40
|
+
primary: "#0074cc",
|
|
41
|
+
accent: "#f48225",
|
|
42
|
+
success: "#2ea44f",
|
|
43
|
+
error: "#cf222e",
|
|
44
|
+
warning: "#bf8700",
|
|
45
|
+
input: "#232629",
|
|
46
|
+
cursor: "#838c95",
|
|
47
|
+
logo1: "#f48225",
|
|
48
|
+
logo2: "#0074cc",
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const dracula: ThemeDef = {
|
|
53
|
+
dark: {
|
|
54
|
+
text: "#f8f8f2",
|
|
55
|
+
textMuted: "#6272a4",
|
|
56
|
+
primary: "#bd93f9",
|
|
57
|
+
accent: "#ff79c6",
|
|
58
|
+
success: "#50fa7b",
|
|
59
|
+
error: "#ff5555",
|
|
60
|
+
warning: "#f1fa8c",
|
|
61
|
+
input: "#f8f8f2",
|
|
62
|
+
cursor: "#6272a4",
|
|
63
|
+
logo1: "#ff79c6",
|
|
64
|
+
logo2: "#bd93f9",
|
|
65
|
+
},
|
|
66
|
+
light: {
|
|
67
|
+
text: "#282a36",
|
|
68
|
+
textMuted: "#6272a4",
|
|
69
|
+
primary: "#7c3aed",
|
|
70
|
+
accent: "#db2777",
|
|
71
|
+
success: "#16a34a",
|
|
72
|
+
error: "#dc2626",
|
|
73
|
+
warning: "#ca8a04",
|
|
74
|
+
input: "#282a36",
|
|
75
|
+
cursor: "#6272a4",
|
|
76
|
+
logo1: "#db2777",
|
|
77
|
+
logo2: "#7c3aed",
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const nord: ThemeDef = {
|
|
82
|
+
dark: {
|
|
83
|
+
text: "#eceff4",
|
|
84
|
+
textMuted: "#4c566a",
|
|
85
|
+
primary: "#88c0d0",
|
|
86
|
+
accent: "#81a1c1",
|
|
87
|
+
success: "#a3be8c",
|
|
88
|
+
error: "#bf616a",
|
|
89
|
+
warning: "#ebcb8b",
|
|
90
|
+
input: "#eceff4",
|
|
91
|
+
cursor: "#4c566a",
|
|
92
|
+
logo1: "#88c0d0",
|
|
93
|
+
logo2: "#81a1c1",
|
|
94
|
+
},
|
|
95
|
+
light: {
|
|
96
|
+
text: "#2e3440",
|
|
97
|
+
textMuted: "#4c566a",
|
|
98
|
+
primary: "#5e81ac",
|
|
99
|
+
accent: "#81a1c1",
|
|
100
|
+
success: "#a3be8c",
|
|
101
|
+
error: "#bf616a",
|
|
102
|
+
warning: "#d08770",
|
|
103
|
+
input: "#2e3440",
|
|
104
|
+
cursor: "#4c566a",
|
|
105
|
+
logo1: "#5e81ac",
|
|
106
|
+
logo2: "#81a1c1",
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const tokyonight: ThemeDef = {
|
|
111
|
+
dark: {
|
|
112
|
+
text: "#c0caf5",
|
|
113
|
+
textMuted: "#565f89",
|
|
114
|
+
primary: "#7aa2f7",
|
|
115
|
+
accent: "#bb9af7",
|
|
116
|
+
success: "#9ece6a",
|
|
117
|
+
error: "#f7768e",
|
|
118
|
+
warning: "#e0af68",
|
|
119
|
+
input: "#c0caf5",
|
|
120
|
+
cursor: "#565f89",
|
|
121
|
+
logo1: "#bb9af7",
|
|
122
|
+
logo2: "#7aa2f7",
|
|
123
|
+
},
|
|
124
|
+
light: {
|
|
125
|
+
text: "#343b58",
|
|
126
|
+
textMuted: "#6172b0",
|
|
127
|
+
primary: "#2e7de9",
|
|
128
|
+
accent: "#9854f1",
|
|
129
|
+
success: "#587539",
|
|
130
|
+
error: "#f52a65",
|
|
131
|
+
warning: "#8c6c3e",
|
|
132
|
+
input: "#343b58",
|
|
133
|
+
cursor: "#6172b0",
|
|
134
|
+
logo1: "#9854f1",
|
|
135
|
+
logo2: "#2e7de9",
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const monokai: ThemeDef = {
|
|
140
|
+
dark: {
|
|
141
|
+
text: "#f8f8f2",
|
|
142
|
+
textMuted: "#75715e",
|
|
143
|
+
primary: "#66d9ef",
|
|
144
|
+
accent: "#f92672",
|
|
145
|
+
success: "#a6e22e",
|
|
146
|
+
error: "#f92672",
|
|
147
|
+
warning: "#e6db74",
|
|
148
|
+
input: "#f8f8f2",
|
|
149
|
+
cursor: "#75715e",
|
|
150
|
+
logo1: "#f92672",
|
|
151
|
+
logo2: "#66d9ef",
|
|
152
|
+
},
|
|
153
|
+
light: {
|
|
154
|
+
text: "#272822",
|
|
155
|
+
textMuted: "#75715e",
|
|
156
|
+
primary: "#0089b3",
|
|
157
|
+
accent: "#c4265e",
|
|
158
|
+
success: "#718c00",
|
|
159
|
+
error: "#c4265e",
|
|
160
|
+
warning: "#c99e00",
|
|
161
|
+
input: "#272822",
|
|
162
|
+
cursor: "#75715e",
|
|
163
|
+
logo1: "#c4265e",
|
|
164
|
+
logo2: "#0089b3",
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const github: ThemeDef = {
|
|
169
|
+
dark: {
|
|
170
|
+
text: "#c9d1d9",
|
|
171
|
+
textMuted: "#8b949e",
|
|
172
|
+
primary: "#58a6ff",
|
|
173
|
+
accent: "#bc8cff",
|
|
174
|
+
success: "#3fb950",
|
|
175
|
+
error: "#f85149",
|
|
176
|
+
warning: "#d29922",
|
|
177
|
+
input: "#c9d1d9",
|
|
178
|
+
cursor: "#8b949e",
|
|
179
|
+
logo1: "#58a6ff",
|
|
180
|
+
logo2: "#bc8cff",
|
|
181
|
+
},
|
|
182
|
+
light: {
|
|
183
|
+
text: "#24292f",
|
|
184
|
+
textMuted: "#57606a",
|
|
185
|
+
primary: "#0969da",
|
|
186
|
+
accent: "#8250df",
|
|
187
|
+
success: "#1a7f37",
|
|
188
|
+
error: "#cf222e",
|
|
189
|
+
warning: "#9a6700",
|
|
190
|
+
input: "#24292f",
|
|
191
|
+
cursor: "#57606a",
|
|
192
|
+
logo1: "#0969da",
|
|
193
|
+
logo2: "#8250df",
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const solarized: ThemeDef = {
|
|
198
|
+
dark: {
|
|
199
|
+
text: "#839496",
|
|
200
|
+
textMuted: "#586e75",
|
|
201
|
+
primary: "#268bd2",
|
|
202
|
+
accent: "#d33682",
|
|
203
|
+
success: "#859900",
|
|
204
|
+
error: "#dc322f",
|
|
205
|
+
warning: "#b58900",
|
|
206
|
+
input: "#93a1a1",
|
|
207
|
+
cursor: "#586e75",
|
|
208
|
+
logo1: "#cb4b16",
|
|
209
|
+
logo2: "#268bd2",
|
|
210
|
+
},
|
|
211
|
+
light: {
|
|
212
|
+
text: "#657b83",
|
|
213
|
+
textMuted: "#93a1a1",
|
|
214
|
+
primary: "#268bd2",
|
|
215
|
+
accent: "#d33682",
|
|
216
|
+
success: "#859900",
|
|
217
|
+
error: "#dc322f",
|
|
218
|
+
warning: "#b58900",
|
|
219
|
+
input: "#586e75",
|
|
220
|
+
cursor: "#93a1a1",
|
|
221
|
+
logo1: "#cb4b16",
|
|
222
|
+
logo2: "#268bd2",
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const THEMES: Record<string, ThemeDef> = {
|
|
227
|
+
codeblog,
|
|
228
|
+
dracula,
|
|
229
|
+
nord,
|
|
230
|
+
tokyonight,
|
|
231
|
+
monokai,
|
|
232
|
+
github,
|
|
233
|
+
solarized,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const THEME_NAMES = Object.keys(THEMES)
|
|
237
|
+
|
|
238
|
+
function detect(): "dark" | "light" {
|
|
239
|
+
const env = process.env.COLORFGBG
|
|
240
|
+
if (env) {
|
|
241
|
+
const parts = env.split(";")
|
|
242
|
+
const bg = parseInt(parts[parts.length - 1] || "0", 10)
|
|
243
|
+
if (bg > 6 && bg !== 8) return "light"
|
|
244
|
+
}
|
|
245
|
+
return "dark"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|
249
|
+
name: "Theme",
|
|
250
|
+
init: () => {
|
|
251
|
+
const [store, setStore] = createStore({
|
|
252
|
+
name: "codeblog" as string,
|
|
253
|
+
mode: detect() as "dark" | "light",
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
get colors(): ThemeColors {
|
|
258
|
+
const def = THEMES[store.name] || THEMES.codeblog
|
|
259
|
+
return def[store.mode]
|
|
260
|
+
},
|
|
261
|
+
get name() { return store.name },
|
|
262
|
+
get mode() { return store.mode },
|
|
263
|
+
set(name: string) {
|
|
264
|
+
if (THEMES[name]) setStore("name", name)
|
|
265
|
+
},
|
|
266
|
+
toggle() {
|
|
267
|
+
setStore("mode", store.mode === "dark" ? "light" : "dark")
|
|
268
|
+
},
|
|
269
|
+
setMode(mode: "dark" | "light") {
|
|
270
|
+
setStore("mode", mode)
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
})
|
package/src/tui/routes/chat.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSignal, For, Show, onMount } from "solid-js"
|
|
2
2
|
import { useKeyboard } from "@opentui/solid"
|
|
3
3
|
import { useRoute } from "../context/route"
|
|
4
|
+
import { useTheme } from "../context/theme"
|
|
4
5
|
|
|
5
6
|
interface Message {
|
|
6
7
|
role: "user" | "assistant"
|
|
@@ -9,6 +10,7 @@ interface Message {
|
|
|
9
10
|
|
|
10
11
|
export function Chat() {
|
|
11
12
|
const route = useRoute()
|
|
13
|
+
const theme = useTheme()
|
|
12
14
|
const [messages, setMessages] = createSignal<Message[]>([])
|
|
13
15
|
const [streaming, setStreaming] = createSignal(false)
|
|
14
16
|
const [streamText, setStreamText] = createSignal("")
|
|
@@ -164,12 +166,12 @@ export function Chat() {
|
|
|
164
166
|
<box flexDirection="column" flexGrow={1}>
|
|
165
167
|
{/* Header */}
|
|
166
168
|
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexShrink={0} flexDirection="row" gap={1}>
|
|
167
|
-
<text fg=
|
|
169
|
+
<text fg={theme.colors.primary}>
|
|
168
170
|
<span style={{ bold: true }}>AI Chat</span>
|
|
169
171
|
</text>
|
|
170
|
-
<text fg=
|
|
172
|
+
<text fg={theme.colors.textMuted}>{modelName()}</text>
|
|
171
173
|
<box flexGrow={1} />
|
|
172
|
-
<text fg=
|
|
174
|
+
<text fg={theme.colors.textMuted}>esc:back · /help · /model · /clear</text>
|
|
173
175
|
</box>
|
|
174
176
|
|
|
175
177
|
{/* Messages */}
|
|
@@ -177,31 +179,31 @@ export function Chat() {
|
|
|
177
179
|
<For each={messages()}>
|
|
178
180
|
{(msg) => (
|
|
179
181
|
<box flexDirection="row" paddingBottom={1}>
|
|
180
|
-
<text fg={msg.role === "user" ?
|
|
182
|
+
<text fg={msg.role === "user" ? theme.colors.primary : theme.colors.success}>
|
|
181
183
|
<span style={{ bold: true }}>{msg.role === "user" ? "❯ " : "◆ "}</span>
|
|
182
184
|
</text>
|
|
183
|
-
<text fg=
|
|
185
|
+
<text fg={theme.colors.text}>{msg.content}</text>
|
|
184
186
|
</box>
|
|
185
187
|
)}
|
|
186
188
|
</For>
|
|
187
189
|
|
|
188
190
|
<Show when={streaming()}>
|
|
189
191
|
<box flexDirection="row" paddingBottom={1}>
|
|
190
|
-
<text fg=
|
|
192
|
+
<text fg={theme.colors.success}>
|
|
191
193
|
<span style={{ bold: true }}>{"◆ "}</span>
|
|
192
194
|
</text>
|
|
193
|
-
<text fg=
|
|
195
|
+
<text fg={theme.colors.textMuted}>{streamText() || "thinking..."}</text>
|
|
194
196
|
</box>
|
|
195
197
|
</Show>
|
|
196
198
|
</box>
|
|
197
199
|
|
|
198
200
|
{/* Input */}
|
|
199
201
|
<box paddingLeft={2} paddingRight={2} paddingBottom={1} flexShrink={0} flexDirection="row">
|
|
200
|
-
<text fg=
|
|
202
|
+
<text fg={theme.colors.primary}>
|
|
201
203
|
<span style={{ bold: true }}>{"❯ "}</span>
|
|
202
204
|
</text>
|
|
203
|
-
<text fg=
|
|
204
|
-
<text fg=
|
|
205
|
+
<text fg={theme.colors.input}>{inputBuf()}</text>
|
|
206
|
+
<text fg={theme.colors.cursor}>{"█"}</text>
|
|
205
207
|
</box>
|
|
206
208
|
</box>
|
|
207
209
|
)
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { createSignal, Show } from "solid-js"
|
|
|
2
2
|
import { useKeyboard } from "@opentui/solid"
|
|
3
3
|
import { useRoute } from "../context/route"
|
|
4
4
|
import { useExit } from "../context/exit"
|
|
5
|
+
import { useTheme, THEME_NAMES } from "../context/theme"
|
|
5
6
|
|
|
6
7
|
const LOGO = [
|
|
7
8
|
" ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ ",
|
|
@@ -26,6 +27,8 @@ const HELP_TEXT = [
|
|
|
26
27
|
" /notifications View notifications",
|
|
27
28
|
" /dashboard Your stats",
|
|
28
29
|
" /models List available AI models",
|
|
30
|
+
" /theme [name] Switch theme (codeblog, dracula, nord, tokyonight, monokai, github, solarized)",
|
|
31
|
+
" /dark | /light Toggle dark/light mode",
|
|
29
32
|
" /help Show this help",
|
|
30
33
|
" /exit Exit",
|
|
31
34
|
"",
|
|
@@ -41,6 +44,7 @@ export function Home(props: {
|
|
|
41
44
|
}) {
|
|
42
45
|
const route = useRoute()
|
|
43
46
|
const exit = useExit()
|
|
47
|
+
const theme = useTheme()
|
|
44
48
|
const [input, setInput] = createSignal("")
|
|
45
49
|
const [message, setMessage] = createSignal("")
|
|
46
50
|
const [messageColor, setMessageColor] = createSignal("#6a737c")
|
|
@@ -72,15 +76,42 @@ export function Home(props: {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
if (cmd === "/login") {
|
|
75
|
-
showMsg("Opening browser for login...",
|
|
79
|
+
showMsg("Opening browser for login...", theme.colors.primary)
|
|
76
80
|
await props.onLogin()
|
|
77
|
-
showMsg("Logged in!",
|
|
81
|
+
showMsg("Logged in!", theme.colors.success)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (cmd === "/theme") {
|
|
86
|
+
const name = parts[1]
|
|
87
|
+
if (!name) {
|
|
88
|
+
showMsg(`Theme: ${theme.name} (${theme.mode}) | Available: ${THEME_NAMES.join(", ")}`, theme.colors.text)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
if (THEME_NAMES.includes(name)) {
|
|
92
|
+
theme.set(name)
|
|
93
|
+
showMsg(`Theme set to ${name}`, theme.colors.success)
|
|
94
|
+
} else {
|
|
95
|
+
showMsg(`Unknown theme: ${name}. Available: ${THEME_NAMES.join(", ")}`, theme.colors.error)
|
|
96
|
+
}
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (cmd === "/dark") {
|
|
101
|
+
theme.setMode("dark")
|
|
102
|
+
showMsg("Switched to dark mode", theme.colors.success)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (cmd === "/light") {
|
|
107
|
+
theme.setMode("light")
|
|
108
|
+
showMsg("Switched to light mode", theme.colors.success)
|
|
78
109
|
return
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
if (cmd === "/config") {
|
|
82
113
|
if (parts[1] === "ai") {
|
|
83
|
-
showMsg("Use CLI: codeblog config --provider anthropic --api-key sk-...",
|
|
114
|
+
showMsg("Use CLI: codeblog config --provider anthropic --api-key sk-...", theme.colors.warning)
|
|
84
115
|
return
|
|
85
116
|
}
|
|
86
117
|
try {
|
|
@@ -89,73 +120,72 @@ export function Home(props: {
|
|
|
89
120
|
const providers = cfg.providers || {}
|
|
90
121
|
const keys = Object.keys(providers)
|
|
91
122
|
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"}`,
|
|
123
|
+
showMsg(`Model: ${model} | Providers: ${keys.length > 0 ? keys.join(", ") : "none"} | URL: ${cfg.api_url || "https://codeblog.ai"}`, theme.colors.text)
|
|
93
124
|
} catch {
|
|
94
|
-
showMsg("Failed to load config",
|
|
125
|
+
showMsg("Failed to load config", theme.colors.error)
|
|
95
126
|
}
|
|
96
127
|
return
|
|
97
128
|
}
|
|
98
129
|
|
|
99
130
|
if (cmd === "/scan") {
|
|
100
|
-
showMsg("Scanning IDE sessions...",
|
|
131
|
+
showMsg("Scanning IDE sessions...", theme.colors.primary)
|
|
101
132
|
try {
|
|
102
133
|
const { registerAllScanners, scanAll } = await import("../../scanner")
|
|
103
134
|
registerAllScanners()
|
|
104
135
|
const sessions = scanAll(10)
|
|
105
136
|
if (sessions.length === 0) {
|
|
106
|
-
showMsg("No IDE sessions found.",
|
|
137
|
+
showMsg("No IDE sessions found.", theme.colors.warning)
|
|
107
138
|
} else {
|
|
108
139
|
const summary = sessions.slice(0, 3).map((s) => `[${s.source}] ${s.project}`).join(" | ")
|
|
109
|
-
showMsg(`Found ${sessions.length} sessions: ${summary}`,
|
|
140
|
+
showMsg(`Found ${sessions.length} sessions: ${summary}`, theme.colors.success)
|
|
110
141
|
}
|
|
111
142
|
} catch (err) {
|
|
112
|
-
showMsg(`Scan failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
143
|
+
showMsg(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
|
|
113
144
|
}
|
|
114
145
|
return
|
|
115
146
|
}
|
|
116
147
|
|
|
117
148
|
if (cmd === "/publish") {
|
|
118
|
-
showMsg("Publishing sessions...",
|
|
149
|
+
showMsg("Publishing sessions...", theme.colors.primary)
|
|
119
150
|
try {
|
|
120
151
|
const { Publisher } = await import("../../publisher")
|
|
121
152
|
const results = await Publisher.scanAndPublish({ limit: 1 })
|
|
122
153
|
const ok = results.filter((r) => r.postId)
|
|
123
154
|
if (ok.length > 0) {
|
|
124
|
-
showMsg(`Published ${ok.length} post(s)!`,
|
|
155
|
+
showMsg(`Published ${ok.length} post(s)!`, theme.colors.success)
|
|
125
156
|
} else {
|
|
126
|
-
showMsg("No sessions to publish.",
|
|
157
|
+
showMsg("No sessions to publish.", theme.colors.warning)
|
|
127
158
|
}
|
|
128
159
|
} catch (err) {
|
|
129
|
-
showMsg(`Publish failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
160
|
+
showMsg(`Publish failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
|
|
130
161
|
}
|
|
131
162
|
return
|
|
132
163
|
}
|
|
133
164
|
|
|
134
165
|
if (cmd === "/ai-publish") {
|
|
135
166
|
if (!props.hasAI) {
|
|
136
|
-
showMsg("No AI configured. Use: /config ai",
|
|
167
|
+
showMsg("No AI configured. Use: /config ai", theme.colors.error)
|
|
137
168
|
return
|
|
138
169
|
}
|
|
139
|
-
showMsg("AI is writing a post from your session...",
|
|
140
|
-
|
|
141
|
-
showMsg("Use CLI: codeblog ai-publish", "#f48225")
|
|
170
|
+
showMsg("AI is writing a post from your session...", theme.colors.primary)
|
|
171
|
+
showMsg("Use CLI: codeblog ai-publish", theme.colors.warning)
|
|
142
172
|
return
|
|
143
173
|
}
|
|
144
174
|
|
|
145
175
|
if (cmd === "/feed") {
|
|
146
|
-
showMsg("Loading feed...",
|
|
176
|
+
showMsg("Loading feed...", theme.colors.primary)
|
|
147
177
|
try {
|
|
148
178
|
const { Feed } = await import("../../api/feed")
|
|
149
179
|
const result = await Feed.list()
|
|
150
180
|
const posts = (result as any).posts || []
|
|
151
181
|
if (posts.length === 0) {
|
|
152
|
-
showMsg("No posts yet.",
|
|
182
|
+
showMsg("No posts yet.", theme.colors.warning)
|
|
153
183
|
} else {
|
|
154
184
|
const summary = posts.slice(0, 3).map((p: any) => p.title?.slice(0, 40)).join(" | ")
|
|
155
|
-
showMsg(`${posts.length} posts: ${summary}`,
|
|
185
|
+
showMsg(`${posts.length} posts: ${summary}`, theme.colors.text)
|
|
156
186
|
}
|
|
157
187
|
} catch (err) {
|
|
158
|
-
showMsg(`Feed failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
188
|
+
showMsg(`Feed failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
|
|
159
189
|
}
|
|
160
190
|
return
|
|
161
191
|
}
|
|
@@ -166,9 +196,9 @@ export function Home(props: {
|
|
|
166
196
|
const models = await AIProvider.available()
|
|
167
197
|
const configured = models.filter((m) => m.hasKey)
|
|
168
198
|
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 ?
|
|
199
|
+
showMsg(configured.length > 0 ? `Available: ${names}` : "No models configured. Use: codeblog config --provider anthropic --api-key sk-...", configured.length > 0 ? theme.colors.success : theme.colors.warning)
|
|
170
200
|
} catch (err) {
|
|
171
|
-
showMsg(`Failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
201
|
+
showMsg(`Failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
|
|
172
202
|
}
|
|
173
203
|
return
|
|
174
204
|
}
|
|
@@ -176,32 +206,32 @@ export function Home(props: {
|
|
|
176
206
|
if (cmd === "/search") {
|
|
177
207
|
const query = parts.slice(1).join(" ")
|
|
178
208
|
if (!query) {
|
|
179
|
-
showMsg("Usage: /search <query>",
|
|
209
|
+
showMsg("Usage: /search <query>", theme.colors.warning)
|
|
180
210
|
return
|
|
181
211
|
}
|
|
182
212
|
try {
|
|
183
213
|
const { Posts } = await import("../../api/posts")
|
|
184
214
|
const result = await Posts.search(query)
|
|
185
215
|
const posts = (result as any).posts || []
|
|
186
|
-
showMsg(posts.length > 0 ? `${posts.length} results for "${query}"` : `No results for "${query}"`, posts.length > 0 ?
|
|
216
|
+
showMsg(posts.length > 0 ? `${posts.length} results for "${query}"` : `No results for "${query}"`, posts.length > 0 ? theme.colors.success : theme.colors.warning)
|
|
187
217
|
} catch (err) {
|
|
188
|
-
showMsg(`Search failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
218
|
+
showMsg(`Search failed: ${err instanceof Error ? err.message : String(err)}`, theme.colors.error)
|
|
189
219
|
}
|
|
190
220
|
return
|
|
191
221
|
}
|
|
192
222
|
|
|
193
223
|
if (cmd === "/trending" || cmd === "/notifications" || cmd === "/dashboard") {
|
|
194
|
-
showMsg(`Use CLI: codeblog ${cmd.slice(1)}`,
|
|
224
|
+
showMsg(`Use CLI: codeblog ${cmd.slice(1)}`, theme.colors.warning)
|
|
195
225
|
return
|
|
196
226
|
}
|
|
197
227
|
|
|
198
|
-
showMsg(`Unknown command: ${cmd}. Type /help`,
|
|
228
|
+
showMsg(`Unknown command: ${cmd}. Type /help`, theme.colors.error)
|
|
199
229
|
return
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
// Regular text → start AI chat
|
|
203
233
|
if (!props.hasAI) {
|
|
204
|
-
showMsg("No AI provider configured. Run: /config ai",
|
|
234
|
+
showMsg("No AI provider configured. Run: /config ai", theme.colors.error)
|
|
205
235
|
return
|
|
206
236
|
}
|
|
207
237
|
|
|
@@ -242,28 +272,28 @@ export function Home(props: {
|
|
|
242
272
|
{/* Logo */}
|
|
243
273
|
<box flexShrink={0} flexDirection="column">
|
|
244
274
|
{LOGO.map((line, i) => (
|
|
245
|
-
<text fg={i < 4 ?
|
|
275
|
+
<text fg={i < 4 ? theme.colors.logo1 : theme.colors.logo2}>{line}</text>
|
|
246
276
|
))}
|
|
247
277
|
</box>
|
|
248
278
|
|
|
249
279
|
<box height={1} flexShrink={0}>
|
|
250
|
-
<text fg=
|
|
280
|
+
<text fg={theme.colors.textMuted}>The AI-powered coding forum</text>
|
|
251
281
|
</box>
|
|
252
282
|
|
|
253
283
|
{/* Status indicators */}
|
|
254
284
|
<box height={2} flexShrink={0} flexDirection="column" paddingTop={1}>
|
|
255
285
|
<box flexDirection="row" gap={2}>
|
|
256
286
|
<Show when={!props.loggedIn}>
|
|
257
|
-
<text fg=
|
|
287
|
+
<text fg={theme.colors.error}>○ Not logged in — type /login</text>
|
|
258
288
|
</Show>
|
|
259
289
|
<Show when={props.loggedIn}>
|
|
260
|
-
<text fg=
|
|
290
|
+
<text fg={theme.colors.success}>● {props.username || "Logged in"}</text>
|
|
261
291
|
</Show>
|
|
262
292
|
<Show when={!props.hasAI}>
|
|
263
|
-
<text fg=
|
|
293
|
+
<text fg={theme.colors.error}>○ No AI — type /config ai</text>
|
|
264
294
|
</Show>
|
|
265
295
|
<Show when={props.hasAI}>
|
|
266
|
-
<text fg=
|
|
296
|
+
<text fg={theme.colors.success}>● {props.aiProvider}</text>
|
|
267
297
|
</Show>
|
|
268
298
|
</box>
|
|
269
299
|
</box>
|
|
@@ -271,11 +301,11 @@ export function Home(props: {
|
|
|
271
301
|
{/* Input prompt */}
|
|
272
302
|
<box width="100%" maxWidth={75} flexShrink={0} paddingTop={1}>
|
|
273
303
|
<box flexDirection="row" width="100%">
|
|
274
|
-
<text fg=
|
|
304
|
+
<text fg={theme.colors.primary}>
|
|
275
305
|
<span style={{ bold: true }}>{"❯ "}</span>
|
|
276
306
|
</text>
|
|
277
|
-
<text fg=
|
|
278
|
-
<text fg=
|
|
307
|
+
<text fg={theme.colors.input}>{input()}</text>
|
|
308
|
+
<text fg={theme.colors.cursor}>{"█"}</text>
|
|
279
309
|
</box>
|
|
280
310
|
</box>
|
|
281
311
|
|
|
@@ -290,7 +320,7 @@ export function Home(props: {
|
|
|
290
320
|
<Show when={showHelp()}>
|
|
291
321
|
<box width="100%" maxWidth={75} paddingTop={1} flexShrink={0} flexDirection="column">
|
|
292
322
|
{HELP_TEXT.map((line) => (
|
|
293
|
-
<text fg={line.startsWith(" /") ?
|
|
323
|
+
<text fg={line.startsWith(" /") ? theme.colors.primary : theme.colors.textMuted}>{line}</text>
|
|
294
324
|
))}
|
|
295
325
|
</box>
|
|
296
326
|
</Show>
|