codeblog-app 2.2.0 → 2.2.2
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/commands.ts +32 -34
- package/src/tui/routes/home.tsx +85 -29
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.2",
|
|
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.4",
|
|
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/commands.ts
CHANGED
|
@@ -31,11 +31,9 @@ export interface CommandDeps {
|
|
|
31
31
|
|
|
32
32
|
export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
33
33
|
return [
|
|
34
|
-
//
|
|
34
|
+
// === Configuration & Setup ===
|
|
35
35
|
{ name: "/ai", description: "Configure AI provider (paste URL + key)", action: () => deps.startAIConfig() },
|
|
36
36
|
{ name: "/model", description: "Choose AI model", action: () => deps.navigate({ type: "model" }) },
|
|
37
|
-
{ name: "/clear", description: "Clear conversation", action: () => deps.clearChat() },
|
|
38
|
-
{ name: "/new", description: "New conversation", action: () => deps.clearChat() },
|
|
39
37
|
{ name: "/login", description: "Sign in to CodeBlog", action: async () => {
|
|
40
38
|
deps.showMsg("Opening browser for login...", deps.colors.primary)
|
|
41
39
|
await deps.onLogin()
|
|
@@ -49,21 +47,10 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
|
49
47
|
deps.onLogout()
|
|
50
48
|
} catch (err) { deps.showMsg(`Logout failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
|
|
51
49
|
}},
|
|
52
|
-
{ name: "/
|
|
53
|
-
{ name: "/
|
|
54
|
-
{ name: "/light", description: "Switch to light mode", action: () => { deps.setMode("light"); deps.showMsg("Light mode", deps.colors.text) } },
|
|
55
|
-
{ name: "/exit", description: "Exit CodeBlog", action: () => deps.exit() },
|
|
56
|
-
{ name: "/resume", description: "Resume last chat session", action: (parts) => deps.resume(parts[1]) },
|
|
57
|
-
{ name: "/history", description: "Show recent chat sessions", action: () => {
|
|
58
|
-
try {
|
|
59
|
-
const sessions = deps.listSessions()
|
|
60
|
-
if (sessions.length === 0) { deps.showMsg("No chat history yet", deps.colors.warning); return }
|
|
61
|
-
const lines = sessions.map((s, i) => `${i + 1}. ${s.title || "(untitled)"} (${s.count} msgs, ${new Date(s.time).toLocaleDateString()})`)
|
|
62
|
-
deps.showMsg(lines.join(" | "), deps.colors.text)
|
|
63
|
-
} catch { deps.showMsg("Failed to load history", deps.colors.error) }
|
|
64
|
-
}},
|
|
50
|
+
{ name: "/config", description: "Show configuration", needsAI: true, action: () => deps.send("Show my current CodeBlog configuration — AI provider, model, login status.") },
|
|
51
|
+
{ name: "/status", description: "Check setup status", needsAI: true, action: () => deps.send("Check my CodeBlog status — login, config, detected IDEs, agent info.") },
|
|
65
52
|
|
|
66
|
-
// === Session
|
|
53
|
+
// === Session Management ===
|
|
67
54
|
{ name: "/scan", description: "Scan IDE coding sessions", needsAI: true, action: () => deps.send("Scan my local IDE coding sessions and tell me what you found. Show sources, projects, and session counts.") },
|
|
68
55
|
{ name: "/read", description: "Read a session: /read <index>", needsAI: true, action: (parts) => {
|
|
69
56
|
const idx = parts[1]
|
|
@@ -74,7 +61,7 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
|
74
61
|
deps.send(idx ? `Analyze session #${idx} — extract topics, problems, solutions, code snippets, and insights.` : "Scan my sessions and analyze the most interesting one.")
|
|
75
62
|
}},
|
|
76
63
|
|
|
77
|
-
// ===
|
|
64
|
+
// === Publishing ===
|
|
78
65
|
{ name: "/publish", description: "Auto-publish a coding session", needsAI: true, action: () => deps.send("Scan my IDE sessions, pick the most interesting one with enough content, and auto-publish it as a blog post on CodeBlog.") },
|
|
79
66
|
{ name: "/write", description: "Write a custom post: /write <title>", needsAI: true, action: (parts) => {
|
|
80
67
|
const title = parts.slice(1).join(" ")
|
|
@@ -82,7 +69,7 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
|
82
69
|
}},
|
|
83
70
|
{ name: "/digest", description: "Weekly coding digest", needsAI: true, action: () => deps.send("Generate a weekly coding digest from my recent sessions — aggregate projects, languages, problems, and insights. Preview it first.") },
|
|
84
71
|
|
|
85
|
-
// ===
|
|
72
|
+
// === Browse & Discover ===
|
|
86
73
|
{ name: "/feed", description: "Browse recent posts", needsAI: true, action: () => deps.send("Browse the latest posts on CodeBlog. Show me titles, authors, votes, tags, and a brief summary of each.") },
|
|
87
74
|
{ name: "/search", description: "Search posts: /search <query>", needsAI: true, action: (parts) => {
|
|
88
75
|
const query = parts.slice(1).join(" ")
|
|
@@ -100,7 +87,7 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
|
100
87
|
{ name: "/trending", description: "Trending topics", needsAI: true, action: () => deps.send("Show me trending topics on CodeBlog — top upvoted, most discussed, active agents, trending tags.") },
|
|
101
88
|
{ name: "/explore", description: "Explore & engage", needsAI: true, action: () => deps.send("Explore the CodeBlog community — find interesting posts, trending topics, and active discussions I can engage with.") },
|
|
102
89
|
|
|
103
|
-
// ===
|
|
90
|
+
// === Interact ===
|
|
104
91
|
{ name: "/comment", description: "Comment: /comment <post_id> <text>", needsAI: true, action: (parts) => {
|
|
105
92
|
const id = parts[1]
|
|
106
93
|
const text = parts.slice(2).join(" ")
|
|
@@ -128,30 +115,41 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
|
128
115
|
deps.send(id ? `Toggle bookmark on post "${id}".` : "Show me my bookmarked posts on CodeBlog.")
|
|
129
116
|
}},
|
|
130
117
|
|
|
131
|
-
// ===
|
|
132
|
-
{ name: "/debate", description: "Tech debates: /debate [topic]", needsAI: true, action: (parts) => {
|
|
133
|
-
const topic = parts.slice(1).join(" ")
|
|
134
|
-
deps.send(topic ? `Create or join a debate about "${topic}" on CodeBlog.` : "Show me active tech debates on CodeBlog.")
|
|
135
|
-
}},
|
|
136
|
-
|
|
137
|
-
// === Notifications (my_notifications) ===
|
|
138
|
-
{ name: "/notifications", description: "My notifications", needsAI: true, action: () => deps.send("Check my CodeBlog notifications and tell me what's new.") },
|
|
139
|
-
|
|
140
|
-
// === Agent tools (manage_agents, my_posts, my_dashboard, follow_user) ===
|
|
118
|
+
// === My Content & Stats ===
|
|
141
119
|
{ name: "/agents", description: "Manage agents", needsAI: true, action: () => deps.send("List my CodeBlog agents and show their status.") },
|
|
142
120
|
{ name: "/posts", description: "My posts", needsAI: true, action: () => deps.send("Show me all my posts on CodeBlog with their stats — votes, views, comments.") },
|
|
143
121
|
{ name: "/dashboard", description: "My dashboard stats", needsAI: true, action: () => deps.send("Show me my CodeBlog dashboard — total posts, votes, views, followers, and top posts.") },
|
|
122
|
+
{ name: "/notifications", description: "My notifications", needsAI: true, action: () => deps.send("Check my CodeBlog notifications and tell me what's new.") },
|
|
123
|
+
|
|
124
|
+
// === Social ===
|
|
144
125
|
{ name: "/follow", description: "Follow: /follow <username>", needsAI: true, action: (parts) => {
|
|
145
126
|
const user = parts[1]
|
|
146
127
|
deps.send(user ? `Follow user "${user}" on CodeBlog.` : "Show me who I'm following on CodeBlog.")
|
|
147
128
|
}},
|
|
129
|
+
{ name: "/debate", description: "Tech debates: /debate [topic]", needsAI: true, action: (parts) => {
|
|
130
|
+
const topic = parts.slice(1).join(" ")
|
|
131
|
+
deps.send(topic ? `Create or join a debate about "${topic}" on CodeBlog.` : "Show me active tech debates on CodeBlog.")
|
|
132
|
+
}},
|
|
148
133
|
|
|
149
|
-
// ===
|
|
150
|
-
{ name: "/
|
|
151
|
-
{ name: "/
|
|
134
|
+
// === UI & Navigation ===
|
|
135
|
+
{ name: "/clear", description: "Clear conversation", action: () => deps.clearChat() },
|
|
136
|
+
{ name: "/new", description: "New conversation", action: () => deps.clearChat() },
|
|
137
|
+
{ name: "/theme", description: "Change color theme", action: () => deps.navigate({ type: "theme" }) },
|
|
138
|
+
{ name: "/dark", description: "Switch to dark mode", action: () => { deps.setMode("dark"); deps.showMsg("Dark mode", deps.colors.text) } },
|
|
139
|
+
{ name: "/light", description: "Switch to light mode", action: () => { deps.setMode("light"); deps.showMsg("Light mode", deps.colors.text) } },
|
|
140
|
+
{ name: "/resume", description: "Resume last chat session", action: (parts) => deps.resume(parts[1]) },
|
|
141
|
+
{ name: "/history", description: "Show recent chat sessions", action: () => {
|
|
142
|
+
try {
|
|
143
|
+
const sessions = deps.listSessions()
|
|
144
|
+
if (sessions.length === 0) { deps.showMsg("No chat history yet", deps.colors.warning); return }
|
|
145
|
+
const lines = sessions.map((s, i) => `${i + 1}. ${s.title || "(untitled)"} (${s.count} msgs, ${new Date(s.time).toLocaleDateString()})`)
|
|
146
|
+
deps.showMsg(lines.join(" | "), deps.colors.text)
|
|
147
|
+
} catch { deps.showMsg("Failed to load history", deps.colors.error) }
|
|
148
|
+
}},
|
|
149
|
+
{ name: "/exit", description: "Exit CodeBlog", action: () => deps.exit() },
|
|
152
150
|
|
|
153
151
|
{ name: "/help", description: "Show all commands", action: () => {
|
|
154
|
-
deps.showMsg("/scan /read /analyze /publish /write
|
|
152
|
+
deps.showMsg("Commands grouped: Setup (/ai /login) | Sessions (/scan /read /analyze) | Publish (/publish /write) | Browse (/feed /search /trending) | Interact (/comment /vote /bookmark) | My Stuff (/agents /posts /dashboard) | UI (/clear /theme /exit)", deps.colors.text)
|
|
155
153
|
}},
|
|
156
154
|
]
|
|
157
155
|
}
|
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>
|
|
@@ -545,7 +601,7 @@ export function Home(props: {
|
|
|
545
601
|
<box flexDirection="column">
|
|
546
602
|
{/* Command autocomplete — above prompt */}
|
|
547
603
|
<Show when={showAutocomplete()}>
|
|
548
|
-
<box flexDirection="column" paddingBottom={1}>
|
|
604
|
+
<box flexDirection="column" paddingBottom={1} maxHeight={8} overflow="hidden">
|
|
549
605
|
<For each={filtered()}>
|
|
550
606
|
{(cmd, i) => {
|
|
551
607
|
const disabled = () => cmd.needsAI && !props.hasAI
|
|
@@ -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>
|