codeblog-app 2.4.0 → 2.5.1
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 +7 -7
- package/src/auth/oauth.ts +4 -0
- package/src/cli/cmd/setup.ts +63 -4
- package/src/index.ts +14 -1
- package/src/tui/app.tsx +140 -77
- package/src/tui/routes/home.tsx +3 -2
- package/src/tui/routes/setup.tsx +219 -204
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": "2.
|
|
4
|
+
"version": "2.5.1",
|
|
5
5
|
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"typescript": "5.8.2"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"codeblog-app-darwin-arm64": "2.
|
|
62
|
-
"codeblog-app-darwin-x64": "2.
|
|
63
|
-
"codeblog-app-linux-arm64": "2.
|
|
64
|
-
"codeblog-app-linux-x64": "2.
|
|
65
|
-
"codeblog-app-windows-x64": "2.
|
|
61
|
+
"codeblog-app-darwin-arm64": "2.5.1",
|
|
62
|
+
"codeblog-app-darwin-x64": "2.5.1",
|
|
63
|
+
"codeblog-app-linux-arm64": "2.5.1",
|
|
64
|
+
"codeblog-app-linux-x64": "2.5.1",
|
|
65
|
+
"codeblog-app-windows-x64": "2.5.1"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@ai-sdk/anthropic": "^3.0.44",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@opentui/core": "^0.1.79",
|
|
74
74
|
"@opentui/solid": "^0.1.79",
|
|
75
75
|
"ai": "^6.0.86",
|
|
76
|
-
"codeblog-mcp": "2.
|
|
76
|
+
"codeblog-mcp": "2.5.1",
|
|
77
77
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
78
78
|
"fuzzysort": "^3.1.0",
|
|
79
79
|
"hono": "4.10.7",
|
package/src/auth/oauth.ts
CHANGED
|
@@ -8,6 +8,8 @@ const log = Log.create({ service: "oauth" })
|
|
|
8
8
|
|
|
9
9
|
/** Set after a successful login — indicates whether the user already has agents. */
|
|
10
10
|
export let lastAuthHasAgents: boolean | undefined = undefined
|
|
11
|
+
/** Set after a successful login — number of agents the user has. */
|
|
12
|
+
export let lastAuthAgentsCount: number | undefined = undefined
|
|
11
13
|
|
|
12
14
|
export namespace OAuth {
|
|
13
15
|
export async function login(options?: { onUrl?: (url: string) => void }) {
|
|
@@ -20,6 +22,8 @@ export namespace OAuth {
|
|
|
20
22
|
const username = params.get("username") || undefined
|
|
21
23
|
const hasAgentsParam = params.get("has_agents")
|
|
22
24
|
lastAuthHasAgents = hasAgentsParam === "true" ? true : hasAgentsParam === "false" ? false : undefined
|
|
25
|
+
const agentsCountParam = params.get("agents_count")
|
|
26
|
+
lastAuthAgentsCount = agentsCountParam ? parseInt(agentsCountParam, 10) : undefined
|
|
23
27
|
|
|
24
28
|
if (key) {
|
|
25
29
|
let ownerMismatch = ""
|
package/src/cli/cmd/setup.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { Auth } from "../../auth"
|
|
3
|
-
import { OAuth, lastAuthHasAgents } from "../../auth/oauth"
|
|
3
|
+
import { OAuth, lastAuthHasAgents, lastAuthAgentsCount } from "../../auth/oauth"
|
|
4
4
|
import { McpBridge } from "../../mcp/client"
|
|
5
5
|
import { UI } from "../ui"
|
|
6
6
|
import { Config } from "../../config"
|
|
@@ -670,6 +670,61 @@ async function createAgentViaAPI(opts: {
|
|
|
670
670
|
}
|
|
671
671
|
}
|
|
672
672
|
|
|
673
|
+
async function agentSelectionPrompt(): Promise<void> {
|
|
674
|
+
await UI.typeText("You have multiple agents. Let's make sure the right one is active.", { charDelay: 10 })
|
|
675
|
+
console.log("")
|
|
676
|
+
|
|
677
|
+
const auth = await Auth.get()
|
|
678
|
+
if (!auth?.value) return
|
|
679
|
+
|
|
680
|
+
const base = await Config.url()
|
|
681
|
+
let agents: Array<{ id: string; name: string; source_type: string; posts_count: number }> = []
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
const res = await fetch(`${base}/api/v1/agents/list`, {
|
|
685
|
+
headers: { Authorization: `Bearer ${auth.value}` },
|
|
686
|
+
})
|
|
687
|
+
if (res.ok) {
|
|
688
|
+
const data = await res.json() as { agents?: Array<{ id: string; name: string; source_type: string; posts_count: number; activated: boolean }> }
|
|
689
|
+
agents = (data.agents || []).filter((a) => a.activated)
|
|
690
|
+
}
|
|
691
|
+
} catch {}
|
|
692
|
+
|
|
693
|
+
if (agents.length <= 1) return
|
|
694
|
+
|
|
695
|
+
const options = agents.map((a) => `${a.name} (${a.source_type}, ${a.posts_count} posts)`)
|
|
696
|
+
const idx = await UI.select(" Which agent should be active?", options)
|
|
697
|
+
|
|
698
|
+
if (idx >= 0 && idx < agents.length) {
|
|
699
|
+
const chosen = agents[idx]!
|
|
700
|
+
|
|
701
|
+
// Switch to the chosen agent via the switch endpoint (returns api_key)
|
|
702
|
+
try {
|
|
703
|
+
const switchRes = await fetch(`${base}/api/v1/agents/switch`, {
|
|
704
|
+
method: "POST",
|
|
705
|
+
headers: { Authorization: `Bearer ${auth.value}`, "Content-Type": "application/json" },
|
|
706
|
+
body: JSON.stringify({ agent_id: chosen.id }),
|
|
707
|
+
})
|
|
708
|
+
if (switchRes.ok) {
|
|
709
|
+
const switchData = await switchRes.json() as { agent: { api_key: string; name: string } }
|
|
710
|
+
await Auth.set({ type: "apikey", value: switchData.agent.api_key, username: auth.username })
|
|
711
|
+
await Config.saveActiveAgent(switchData.agent.name, auth.username)
|
|
712
|
+
|
|
713
|
+
// Sync to MCP config
|
|
714
|
+
try {
|
|
715
|
+
await McpBridge.callTool("codeblog_setup", { api_key: switchData.agent.api_key })
|
|
716
|
+
} catch {}
|
|
717
|
+
|
|
718
|
+
UI.success(`Active agent: ${switchData.agent.name}`)
|
|
719
|
+
} else {
|
|
720
|
+
UI.error("Failed to switch agent. You can switch later with: codeblog agent switch")
|
|
721
|
+
}
|
|
722
|
+
} catch {
|
|
723
|
+
UI.error("Failed to switch agent. You can switch later with: codeblog agent switch")
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
673
728
|
async function agentCreationWizard(): Promise<void> {
|
|
674
729
|
await UI.typeText("Now let's create your AI Agent!", { charDelay: 10 })
|
|
675
730
|
await UI.typeText("Your agent is your coding persona on CodeBlog — it represents you and your coding style.", { charDelay: 10 })
|
|
@@ -776,10 +831,10 @@ export const SetupCommand: CommandModule = {
|
|
|
776
831
|
describe: "First-time setup wizard: authenticate, scan, publish, configure AI",
|
|
777
832
|
handler: async () => {
|
|
778
833
|
// Phase 1: Welcome
|
|
779
|
-
|
|
834
|
+
Bun.stderr.write(UI.logo() + "\n")
|
|
780
835
|
await UI.typeText("Welcome to CodeBlog!", { charDelay: 20 })
|
|
781
836
|
await UI.typeText("The AI-powered coding forum in your terminal.", { charDelay: 15 })
|
|
782
|
-
|
|
837
|
+
Bun.stderr.write("\n")
|
|
783
838
|
|
|
784
839
|
// Phase 2: Authentication
|
|
785
840
|
const alreadyAuthed = await Auth.authenticated()
|
|
@@ -814,11 +869,15 @@ export const SetupCommand: CommandModule = {
|
|
|
814
869
|
console.log("")
|
|
815
870
|
await runAISetupWizard("setup")
|
|
816
871
|
|
|
817
|
-
// Phase 3.5: Agent creation
|
|
872
|
+
// Phase 3.5: Agent creation or selection
|
|
818
873
|
const needsAgent = lastAuthHasAgents === false || (lastAuthHasAgents === undefined && !(await Auth.get())?.type?.startsWith("apikey"))
|
|
819
874
|
if (needsAgent) {
|
|
820
875
|
UI.divider()
|
|
821
876
|
await agentCreationWizard()
|
|
877
|
+
} else if (lastAuthAgentsCount !== undefined && lastAuthAgentsCount > 1) {
|
|
878
|
+
// User has multiple agents — offer selection
|
|
879
|
+
UI.divider()
|
|
880
|
+
await agentSelectionPrompt()
|
|
822
881
|
}
|
|
823
882
|
|
|
824
883
|
// Phase 4: Interactive scan & publish
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import yargs from "yargs"
|
|
2
|
+
import path from "path"
|
|
2
3
|
import { hideBin } from "yargs/helpers"
|
|
3
4
|
import { Log } from "./util/log"
|
|
4
5
|
import { UI } from "./cli/ui"
|
|
5
6
|
import { EOL } from "os"
|
|
6
7
|
import { McpBridge } from "./mcp/client"
|
|
7
8
|
import { Auth } from "./auth"
|
|
9
|
+
import { Global } from "./global"
|
|
8
10
|
import { checkAndAutoUpdate } from "./cli/auto-update"
|
|
9
11
|
|
|
10
12
|
// Commands
|
|
@@ -171,10 +173,21 @@ if (!hasSubcommand && !isHelp && !isVersion) {
|
|
|
171
173
|
await Log.init({ print: false })
|
|
172
174
|
Log.Default.info("codeblog", { version: VERSION, args: [] })
|
|
173
175
|
|
|
176
|
+
// Theme setup — must happen before anything else so all UI is readable
|
|
177
|
+
const themePath = path.join(Global.Path.config, "theme.json")
|
|
178
|
+
let hasTheme = false
|
|
179
|
+
try { await Bun.file(themePath).text(); hasTheme = true } catch {}
|
|
180
|
+
if (!hasTheme) {
|
|
181
|
+
const { themeSetupTui } = await import("./tui/app")
|
|
182
|
+
await themeSetupTui()
|
|
183
|
+
// Clear screen on both stdout and stderr to remove renderer cleanup artifacts
|
|
184
|
+
process.stdout.write("\x1b[2J\x1b[H")
|
|
185
|
+
process.stderr.write("\x1b[2J\x1b[H")
|
|
186
|
+
}
|
|
187
|
+
|
|
174
188
|
const authed = await Auth.authenticated()
|
|
175
189
|
if (!authed) {
|
|
176
190
|
console.log("")
|
|
177
|
-
// Use the statically imported SetupCommand
|
|
178
191
|
await (SetupCommand.handler as Function)({})
|
|
179
192
|
|
|
180
193
|
// Check if setup completed successfully
|
package/src/tui/app.tsx
CHANGED
|
@@ -4,7 +4,7 @@ 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 { ThemePicker } from "./routes/setup"
|
|
7
|
+
import { ThemeSetup, ThemePicker } from "./routes/setup"
|
|
8
8
|
import { ModelPicker } from "./routes/model"
|
|
9
9
|
import { Post } from "./routes/post"
|
|
10
10
|
import { Search } from "./routes/search"
|
|
@@ -15,16 +15,79 @@ import { emitInputIntent, isShiftEnterSequence } from "./input-intent"
|
|
|
15
15
|
import pkg from "../../package.json"
|
|
16
16
|
const VERSION = pkg.version
|
|
17
17
|
|
|
18
|
+
const RENDER_OPTS = {
|
|
19
|
+
targetFps: 30,
|
|
20
|
+
exitOnCtrlC: false,
|
|
21
|
+
autoFocus: false,
|
|
22
|
+
openConsoleOnError: false,
|
|
23
|
+
useKittyKeyboard: {
|
|
24
|
+
disambiguate: true,
|
|
25
|
+
alternateKeys: true,
|
|
26
|
+
events: true,
|
|
27
|
+
allKeysAsEscapes: true,
|
|
28
|
+
reportText: true,
|
|
29
|
+
},
|
|
30
|
+
prependInputHandlers: [
|
|
31
|
+
(sequence: string) => {
|
|
32
|
+
if (!isShiftEnterSequence(sequence)) return false
|
|
33
|
+
emitInputIntent("newline")
|
|
34
|
+
return true
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
function enableModifyOtherKeys() {
|
|
19
40
|
if (!process.stdout.isTTY) return () => {}
|
|
20
|
-
// Ask xterm-compatible terminals to include modifier info for keys like Enter.
|
|
21
41
|
process.stdout.write("\x1b[>4;2m")
|
|
22
42
|
return () => {
|
|
23
|
-
// Disable modifyOtherKeys on exit.
|
|
24
43
|
process.stdout.write("\x1b[>4m")
|
|
25
44
|
}
|
|
26
45
|
}
|
|
27
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Standalone theme setup TUI — runs before the main app for first-time users.
|
|
49
|
+
* Renders ThemeSetup full-screen, then destroys itself when done.
|
|
50
|
+
*/
|
|
51
|
+
export function themeSetupTui() {
|
|
52
|
+
return new Promise<void>((resolve) => {
|
|
53
|
+
const restoreModifiers = enableModifyOtherKeys()
|
|
54
|
+
|
|
55
|
+
function ThemeSetupApp() {
|
|
56
|
+
const renderer = useRenderer()
|
|
57
|
+
const dimensions = useTerminalDimensions()
|
|
58
|
+
|
|
59
|
+
useKeyboard((evt) => {
|
|
60
|
+
if (evt.ctrl && evt.name === "c") {
|
|
61
|
+
renderer.setTerminalTitle("")
|
|
62
|
+
renderer.destroy()
|
|
63
|
+
restoreModifiers()
|
|
64
|
+
process.exit(0)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<box flexDirection="column" width={dimensions().width} height={dimensions().height}>
|
|
70
|
+
<ThemeSetup onDone={() => {
|
|
71
|
+
renderer.setTerminalTitle("")
|
|
72
|
+
renderer.destroy()
|
|
73
|
+
restoreModifiers()
|
|
74
|
+
resolve()
|
|
75
|
+
}} />
|
|
76
|
+
</box>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
() => (
|
|
82
|
+
<ThemeProvider>
|
|
83
|
+
<ThemeSetupApp />
|
|
84
|
+
</ThemeProvider>
|
|
85
|
+
),
|
|
86
|
+
RENDER_OPTS,
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
28
91
|
export function tui(input: { onExit?: () => Promise<void> }) {
|
|
29
92
|
return new Promise<void>(async (resolve) => {
|
|
30
93
|
const restoreModifiers = enableModifyOtherKeys()
|
|
@@ -38,26 +101,7 @@ export function tui(input: { onExit?: () => Promise<void> }) {
|
|
|
38
101
|
</ThemeProvider>
|
|
39
102
|
</ExitProvider>
|
|
40
103
|
),
|
|
41
|
-
|
|
42
|
-
targetFps: 30,
|
|
43
|
-
exitOnCtrlC: false,
|
|
44
|
-
autoFocus: false,
|
|
45
|
-
openConsoleOnError: false,
|
|
46
|
-
useKittyKeyboard: {
|
|
47
|
-
disambiguate: true,
|
|
48
|
-
alternateKeys: true,
|
|
49
|
-
events: true,
|
|
50
|
-
allKeysAsEscapes: true,
|
|
51
|
-
reportText: true,
|
|
52
|
-
},
|
|
53
|
-
prependInputHandlers: [
|
|
54
|
-
(sequence) => {
|
|
55
|
-
if (!isShiftEnterSequence(sequence)) return false
|
|
56
|
-
emitInputIntent("newline")
|
|
57
|
-
return true
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
},
|
|
104
|
+
RENDER_OPTS,
|
|
61
105
|
)
|
|
62
106
|
})
|
|
63
107
|
}
|
|
@@ -71,6 +115,7 @@ function App() {
|
|
|
71
115
|
const [loggedIn, setLoggedIn] = createSignal(false)
|
|
72
116
|
const [username, setUsername] = createSignal("")
|
|
73
117
|
const [activeAgent, setActiveAgent] = createSignal("")
|
|
118
|
+
const [agentCount, setAgentCount] = createSignal(0)
|
|
74
119
|
const [hasAI, setHasAI] = createSignal(false)
|
|
75
120
|
const [aiProvider, setAiProvider] = createSignal("")
|
|
76
121
|
const [modelName, setModelName] = createSignal("")
|
|
@@ -118,6 +163,21 @@ function App() {
|
|
|
118
163
|
}
|
|
119
164
|
setActiveAgent(name)
|
|
120
165
|
await Config.saveActiveAgent(name, username || undefined)
|
|
166
|
+
// Fetch agent count for multi-agent display
|
|
167
|
+
try {
|
|
168
|
+
const listRes = await fetch(`${base}/api/v1/agents/list`, {
|
|
169
|
+
headers: { Authorization: `Bearer ${token.value}` },
|
|
170
|
+
})
|
|
171
|
+
if (listRes.ok) {
|
|
172
|
+
const listData = await listRes.json() as { agents?: Array<{ activated: boolean }> }
|
|
173
|
+
const activated = listData.agents?.filter((a) => a.activated)?.length || 0
|
|
174
|
+
setAgentCount(activated)
|
|
175
|
+
} else {
|
|
176
|
+
setAgentCount(0)
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
setAgentCount(0)
|
|
180
|
+
}
|
|
121
181
|
} catch {
|
|
122
182
|
if (!cached) setActiveAgent("")
|
|
123
183
|
}
|
|
@@ -165,60 +225,63 @@ function App() {
|
|
|
165
225
|
|
|
166
226
|
return (
|
|
167
227
|
<box flexDirection="column" width={dimensions().width} height={dimensions().height}>
|
|
168
|
-
<
|
|
169
|
-
<
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
|
|
228
|
+
<Show when={!theme.needsSetup} fallback={<ThemeSetup />}>
|
|
229
|
+
<Switch>
|
|
230
|
+
<Match when={route.data.type === "home"}>
|
|
231
|
+
<Home
|
|
232
|
+
loggedIn={loggedIn()}
|
|
233
|
+
username={username()}
|
|
234
|
+
activeAgent={activeAgent()}
|
|
235
|
+
agentCount={agentCount()}
|
|
236
|
+
hasAI={hasAI()}
|
|
237
|
+
aiProvider={aiProvider()}
|
|
238
|
+
modelName={modelName()}
|
|
239
|
+
onLogin={async () => {
|
|
240
|
+
try {
|
|
241
|
+
const { OAuth } = await import("../auth/oauth")
|
|
242
|
+
await OAuth.login()
|
|
243
|
+
await refreshAuth()
|
|
244
|
+
return { ok: true }
|
|
245
|
+
} catch (err) {
|
|
246
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
247
|
+
await refreshAuth()
|
|
248
|
+
return { ok: false, error: `Login failed: ${msg}` }
|
|
249
|
+
}
|
|
250
|
+
}}
|
|
251
|
+
onLogout={() => { setLoggedIn(false); setUsername(""); setActiveAgent("") }}
|
|
252
|
+
onAIConfigured={refreshAI}
|
|
253
|
+
/>
|
|
254
|
+
</Match>
|
|
255
|
+
<Match when={route.data.type === "theme"}>
|
|
256
|
+
<ThemePicker onDone={() => route.navigate({ type: "home" })} />
|
|
257
|
+
</Match>
|
|
258
|
+
<Match when={route.data.type === "model"}>
|
|
259
|
+
<ModelPicker onDone={async (model) => {
|
|
260
|
+
if (model) setModelName(model)
|
|
261
|
+
await refreshAI()
|
|
262
|
+
route.navigate({ type: "home" })
|
|
263
|
+
}} />
|
|
264
|
+
</Match>
|
|
265
|
+
<Match when={route.data.type === "post"}>
|
|
266
|
+
<Post />
|
|
267
|
+
</Match>
|
|
268
|
+
<Match when={route.data.type === "search"}>
|
|
269
|
+
<Search />
|
|
270
|
+
</Match>
|
|
271
|
+
<Match when={route.data.type === "trending"}>
|
|
272
|
+
<Trending />
|
|
273
|
+
</Match>
|
|
274
|
+
<Match when={route.data.type === "notifications"}>
|
|
275
|
+
<Notifications />
|
|
276
|
+
</Match>
|
|
277
|
+
</Switch>
|
|
278
|
+
|
|
279
|
+
{/* Status bar — only version */}
|
|
280
|
+
<box paddingLeft={2} paddingRight={2} flexShrink={0} flexDirection="row" gap={2}>
|
|
281
|
+
<box flexGrow={1} />
|
|
282
|
+
<text fg={theme.colors.textMuted}>v{VERSION}</text>
|
|
283
|
+
</box>
|
|
284
|
+
</Show>
|
|
222
285
|
</box>
|
|
223
286
|
)
|
|
224
287
|
}
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -57,6 +57,7 @@ export function Home(props: {
|
|
|
57
57
|
loggedIn: boolean
|
|
58
58
|
username: string
|
|
59
59
|
activeAgent: string
|
|
60
|
+
agentCount: number
|
|
60
61
|
hasAI: boolean
|
|
61
62
|
aiProvider: string
|
|
62
63
|
modelName: string
|
|
@@ -156,7 +157,7 @@ export function Home(props: {
|
|
|
156
157
|
return "info"
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
function showMsg(text: string, color =
|
|
160
|
+
function showMsg(text: string, color = theme.colors.textMuted) {
|
|
160
161
|
ensureSession()
|
|
161
162
|
setMessages((p) => [...p, { role: "system", content: text, tone: tone(color) }])
|
|
162
163
|
}
|
|
@@ -823,7 +824,7 @@ export function Home(props: {
|
|
|
823
824
|
{props.loggedIn ? props.username : "Not logged in"}
|
|
824
825
|
</text>
|
|
825
826
|
<Show when={props.loggedIn && props.activeAgent}>
|
|
826
|
-
<text fg={theme.colors.textMuted}> / {props.activeAgent}</text>
|
|
827
|
+
<text fg={theme.colors.textMuted}> / {props.activeAgent}{props.agentCount > 1 ? ` (${props.agentCount} agents)` : ""}</text>
|
|
827
828
|
</Show>
|
|
828
829
|
<Show when={!props.loggedIn}>
|
|
829
830
|
<text fg={theme.colors.textMuted}> — type /login</text>
|
package/src/tui/routes/setup.tsx
CHANGED
|
@@ -1,200 +1,223 @@
|
|
|
1
|
-
import { createSignal } from "solid-js"
|
|
2
|
-
import { useKeyboard } from "@opentui/solid"
|
|
3
|
-
import { useTheme, THEME_NAMES, THEMES } from "../context/theme"
|
|
1
|
+
import { createSignal, createMemo } from "solid-js"
|
|
2
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
|
3
|
+
import { useTheme, THEME_NAMES, THEMES, type ThemeColors } from "../context/theme"
|
|
4
4
|
|
|
5
|
-
// High-contrast colors that are visible on ANY terminal background
|
|
6
5
|
const HC = {
|
|
7
6
|
title: "#ff6600",
|
|
8
|
-
text: "#
|
|
9
|
-
|
|
10
|
-
dim: "#999999",
|
|
7
|
+
text: "#aaaaaa",
|
|
8
|
+
dim: "#aaaaaa",
|
|
11
9
|
}
|
|
12
10
|
|
|
11
|
+
const LOGO_ORANGE = "#f48225"
|
|
12
|
+
const LOGO_CYAN = "#00c8ff"
|
|
13
|
+
|
|
14
|
+
const LOGO = [
|
|
15
|
+
" ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ ",
|
|
16
|
+
"██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██║ ██╔═══██╗██╔════╝ ",
|
|
17
|
+
"██║ ██║ ██║██║ ██║█████╗ ██████╔╝██║ ██║ ██║██║ ███╗",
|
|
18
|
+
"██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██║ ██║ ██║██║ ██║",
|
|
19
|
+
"╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝███████╗╚██████╔╝╚██████╔╝",
|
|
20
|
+
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ",
|
|
21
|
+
]
|
|
22
|
+
|
|
13
23
|
function resolveThemeDef(name: string) {
|
|
14
24
|
const fallback = THEMES.codeblog ?? Object.values(THEMES).find(Boolean)
|
|
15
|
-
if (!fallback)
|
|
16
|
-
throw new Error("No themes available")
|
|
17
|
-
}
|
|
25
|
+
if (!fallback) throw new Error("No themes available")
|
|
18
26
|
return THEMES[name] ?? fallback
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
type ThemeOption = { name: string; mode: "dark" | "light"; label: string; colors: ThemeColors }
|
|
30
|
+
|
|
31
|
+
const SETUP_OPTIONS: ThemeOption[] = [
|
|
32
|
+
{ name: "codeblog", mode: "dark", label: "Dark mode", colors: resolveThemeDef("codeblog").dark },
|
|
33
|
+
{ name: "codeblog", mode: "light", label: "Light mode", colors: resolveThemeDef("codeblog").light },
|
|
34
|
+
{ name: "dracula", mode: "dark", label: "Dark — Dracula", colors: resolveThemeDef("dracula").dark },
|
|
35
|
+
{ name: "tokyonight", mode: "dark", label: "Dark — Tokyo Night", colors: resolveThemeDef("tokyonight").dark },
|
|
36
|
+
{ name: "catppuccin", mode: "dark", label: "Dark — Catppuccin", colors: resolveThemeDef("catppuccin").dark },
|
|
37
|
+
{ name: "github", mode: "dark", label: "Dark — GitHub", colors: resolveThemeDef("github").dark },
|
|
38
|
+
{ name: "gruvbox", mode: "dark", label: "Dark — Gruvbox", colors: resolveThemeDef("gruvbox").dark },
|
|
39
|
+
{ name: "github", mode: "light", label: "Light — GitHub", colors: resolveThemeDef("github").light },
|
|
40
|
+
{ name: "catppuccin", mode: "light", label: "Light — Catppuccin", colors: resolveThemeDef("catppuccin").light },
|
|
41
|
+
{ name: "solarized", mode: "light", label: "Light — Solarized", colors: resolveThemeDef("solarized").light },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export function ThemeSetup(props: { onDone?: () => void }) {
|
|
22
45
|
const theme = useTheme()
|
|
23
|
-
const
|
|
24
|
-
const [
|
|
25
|
-
|
|
26
|
-
const [
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
const dimensions = useTerminalDimensions()
|
|
47
|
+
const [idx, setIdx] = createSignal(0)
|
|
48
|
+
|
|
49
|
+
const current = createMemo(() => SETUP_OPTIONS[idx()] ?? SETUP_OPTIONS[0]!)
|
|
50
|
+
|
|
51
|
+
function apply(i: number) {
|
|
52
|
+
const opt = SETUP_OPTIONS[i]
|
|
53
|
+
if (!opt) return
|
|
54
|
+
theme.set(opt.name)
|
|
55
|
+
theme.setMode(opt.mode)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
apply(0)
|
|
29
59
|
|
|
30
60
|
useKeyboard((evt) => {
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (evt.name === "down" || evt.name === "j") {
|
|
38
|
-
setModeIdx((i) => (i + 1) % modes.length)
|
|
39
|
-
evt.preventDefault()
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
if (evt.name === "return") {
|
|
43
|
-
theme.setMode(modes[modeIdx()] ?? "dark")
|
|
44
|
-
setStep("theme")
|
|
45
|
-
evt.preventDefault()
|
|
46
|
-
return
|
|
47
|
-
}
|
|
61
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
62
|
+
const next = (idx() - 1 + SETUP_OPTIONS.length) % SETUP_OPTIONS.length
|
|
63
|
+
setIdx(next)
|
|
64
|
+
apply(next)
|
|
65
|
+
evt.preventDefault()
|
|
66
|
+
return
|
|
48
67
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
theme.set(getThemeName(next))
|
|
62
|
-
evt.preventDefault()
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
if (evt.name === "return") {
|
|
66
|
-
theme.finishSetup()
|
|
67
|
-
evt.preventDefault()
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
if (evt.name === "escape") {
|
|
71
|
-
setStep("mode")
|
|
72
|
-
evt.preventDefault()
|
|
73
|
-
return
|
|
74
|
-
}
|
|
68
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
69
|
+
const next = (idx() + 1) % SETUP_OPTIONS.length
|
|
70
|
+
setIdx(next)
|
|
71
|
+
apply(next)
|
|
72
|
+
evt.preventDefault()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (evt.name === "return") {
|
|
76
|
+
theme.finishSetup()
|
|
77
|
+
props.onDone?.()
|
|
78
|
+
evt.preventDefault()
|
|
79
|
+
return
|
|
75
80
|
}
|
|
76
81
|
})
|
|
77
82
|
|
|
83
|
+
const wide = createMemo(() => (dimensions().width ?? 80) >= 90)
|
|
84
|
+
const c = createMemo(() => current().colors)
|
|
85
|
+
|
|
78
86
|
return (
|
|
79
|
-
<box flexDirection="column" flexGrow={1}
|
|
87
|
+
<box flexDirection="column" flexGrow={1} paddingLeft={2} paddingRight={2}>
|
|
80
88
|
<box flexGrow={1} minHeight={0} />
|
|
81
89
|
|
|
90
|
+
{/* Logo */}
|
|
91
|
+
<box flexShrink={0} flexDirection="column" alignItems="center">
|
|
92
|
+
{LOGO.map((line, i) => (
|
|
93
|
+
<text fg={i < 3 ? LOGO_ORANGE : LOGO_CYAN}>{line}</text>
|
|
94
|
+
))}
|
|
95
|
+
<box height={1} />
|
|
96
|
+
<text fg={HC.text}>{"The AI-powered coding forum in your terminal"}</text>
|
|
97
|
+
<box height={1} />
|
|
98
|
+
</box>
|
|
99
|
+
|
|
100
|
+
{/* Main content */}
|
|
82
101
|
<box flexShrink={0} flexDirection="column" alignItems="center">
|
|
83
102
|
<text fg={HC.title}>
|
|
84
|
-
<span style={{ bold: true }}>{"
|
|
103
|
+
<span style={{ bold: true }}>{"Choose the text style that looks best with your terminal:"}</span>
|
|
85
104
|
</text>
|
|
105
|
+
<text fg={HC.dim}>{"To change this later, run /theme"}</text>
|
|
86
106
|
<box height={1} />
|
|
87
|
-
</box>
|
|
88
107
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<box flexDirection="row" paddingLeft={2}>
|
|
97
|
-
<text fg={modeIdx() === i ? HC.selected : HC.dim}>
|
|
98
|
-
{modeIdx() === i ? "❯ " : " "}
|
|
99
|
-
</text>
|
|
100
|
-
<text fg={modeIdx() === i ? HC.selected : HC.dim}>
|
|
101
|
-
<span style={{ bold: modeIdx() === i }}>
|
|
102
|
-
{m === "dark" ? "Dark background (black/dark terminal)" : "Light background (white/light terminal)"}
|
|
103
|
-
</span>
|
|
104
|
-
</text>
|
|
105
|
-
</box>
|
|
106
|
-
))}
|
|
107
|
-
<box height={1} />
|
|
108
|
-
<text fg={HC.text}>{"↑↓ select · Enter confirm"}</text>
|
|
109
|
-
</box>
|
|
110
|
-
) : (
|
|
111
|
-
<box flexShrink={0} flexDirection="column" width="100%" maxWidth={60}>
|
|
112
|
-
<text fg={theme.colors.text}>
|
|
113
|
-
<span style={{ bold: true }}>{"Choose a color theme:"}</span>
|
|
114
|
-
</text>
|
|
115
|
-
<box height={1} />
|
|
116
|
-
{THEME_NAMES.map((name, i) => {
|
|
117
|
-
const c = getThemeColors(name)
|
|
118
|
-
return (
|
|
119
|
-
<box flexDirection="row" paddingLeft={2}>
|
|
120
|
-
<text fg={themeIdx() === i ? c.primary : theme.colors.textMuted}>
|
|
121
|
-
{themeIdx() === i ? "❯ " : " "}
|
|
108
|
+
<box flexDirection="row" justifyContent="center" gap={wide() ? 6 : 3}>
|
|
109
|
+
{/* Options list */}
|
|
110
|
+
<box flexDirection="column" width={wide() ? 28 : 26}>
|
|
111
|
+
{SETUP_OPTIONS.map((opt, i) => (
|
|
112
|
+
<box flexDirection="row">
|
|
113
|
+
<text fg={idx() === i ? opt.colors.primary : HC.dim}>
|
|
114
|
+
{idx() === i ? "❯ " : " "}
|
|
122
115
|
</text>
|
|
123
|
-
<text fg={
|
|
124
|
-
<span style={{ bold:
|
|
125
|
-
{
|
|
116
|
+
<text fg={idx() === i ? opt.colors.text : HC.dim}>
|
|
117
|
+
<span style={{ bold: idx() === i }}>
|
|
118
|
+
{`${(i + 1).toString().padStart(2)}. ${opt.label}`}
|
|
126
119
|
</span>
|
|
127
120
|
</text>
|
|
128
|
-
<text fg={
|
|
129
|
-
<text fg={c.logo2}>{"●"}</text>
|
|
130
|
-
<text fg={c.primary}>{"●"}</text>
|
|
131
|
-
<text fg={c.accent}>{"●"}</text>
|
|
132
|
-
<text fg={c.success}>{"●"}</text>
|
|
133
|
-
<text fg={c.error}>{"●"}</text>
|
|
121
|
+
{idx() === i && <text fg={opt.colors.success}>{"✓"}</text>}
|
|
134
122
|
</box>
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
))}
|
|
124
|
+
</box>
|
|
125
|
+
|
|
126
|
+
{/* Live preview */}
|
|
127
|
+
<box flexDirection="column" width={wide() ? 44 : 38}>
|
|
128
|
+
<text fg={c().text}><span style={{ bold: true }}>{"Preview"}</span></text>
|
|
129
|
+
<box height={1} />
|
|
130
|
+
<box flexDirection="column" paddingLeft={2}>
|
|
131
|
+
<text fg={c().textMuted}>{"// A coding conversation"}</text>
|
|
132
|
+
<box height={1} />
|
|
133
|
+
<box flexDirection="row">
|
|
134
|
+
<text fg={c().primary}><span style={{ bold: true }}>{"You: "}</span></text>
|
|
135
|
+
<text fg={c().text}>{"Refactor the auth module"}</text>
|
|
136
|
+
</box>
|
|
137
|
+
<box flexDirection="row">
|
|
138
|
+
<text fg={c().accent}><span style={{ bold: true }}>{"AI: "}</span></text>
|
|
139
|
+
<text fg={c().text}>{"I'll update 3 files..."}</text>
|
|
140
|
+
</box>
|
|
141
|
+
<box height={1} />
|
|
142
|
+
<text fg={c().textMuted}>{" src/auth.ts"}</text>
|
|
143
|
+
<box flexDirection="row">
|
|
144
|
+
<text fg={c().error}>{" - "}</text>
|
|
145
|
+
<text fg={c().error}>{"const token = getOld()"}</text>
|
|
146
|
+
</box>
|
|
147
|
+
<box flexDirection="row">
|
|
148
|
+
<text fg={c().success}>{" + "}</text>
|
|
149
|
+
<text fg={c().success}>{"const token = getNew()"}</text>
|
|
150
|
+
</box>
|
|
151
|
+
<box height={1} />
|
|
152
|
+
<box flexDirection="row">
|
|
153
|
+
<text fg={c().success}>{"✓ "}</text>
|
|
154
|
+
<text fg={c().text}>{"Changes applied"}</text>
|
|
155
|
+
</box>
|
|
156
|
+
<box flexDirection="row">
|
|
157
|
+
<text fg={c().warning}>{"⚠ "}</text>
|
|
158
|
+
<text fg={c().textMuted}>{"3 tests need updating"}</text>
|
|
159
|
+
</box>
|
|
160
|
+
</box>
|
|
161
|
+
</box>
|
|
139
162
|
</box>
|
|
140
|
-
|
|
163
|
+
|
|
164
|
+
<box height={1} />
|
|
165
|
+
<text fg={HC.text}>{"↑↓ select · Enter confirm"}</text>
|
|
166
|
+
</box>
|
|
141
167
|
|
|
142
168
|
<box flexGrow={1} minHeight={0} />
|
|
143
169
|
</box>
|
|
144
170
|
)
|
|
145
171
|
}
|
|
146
172
|
|
|
173
|
+
// Full theme picker (all themes × dark/light) for /theme command in main TUI
|
|
174
|
+
function buildAllOptions(): ThemeOption[] {
|
|
175
|
+
const out: ThemeOption[] = []
|
|
176
|
+
for (const name of THEME_NAMES) {
|
|
177
|
+
const def = resolveThemeDef(name)
|
|
178
|
+
out.push({ name, mode: "dark", label: `${name} — dark`, colors: def.dark })
|
|
179
|
+
out.push({ name, mode: "light", label: `${name} — light`, colors: def.light })
|
|
180
|
+
}
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const ALL_OPTIONS = buildAllOptions()
|
|
185
|
+
|
|
147
186
|
export function ThemePicker(props: { onDone: () => void }) {
|
|
148
187
|
const theme = useTheme()
|
|
149
|
-
const [idx, setIdx] = createSignal(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
188
|
+
const [idx, setIdx] = createSignal(
|
|
189
|
+
Math.max(0, ALL_OPTIONS.findIndex((o) => o.name === theme.name && o.mode === theme.mode))
|
|
190
|
+
)
|
|
191
|
+
const current = createMemo(() => ALL_OPTIONS[idx()] ?? ALL_OPTIONS[0]!)
|
|
192
|
+
|
|
193
|
+
function apply(i: number) {
|
|
194
|
+
const opt = ALL_OPTIONS[i]
|
|
195
|
+
if (!opt) return
|
|
196
|
+
theme.set(opt.name)
|
|
197
|
+
theme.setMode(opt.mode)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const c = createMemo(() => current().colors)
|
|
153
201
|
|
|
154
202
|
useKeyboard((evt) => {
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
if (evt.name === "down" || evt.name === "j") {
|
|
164
|
-
const next = (idx() + 1) % THEME_NAMES.length
|
|
165
|
-
setIdx(next)
|
|
166
|
-
theme.set(getThemeName(next))
|
|
167
|
-
evt.preventDefault()
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
if (evt.name === "tab") {
|
|
171
|
-
setTab("mode")
|
|
172
|
-
evt.preventDefault()
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
if (evt.name === "return" || evt.name === "escape") {
|
|
176
|
-
props.onDone()
|
|
177
|
-
evt.preventDefault()
|
|
178
|
-
return
|
|
179
|
-
}
|
|
203
|
+
if (evt.name === "up" || evt.name === "k") {
|
|
204
|
+
const next = (idx() - 1 + ALL_OPTIONS.length) % ALL_OPTIONS.length
|
|
205
|
+
setIdx(next)
|
|
206
|
+
apply(next)
|
|
207
|
+
evt.preventDefault()
|
|
208
|
+
return
|
|
180
209
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
if (evt.name === "return" || evt.name === "escape") {
|
|
194
|
-
props.onDone()
|
|
195
|
-
evt.preventDefault()
|
|
196
|
-
return
|
|
197
|
-
}
|
|
210
|
+
if (evt.name === "down" || evt.name === "j") {
|
|
211
|
+
const next = (idx() + 1) % ALL_OPTIONS.length
|
|
212
|
+
setIdx(next)
|
|
213
|
+
apply(next)
|
|
214
|
+
evt.preventDefault()
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if (evt.name === "return" || evt.name === "escape") {
|
|
218
|
+
props.onDone()
|
|
219
|
+
evt.preventDefault()
|
|
220
|
+
return
|
|
198
221
|
}
|
|
199
222
|
})
|
|
200
223
|
|
|
@@ -205,60 +228,52 @@ export function ThemePicker(props: { onDone: () => void }) {
|
|
|
205
228
|
<span style={{ bold: true }}>Theme Settings</span>
|
|
206
229
|
</text>
|
|
207
230
|
<box flexGrow={1} />
|
|
208
|
-
<text fg={theme.colors.textMuted}>{"
|
|
231
|
+
<text fg={theme.colors.textMuted}>{"Enter/Esc: done"}</text>
|
|
209
232
|
</box>
|
|
210
233
|
|
|
211
234
|
<box flexDirection="row" flexGrow={1} paddingTop={1} gap={4}>
|
|
212
|
-
{/*
|
|
213
|
-
<box flexDirection="column" width={
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<span style={{ bold: idx() === i }}>
|
|
227
|
-
{name.padEnd(14)}
|
|
228
|
-
</span>
|
|
229
|
-
</text>
|
|
230
|
-
<text fg={c.logo1}>{" ●"}</text>
|
|
231
|
-
<text fg={c.logo2}>{"●"}</text>
|
|
232
|
-
<text fg={c.primary}>{"●"}</text>
|
|
233
|
-
<text fg={c.accent}>{"●"}</text>
|
|
234
|
-
<text fg={c.success}>{"●"}</text>
|
|
235
|
-
<text fg={c.error}>{"●"}</text>
|
|
236
|
-
</box>
|
|
237
|
-
)
|
|
238
|
-
})}
|
|
235
|
+
{/* Options list */}
|
|
236
|
+
<box flexDirection="column" width={30}>
|
|
237
|
+
{ALL_OPTIONS.map((opt, i) => (
|
|
238
|
+
<box flexDirection="row">
|
|
239
|
+
<text fg={idx() === i ? opt.colors.primary : theme.colors.textMuted}>
|
|
240
|
+
{idx() === i ? "❯ " : " "}
|
|
241
|
+
</text>
|
|
242
|
+
<text fg={idx() === i ? opt.colors.text : theme.colors.textMuted}>
|
|
243
|
+
<span style={{ bold: idx() === i }}>
|
|
244
|
+
{opt.label}
|
|
245
|
+
</span>
|
|
246
|
+
</text>
|
|
247
|
+
</box>
|
|
248
|
+
))}
|
|
239
249
|
</box>
|
|
240
250
|
|
|
241
|
-
{/*
|
|
242
|
-
<box flexDirection="column" width={
|
|
243
|
-
<text fg={
|
|
244
|
-
<span style={{ bold: true }}>{"Background Mode"}</span>
|
|
245
|
-
</text>
|
|
251
|
+
{/* Preview */}
|
|
252
|
+
<box flexDirection="column" width={40}>
|
|
253
|
+
<text fg={c().text}><span style={{ bold: true }}>{"Preview"}</span></text>
|
|
246
254
|
<box height={1} />
|
|
247
|
-
<box flexDirection="
|
|
248
|
-
<
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
255
|
+
<box flexDirection="column" paddingLeft={2}>
|
|
256
|
+
<box flexDirection="row">
|
|
257
|
+
<text fg={c().primary}><span style={{ bold: true }}>{"You: "}</span></text>
|
|
258
|
+
<text fg={c().text}>{"Show me trending posts"}</text>
|
|
259
|
+
</box>
|
|
260
|
+
<box flexDirection="row">
|
|
261
|
+
<text fg={c().accent}><span style={{ bold: true }}>{"AI: "}</span></text>
|
|
262
|
+
<text fg={c().text}>{"Here are today's top..."}</text>
|
|
263
|
+
</box>
|
|
264
|
+
<box height={1} />
|
|
265
|
+
<box flexDirection="row">
|
|
266
|
+
<text fg={c().success}>{"✓ "}</text>
|
|
267
|
+
<text fg={c().text}>{"Published successfully"}</text>
|
|
268
|
+
</box>
|
|
269
|
+
<box flexDirection="row">
|
|
270
|
+
<text fg={c().warning}>{"⚠ "}</text>
|
|
271
|
+
<text fg={c().textMuted}>{"Rate limit reached"}</text>
|
|
272
|
+
</box>
|
|
273
|
+
<box flexDirection="row">
|
|
274
|
+
<text fg={c().error}>{"✗ "}</text>
|
|
275
|
+
<text fg={c().textMuted}>{"Connection failed"}</text>
|
|
276
|
+
</box>
|
|
262
277
|
</box>
|
|
263
278
|
</box>
|
|
264
279
|
</box>
|