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 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.0",
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.0",
60
- "codeblog-app-darwin-x64": "2.2.0",
61
- "codeblog-app-linux-arm64": "2.2.0",
62
- "codeblog-app-linux-x64": "2.2.0",
63
- "codeblog-app-windows-x64": "2.2.0"
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.2",
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 async function stream(messages: Message[], callbacks: StreamCallbacks, modelID?: string, signal?: AbortSignal) {
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
- log.info("streaming", { model: modelID || AIProvider.DEFAULT_MODEL, messages: messages.length, toolCount: Object.keys(tools).length })
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(MAX_TOOL_STEPS),
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
- log.info("tool-call", { toolName: (part as any).toolName, partCount })
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?.((part as any).toolName, (part as any).input ?? (part as any).args)
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
- log.error("tool-error", { toolName: (part as any).toolName, error: String((part as any).error).slice(0, 500) })
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": {
@@ -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
- const pkg = npm || PROVIDER_NPM[providerID] || "@ai-sdk/openai-compatible"
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
- try {
95
- const result = await mcp(name, clean(args))
96
- const resultStr = typeof result === "string" ? result : JSON.stringify(result)
97
- log.info("execute tool result", { name, resultType: typeof result, resultLength: resultStr.length, resultPreview: resultStr.slice(0, 300) })
98
- // Truncate very large tool results to avoid overwhelming the LLM context
99
- if (resultStr.length > 8000) {
100
- log.info("truncating large tool result", { name, originalLength: resultStr.length })
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 — like OpenCode */}
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>
@@ -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
- </box>
438
- <Show when={!props.loggedIn || !props.hasAI}>
439
- <box flexShrink={0} flexDirection="column" paddingTop={1} alignItems="center">
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}>{props.hasAI ? "✓" : "●"}</text>
442
- <text fg={props.hasAI ? theme.colors.textMuted : theme.colors.text}>
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}>{props.loggedIn ? "✓" : "●"}</text>
448
- <text fg={props.loggedIn ? theme.colors.textMuted : theme.colors.text}>
449
- {props.loggedIn ? `Logged in as ${props.username}` : "Type /login to sign in"}
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
- </Show>
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 flexDirection="row" paddingBottom={1}>
487
- <text fg={theme.colors.success} flexShrink={0}>
488
- <span style={{ bold: true }}>{""}</span>
489
- </text>
490
- <text fg={theme.colors.text} wrapMode="word" flexGrow={1} flexShrink={1}>{msg.content}</text>
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
- <text fg={theme.colors.success} flexShrink={0}>
504
- <span style={{ bold: true }}>{streaming() ? "◆ " : ""}</span>
505
- </text>
506
- <text fg={streamText() ? theme.colors.text : theme.colors.textMuted} wrapMode="word" flexGrow={1} flexShrink={1}>
507
- {streaming() ? (streamText() || shimmerText()) : ""}
508
- </text>
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>