codeblog-app 2.2.0 → 2.2.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/ai/chat.ts +30 -7
- package/src/ai/provider.ts +14 -1
- package/src/ai/tools.ts +8 -14
- package/src/tui/app.tsx +12 -26
- package/src/tui/routes/home.tsx +84 -28
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.2.
|
|
4
|
+
"version": "2.2.1",
|
|
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": "2.2.
|
|
60
|
-
"codeblog-app-darwin-x64": "2.2.
|
|
61
|
-
"codeblog-app-linux-arm64": "2.2.
|
|
62
|
-
"codeblog-app-linux-x64": "2.2.
|
|
63
|
-
"codeblog-app-windows-x64": "2.2.
|
|
59
|
+
"codeblog-app-darwin-arm64": "2.2.1",
|
|
60
|
+
"codeblog-app-darwin-x64": "2.2.1",
|
|
61
|
+
"codeblog-app-linux-arm64": "2.2.1",
|
|
62
|
+
"codeblog-app-linux-x64": "2.2.1",
|
|
63
|
+
"codeblog-app-windows-x64": "2.2.1"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@ai-sdk/anthropic": "^3.0.44",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@opentui/core": "^0.1.79",
|
|
72
72
|
"@opentui/solid": "^0.1.79",
|
|
73
73
|
"ai": "^6.0.86",
|
|
74
|
-
"codeblog-mcp": "^2.1.
|
|
74
|
+
"codeblog-mcp": "^2.1.3",
|
|
75
75
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
76
76
|
"fuzzysort": "^3.1.0",
|
|
77
77
|
"hono": "4.10.7",
|
package/src/ai/chat.ts
CHANGED
|
@@ -18,11 +18,16 @@ You help developers with everything on the platform:
|
|
|
18
18
|
You have 20+ tools. Use them whenever the user's request matches. Chain multiple tools if needed.
|
|
19
19
|
After a tool returns results, summarize them naturally for the user.
|
|
20
20
|
|
|
21
|
+
CRITICAL: When using tools, ALWAYS use the EXACT data returned by previous tool calls.
|
|
22
|
+
- If scan_sessions returns a path like "/Users/zhaoyifei/...", use that EXACT path
|
|
23
|
+
- NEVER modify, guess, or infer file paths — use them exactly as returned
|
|
24
|
+
- If a tool call fails with "file not found", the path is wrong — check the scan results again
|
|
25
|
+
|
|
21
26
|
Write casually like a dev talking to another dev. Be specific, opinionated, and genuine.
|
|
22
27
|
Use code examples when relevant. Think Juejin / HN / Linux.do vibes — not a conference paper.`
|
|
23
28
|
|
|
24
|
-
const MAX_TOOL_STEPS = 1
|
|
25
29
|
const IDLE_TIMEOUT_MS = 15_000 // 15s without any stream event → abort
|
|
30
|
+
const DEFAULT_MAX_STEPS = 10 // Allow AI to retry tools up to 10 steps (each tool call + result = 1 step)
|
|
26
31
|
|
|
27
32
|
export namespace AIChat {
|
|
28
33
|
export interface Message {
|
|
@@ -38,10 +43,21 @@ export namespace AIChat {
|
|
|
38
43
|
onToolResult?: (name: string, result: unknown) => void
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
export
|
|
46
|
+
export interface StreamOptions {
|
|
47
|
+
maxSteps?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function stream(
|
|
51
|
+
messages: Message[],
|
|
52
|
+
callbacks: StreamCallbacks,
|
|
53
|
+
modelID?: string,
|
|
54
|
+
signal?: AbortSignal,
|
|
55
|
+
options?: StreamOptions
|
|
56
|
+
) {
|
|
42
57
|
const model = await AIProvider.getModel(modelID)
|
|
43
58
|
const tools = await getChatTools()
|
|
44
|
-
|
|
59
|
+
const maxSteps = options?.maxSteps ?? DEFAULT_MAX_STEPS
|
|
60
|
+
log.info("streaming", { model: modelID || AIProvider.DEFAULT_MODEL, messages: messages.length, toolCount: Object.keys(tools).length, maxSteps })
|
|
45
61
|
|
|
46
62
|
const history = messages
|
|
47
63
|
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
@@ -61,9 +77,10 @@ export namespace AIChat {
|
|
|
61
77
|
system: SYSTEM_PROMPT,
|
|
62
78
|
messages: history,
|
|
63
79
|
tools,
|
|
64
|
-
stopWhen: stepCountIs(
|
|
80
|
+
stopWhen: stepCountIs(maxSteps),
|
|
65
81
|
toolChoice: "auto",
|
|
66
82
|
abortSignal: internalAbort.signal,
|
|
83
|
+
experimental_toolCallStreaming: false, // Disable streaming tool calls to avoid incomplete arguments bug
|
|
67
84
|
onStepFinish: (stepResult) => {
|
|
68
85
|
log.info("onStepFinish", {
|
|
69
86
|
stepNumber: stepResult.stepNumber,
|
|
@@ -106,11 +123,13 @@ export namespace AIChat {
|
|
|
106
123
|
break
|
|
107
124
|
}
|
|
108
125
|
case "tool-call": {
|
|
109
|
-
|
|
126
|
+
const toolName = (part as any).toolName
|
|
127
|
+
const toolArgs = (part as any).args ?? (part as any).input ?? {}
|
|
128
|
+
log.info("tool-call", { toolName, args: toolArgs, partCount })
|
|
110
129
|
// Pause idle timer — tool execution happens between tool-call and tool-result
|
|
111
130
|
toolExecuting = true
|
|
112
131
|
if (idleTimer) { clearTimeout(idleTimer); idleTimer = undefined }
|
|
113
|
-
callbacks.onToolCall?.(
|
|
132
|
+
callbacks.onToolCall?.(toolName, toolArgs)
|
|
114
133
|
break
|
|
115
134
|
}
|
|
116
135
|
case "tool-result": {
|
|
@@ -120,8 +139,12 @@ export namespace AIChat {
|
|
|
120
139
|
break
|
|
121
140
|
}
|
|
122
141
|
case "tool-error" as any: {
|
|
123
|
-
|
|
142
|
+
const errorMsg = String((part as any).error).slice(0, 500)
|
|
143
|
+
log.error("tool-error", { toolName: (part as any).toolName, error: errorMsg })
|
|
124
144
|
toolExecuting = false
|
|
145
|
+
// Abort the stream on tool error to prevent infinite retry loops
|
|
146
|
+
log.info("aborting stream due to tool error")
|
|
147
|
+
internalAbort.abort()
|
|
125
148
|
break
|
|
126
149
|
}
|
|
127
150
|
case "error": {
|
package/src/ai/provider.ts
CHANGED
|
@@ -163,7 +163,20 @@ export namespace AIProvider {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
function getLanguageModel(providerID: string, modelID: string, apiKey: string, npm?: string, baseURL?: string): LanguageModel {
|
|
166
|
-
|
|
166
|
+
// Auto-detect Anthropic models and use @ai-sdk/anthropic instead of openai-compatible
|
|
167
|
+
// This fixes streaming tool call argument parsing issues with openai-compatible provider
|
|
168
|
+
let pkg = npm || PROVIDER_NPM[providerID]
|
|
169
|
+
|
|
170
|
+
// Force Anthropic SDK for Claude models, even if provider is openai-compatible
|
|
171
|
+
if (modelID.startsWith("claude-") && pkg === "@ai-sdk/openai-compatible") {
|
|
172
|
+
pkg = "@ai-sdk/anthropic"
|
|
173
|
+
log.info("auto-detected Claude model, switching from openai-compatible to @ai-sdk/anthropic", { model: modelID })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!pkg) {
|
|
177
|
+
pkg = "@ai-sdk/openai-compatible"
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
const cacheKey = `${providerID}:${pkg}:${apiKey.slice(0, 8)}`
|
|
168
181
|
|
|
169
182
|
log.info("loading model", { provider: providerID, model: modelID, pkg })
|
package/src/ai/tools.ts
CHANGED
|
@@ -91,21 +91,15 @@ export async function getChatTools(): Promise<Record<string, any>> {
|
|
|
91
91
|
inputSchema: jsonSchema(normalizeToolSchema(rawSchema)),
|
|
92
92
|
execute: async (args: any) => {
|
|
93
93
|
log.info("execute tool", { name, args })
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return resultStr.slice(0, 8000) + "\n...(truncated)"
|
|
102
|
-
}
|
|
103
|
-
return resultStr
|
|
104
|
-
} catch (err) {
|
|
105
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
106
|
-
log.error("execute tool error", { name, error: msg })
|
|
107
|
-
return JSON.stringify({ error: msg })
|
|
94
|
+
const result = await mcp(name, clean(args))
|
|
95
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result)
|
|
96
|
+
log.info("execute tool result", { name, resultType: typeof result, resultLength: resultStr.length, resultPreview: resultStr.slice(0, 300) })
|
|
97
|
+
// Truncate very large tool results to avoid overwhelming the LLM context
|
|
98
|
+
if (resultStr.length > 8000) {
|
|
99
|
+
log.info("truncating large tool result", { name, originalLength: resultStr.length })
|
|
100
|
+
return resultStr.slice(0, 8000) + "\n...(truncated)"
|
|
108
101
|
}
|
|
102
|
+
return resultStr
|
|
109
103
|
},
|
|
110
104
|
})
|
|
111
105
|
}
|
package/src/tui/app.tsx
CHANGED
|
@@ -44,6 +44,7 @@ function App() {
|
|
|
44
44
|
const renderer = useRenderer()
|
|
45
45
|
const [loggedIn, setLoggedIn] = createSignal(false)
|
|
46
46
|
const [username, setUsername] = createSignal("")
|
|
47
|
+
const [activeAgent, setActiveAgent] = createSignal("")
|
|
47
48
|
const [hasAI, setHasAI] = createSignal(false)
|
|
48
49
|
const [aiProvider, setAiProvider] = createSignal("")
|
|
49
50
|
const [modelName, setModelName] = createSignal("")
|
|
@@ -78,6 +79,15 @@ function App() {
|
|
|
78
79
|
}
|
|
79
80
|
} catch {}
|
|
80
81
|
|
|
82
|
+
// Get active agent
|
|
83
|
+
try {
|
|
84
|
+
const { Config } = await import("../config")
|
|
85
|
+
const cfg = await Config.load()
|
|
86
|
+
if (cfg.activeAgent) {
|
|
87
|
+
setActiveAgent(cfg.activeAgent)
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
|
|
81
91
|
await refreshAI()
|
|
82
92
|
})
|
|
83
93
|
|
|
@@ -103,6 +113,7 @@ function App() {
|
|
|
103
113
|
<Home
|
|
104
114
|
loggedIn={loggedIn()}
|
|
105
115
|
username={username()}
|
|
116
|
+
activeAgent={activeAgent()}
|
|
106
117
|
hasAI={hasAI()}
|
|
107
118
|
aiProvider={aiProvider()}
|
|
108
119
|
modelName={modelName()}
|
|
@@ -144,34 +155,9 @@ function App() {
|
|
|
144
155
|
</Match>
|
|
145
156
|
</Switch>
|
|
146
157
|
|
|
147
|
-
{/* Status bar —
|
|
158
|
+
{/* Status bar — only version */}
|
|
148
159
|
<box paddingLeft={2} paddingRight={2} flexShrink={0} flexDirection="row" gap={2}>
|
|
149
|
-
<text fg={theme.colors.textMuted}>{process.cwd()}</text>
|
|
150
160
|
<box flexGrow={1} />
|
|
151
|
-
<Show when={hasAI()}>
|
|
152
|
-
<text fg={theme.colors.text}>
|
|
153
|
-
<span style={{ fg: theme.colors.success }}>● </span>
|
|
154
|
-
{modelName()}
|
|
155
|
-
</text>
|
|
156
|
-
</Show>
|
|
157
|
-
<Show when={!hasAI()}>
|
|
158
|
-
<text fg={theme.colors.text}>
|
|
159
|
-
<span style={{ fg: theme.colors.error }}>○ </span>
|
|
160
|
-
no AI <span style={{ fg: theme.colors.textMuted }}>/ai</span>
|
|
161
|
-
</text>
|
|
162
|
-
</Show>
|
|
163
|
-
<Show when={loggedIn()}>
|
|
164
|
-
<text fg={theme.colors.text}>
|
|
165
|
-
<span style={{ fg: theme.colors.success }}>● </span>
|
|
166
|
-
{username() || "logged in"}
|
|
167
|
-
</text>
|
|
168
|
-
</Show>
|
|
169
|
-
<Show when={!loggedIn()}>
|
|
170
|
-
<text fg={theme.colors.text}>
|
|
171
|
-
<span style={{ fg: theme.colors.error }}>○ </span>
|
|
172
|
-
<span style={{ fg: theme.colors.textMuted }}>/login</span>
|
|
173
|
-
</text>
|
|
174
|
-
</Show>
|
|
175
161
|
<text fg={theme.colors.textMuted}>v{VERSION}</text>
|
|
176
162
|
</box>
|
|
177
163
|
</box>
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
import { createSignal, createMemo, createEffect, onCleanup, Show, For } from "solid-js"
|
|
2
2
|
import { useKeyboard, usePaste } from "@opentui/solid"
|
|
3
|
+
import { SyntaxStyle, type ThemeTokenStyle } from "@opentui/core"
|
|
3
4
|
import { useRoute } from "../context/route"
|
|
4
5
|
import { useExit } from "../context/exit"
|
|
5
|
-
import { useTheme } from "../context/theme"
|
|
6
|
+
import { useTheme, type ThemeColors } from "../context/theme"
|
|
6
7
|
import { createCommands, LOGO, TIPS, TIPS_NO_AI } from "../commands"
|
|
7
8
|
import { TOOL_LABELS } from "../../ai/tools"
|
|
8
9
|
import { mask, saveProvider } from "../../ai/configure"
|
|
9
10
|
import { ChatHistory } from "../../storage/chat"
|
|
10
11
|
|
|
12
|
+
function buildMarkdownSyntaxRules(colors: ThemeColors): ThemeTokenStyle[] {
|
|
13
|
+
return [
|
|
14
|
+
{ scope: ["default"], style: { foreground: colors.text } },
|
|
15
|
+
{ scope: ["spell", "nospell"], style: { foreground: colors.text } },
|
|
16
|
+
{ scope: ["conceal"], style: { foreground: colors.textMuted } },
|
|
17
|
+
{ scope: ["markup.heading", "markup.heading.1", "markup.heading.2", "markup.heading.3", "markup.heading.4", "markup.heading.5", "markup.heading.6"], style: { foreground: colors.primary, bold: true } },
|
|
18
|
+
{ scope: ["markup.bold", "markup.strong"], style: { foreground: colors.text, bold: true } },
|
|
19
|
+
{ scope: ["markup.italic"], style: { foreground: colors.text, italic: true } },
|
|
20
|
+
{ scope: ["markup.list"], style: { foreground: colors.text } },
|
|
21
|
+
{ scope: ["markup.quote"], style: { foreground: colors.textMuted, italic: true } },
|
|
22
|
+
{ scope: ["markup.raw", "markup.raw.block", "markup.raw.inline"], style: { foreground: colors.accent } },
|
|
23
|
+
{ scope: ["markup.link", "markup.link.url"], style: { foreground: colors.primary, underline: true } },
|
|
24
|
+
{ scope: ["markup.link.label"], style: { foreground: colors.primary, underline: true } },
|
|
25
|
+
{ scope: ["label"], style: { foreground: colors.primary } },
|
|
26
|
+
{ scope: ["comment"], style: { foreground: colors.textMuted, italic: true } },
|
|
27
|
+
{ scope: ["string", "symbol"], style: { foreground: colors.success } },
|
|
28
|
+
{ scope: ["number", "boolean"], style: { foreground: colors.accent } },
|
|
29
|
+
{ scope: ["keyword"], style: { foreground: colors.primary, italic: true } },
|
|
30
|
+
{ scope: ["keyword.function", "function.method", "function", "constructor", "variable.member"], style: { foreground: colors.primary } },
|
|
31
|
+
{ scope: ["variable", "variable.parameter", "property", "parameter"], style: { foreground: colors.text } },
|
|
32
|
+
{ scope: ["type", "module", "class"], style: { foreground: colors.warning } },
|
|
33
|
+
{ scope: ["operator", "keyword.operator", "punctuation.delimiter"], style: { foreground: colors.textMuted } },
|
|
34
|
+
{ scope: ["punctuation", "punctuation.bracket"], style: { foreground: colors.textMuted } },
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
interface ChatMsg {
|
|
12
39
|
role: "user" | "assistant" | "tool"
|
|
13
40
|
content: string
|
|
@@ -18,6 +45,7 @@ interface ChatMsg {
|
|
|
18
45
|
export function Home(props: {
|
|
19
46
|
loggedIn: boolean
|
|
20
47
|
username: string
|
|
48
|
+
activeAgent: string
|
|
21
49
|
hasAI: boolean
|
|
22
50
|
aiProvider: string
|
|
23
51
|
modelName: string
|
|
@@ -40,6 +68,7 @@ export function Home(props: {
|
|
|
40
68
|
let escCooldown = 0
|
|
41
69
|
let sessionId = ""
|
|
42
70
|
const chatting = createMemo(() => messages().length > 0 || streaming())
|
|
71
|
+
const syntaxStyle = createMemo(() => SyntaxStyle.fromTheme(buildMarkdownSyntaxRules(theme.colors)))
|
|
43
72
|
|
|
44
73
|
function ensureSession() {
|
|
45
74
|
if (!sessionId) {
|
|
@@ -151,6 +180,7 @@ export function Home(props: {
|
|
|
151
180
|
setStreaming(true)
|
|
152
181
|
setStreamText("")
|
|
153
182
|
setMessage("")
|
|
183
|
+
let summaryStreamActive = false
|
|
154
184
|
|
|
155
185
|
try {
|
|
156
186
|
const { AIChat } = await import("../../ai/chat")
|
|
@@ -165,7 +195,6 @@ export function Home(props: {
|
|
|
165
195
|
let hasToolCalls = false
|
|
166
196
|
let lastToolName = ""
|
|
167
197
|
let lastToolResult = ""
|
|
168
|
-
let summaryStreamActive = false
|
|
169
198
|
abortCtrl = new AbortController()
|
|
170
199
|
sendLog.info("calling AIChat.stream", { model: mid, msgCount: allMsgs.length })
|
|
171
200
|
await AIChat.stream(allMsgs, {
|
|
@@ -256,7 +285,7 @@ export function Home(props: {
|
|
|
256
285
|
setStreamText(""); setStreaming(false)
|
|
257
286
|
saveChat()
|
|
258
287
|
},
|
|
259
|
-
}, mid, abortCtrl.signal)
|
|
288
|
+
}, mid, abortCtrl.signal, { maxSteps: 10 })
|
|
260
289
|
sendLog.info("AIChat.stream returned normally")
|
|
261
290
|
abortCtrl = undefined
|
|
262
291
|
} catch (err) {
|
|
@@ -434,23 +463,37 @@ export function Home(props: {
|
|
|
434
463
|
))}
|
|
435
464
|
<box height={1} />
|
|
436
465
|
<text fg={theme.colors.textMuted}>The AI-powered coding forum</text>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
<box
|
|
466
|
+
|
|
467
|
+
{/* Status info below logo */}
|
|
468
|
+
<box height={1} />
|
|
469
|
+
<box flexDirection="column" alignItems="center" gap={0}>
|
|
440
470
|
<box flexDirection="row" gap={1}>
|
|
441
|
-
<text fg={props.hasAI ? theme.colors.success : theme.colors.warning}>
|
|
442
|
-
|
|
443
|
-
{props.hasAI ? `AI: ${props.modelName}` : "Type /ai to configure AI"}
|
|
471
|
+
<text fg={props.hasAI ? theme.colors.success : theme.colors.warning}>
|
|
472
|
+
{props.hasAI ? "●" : "○"}
|
|
444
473
|
</text>
|
|
474
|
+
<text fg={theme.colors.text}>
|
|
475
|
+
{props.hasAI ? props.modelName : "No AI"}
|
|
476
|
+
</text>
|
|
477
|
+
<Show when={!props.hasAI}>
|
|
478
|
+
<text fg={theme.colors.textMuted}> — type /ai</text>
|
|
479
|
+
</Show>
|
|
445
480
|
</box>
|
|
446
481
|
<box flexDirection="row" gap={1}>
|
|
447
|
-
<text fg={props.loggedIn ? theme.colors.success : theme.colors.warning}>
|
|
448
|
-
|
|
449
|
-
|
|
482
|
+
<text fg={props.loggedIn ? theme.colors.success : theme.colors.warning}>
|
|
483
|
+
{props.loggedIn ? "●" : "○"}
|
|
484
|
+
</text>
|
|
485
|
+
<text fg={theme.colors.text}>
|
|
486
|
+
{props.loggedIn ? props.username : "Not logged in"}
|
|
450
487
|
</text>
|
|
488
|
+
<Show when={props.loggedIn && props.activeAgent}>
|
|
489
|
+
<text fg={theme.colors.textMuted}> / {props.activeAgent}</text>
|
|
490
|
+
</Show>
|
|
491
|
+
<Show when={!props.loggedIn}>
|
|
492
|
+
<text fg={theme.colors.textMuted}> — type /login</text>
|
|
493
|
+
</Show>
|
|
451
494
|
</box>
|
|
452
495
|
</box>
|
|
453
|
-
</
|
|
496
|
+
</box>
|
|
454
497
|
</Show>
|
|
455
498
|
|
|
456
499
|
{/* When chatting: messages fill the space */}
|
|
@@ -483,29 +526,42 @@ export function Home(props: {
|
|
|
483
526
|
</Show>
|
|
484
527
|
{/* Assistant message — ◆ prefix */}
|
|
485
528
|
<Show when={msg.role === "assistant"}>
|
|
486
|
-
<box
|
|
487
|
-
<
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
529
|
+
<box paddingBottom={1} flexShrink={0}>
|
|
530
|
+
<code
|
|
531
|
+
filetype="markdown"
|
|
532
|
+
drawUnstyledText={false}
|
|
533
|
+
syntaxStyle={syntaxStyle()}
|
|
534
|
+
content={msg.content}
|
|
535
|
+
conceal={true}
|
|
536
|
+
fg={theme.colors.text}
|
|
537
|
+
/>
|
|
491
538
|
</box>
|
|
492
539
|
</Show>
|
|
493
540
|
</box>
|
|
494
541
|
)}
|
|
495
542
|
</For>
|
|
496
543
|
<box
|
|
497
|
-
flexDirection="row"
|
|
498
|
-
paddingBottom={streaming() ? 1 : 0}
|
|
499
544
|
flexShrink={0}
|
|
545
|
+
paddingBottom={streaming() ? 1 : 0}
|
|
500
546
|
height={streaming() ? undefined : 0}
|
|
501
547
|
overflow="hidden"
|
|
502
548
|
>
|
|
503
|
-
<
|
|
504
|
-
<
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
549
|
+
<Show when={streaming() && streamText()}>
|
|
550
|
+
<code
|
|
551
|
+
filetype="markdown"
|
|
552
|
+
drawUnstyledText={false}
|
|
553
|
+
streaming={true}
|
|
554
|
+
syntaxStyle={syntaxStyle()}
|
|
555
|
+
content={streamText()}
|
|
556
|
+
conceal={true}
|
|
557
|
+
fg={theme.colors.text}
|
|
558
|
+
/>
|
|
559
|
+
</Show>
|
|
560
|
+
<Show when={streaming() && !streamText()}>
|
|
561
|
+
<text fg={theme.colors.textMuted} wrapMode="word">
|
|
562
|
+
{"◆ " + shimmerText()}
|
|
563
|
+
</text>
|
|
564
|
+
</Show>
|
|
509
565
|
</box>
|
|
510
566
|
</scrollbox>
|
|
511
567
|
</Show>
|
|
@@ -575,11 +631,11 @@ export function Home(props: {
|
|
|
575
631
|
<text fg={theme.colors.textMuted}>{tipPool()[tipIdx % tipPool().length]}</text>
|
|
576
632
|
</box>
|
|
577
633
|
</Show>
|
|
578
|
-
{/* Input line */}
|
|
634
|
+
{/* Input line with blinking cursor */}
|
|
579
635
|
<box flexDirection="row">
|
|
580
636
|
<text fg={theme.colors.primary}><span style={{ bold: true }}>{"❯ "}</span></text>
|
|
581
637
|
<text fg={theme.colors.input}>{input()}</text>
|
|
582
|
-
<text fg={theme.colors.cursor}>{"█"}</text>
|
|
638
|
+
<text fg={theme.colors.cursor} style={{ bold: true }}>{"█"}</text>
|
|
583
639
|
</box>
|
|
584
640
|
</box>
|
|
585
641
|
</Show>
|