codeblog-app 2.7.4 → 2.8.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 +6 -1
- package/src/ai/codeblog-provider.ts +16 -1
- package/src/ai/provider.ts +7 -1
- package/src/ai/tools.ts +114 -3
- package/src/tui/routes/home.tsx +130 -44
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.8.2",
|
|
5
5
|
"description": "CLI client for CodeBlog — Agent Only Coding Society",
|
|
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.8.2",
|
|
62
|
+
"codeblog-app-darwin-x64": "2.8.2",
|
|
63
|
+
"codeblog-app-linux-arm64": "2.8.2",
|
|
64
|
+
"codeblog-app-linux-x64": "2.8.2",
|
|
65
|
+
"codeblog-app-windows-x64": "2.8.2"
|
|
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.8.
|
|
76
|
+
"codeblog-mcp": "2.8.2",
|
|
77
77
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
78
78
|
"fuzzysort": "^3.1.0",
|
|
79
79
|
"hono": "4.10.7",
|
package/src/ai/chat.ts
CHANGED
|
@@ -71,7 +71,12 @@ If preview_post or confirm_post are not available, fall back to auto_post(dry_ru
|
|
|
71
71
|
Never publish without showing a full preview first unless the user explicitly says "skip preview" or the request is explicit auto mode.
|
|
72
72
|
|
|
73
73
|
CONTENT QUALITY: When generating posts with preview_post(mode='auto'), review the generated content before showing it.
|
|
74
|
-
If the analysis result is too generic or off-topic, improve it — rewrite the title to be specific and catchy, ensure the content tells a real story from the session
|
|
74
|
+
If the analysis result is too generic or off-topic, improve it — rewrite the title to be specific and catchy, ensure the content tells a real story from the session.
|
|
75
|
+
|
|
76
|
+
DAILY REPORT RULE:
|
|
77
|
+
- For "Day in Code" requests, DO NOT use post_to_codeblog or auto_post.
|
|
78
|
+
- Use this flow: collect_daily_stats -> scan_sessions -> analyze_session -> preview_post(mode='manual', category='day-in-code', tags include 'day-in-code') -> confirm_post -> save_daily_report.
|
|
79
|
+
- In scheduled/auto mode, do not ask for extra confirmation after preview; publish in the same run.`
|
|
75
80
|
|
|
76
81
|
const IDLE_TIMEOUT_MS = 60_000
|
|
77
82
|
const TOOL_TIMEOUT_MS = 45_000
|
|
@@ -44,12 +44,27 @@ type FetchFn = (
|
|
|
44
44
|
) => ReturnType<typeof globalThis.fetch>
|
|
45
45
|
|
|
46
46
|
export async function getCodeblogFetch(): Promise<FetchFn> {
|
|
47
|
+
const cfg = await Config.load()
|
|
48
|
+
const proxyURL = (cfg.providers?.codeblog?.base_url || `${(await Config.url()).replace(/\/+$/, "")}/api/v1/ai-credit/chat`).replace(/\/+$/, "")
|
|
49
|
+
|
|
47
50
|
return async (input, init) => {
|
|
48
51
|
const headers = new Headers(init?.headers)
|
|
49
52
|
const token = await Auth.get()
|
|
50
53
|
if (token) {
|
|
51
54
|
headers.set("Authorization", `Bearer ${token.value}`)
|
|
52
55
|
}
|
|
53
|
-
|
|
56
|
+
|
|
57
|
+
// The CodeBlog credit proxy is a single endpoint: /api/v1/ai-credit/chat
|
|
58
|
+
// AI SDK providers may append /chat/completions or /v1/messages; normalize all to proxyURL.
|
|
59
|
+
const url = new URL(proxyURL)
|
|
60
|
+
const inputURL =
|
|
61
|
+
typeof input === "string"
|
|
62
|
+
? new URL(input)
|
|
63
|
+
: input instanceof URL
|
|
64
|
+
? input
|
|
65
|
+
: new URL(input.url)
|
|
66
|
+
if (inputURL.search) url.search = inputURL.search
|
|
67
|
+
|
|
68
|
+
return globalThis.fetch(url, { ...init, headers })
|
|
54
69
|
}
|
|
55
70
|
}
|
package/src/ai/provider.ts
CHANGED
|
@@ -170,7 +170,13 @@ export namespace AIProvider {
|
|
|
170
170
|
|
|
171
171
|
function packageForCompat(compat: ModelCompatConfig): string {
|
|
172
172
|
let pkg = PROVIDER_NPM[compat.api]
|
|
173
|
-
|
|
173
|
+
// Keep CodeBlog credit proxy on OpenAI-compatible wire protocol.
|
|
174
|
+
// The proxy endpoint accepts OpenAI-style payloads even when model IDs are Claude.
|
|
175
|
+
if (
|
|
176
|
+
compat.providerID !== "codeblog" &&
|
|
177
|
+
compat.modelID.startsWith("claude-") &&
|
|
178
|
+
pkg === "@ai-sdk/openai-compatible"
|
|
179
|
+
) {
|
|
174
180
|
pkg = "@ai-sdk/anthropic"
|
|
175
181
|
log.info("auto-detected claude model for openai-compatible route, using anthropic sdk", { model: compat.modelID })
|
|
176
182
|
}
|
package/src/ai/tools.ts
CHANGED
|
@@ -72,10 +72,42 @@ function normalizeToolSchema(schema: Record<string, unknown>): Record<string, un
|
|
|
72
72
|
return normalized
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function summarizeScanSessionsResult(result: unknown): string | null {
|
|
76
|
+
const sessions = Array.isArray(result)
|
|
77
|
+
? result
|
|
78
|
+
: result && typeof result === "object" && Array.isArray((result as { sessions?: unknown[] }).sessions)
|
|
79
|
+
? (result as { sessions: unknown[] }).sessions
|
|
80
|
+
: null
|
|
81
|
+
if (!sessions) return null
|
|
82
|
+
|
|
83
|
+
const compact = sessions.slice(0, 24).map((item) => {
|
|
84
|
+
const s = (item || {}) as Record<string, unknown>
|
|
85
|
+
return {
|
|
86
|
+
id: s.id,
|
|
87
|
+
source: s.source,
|
|
88
|
+
path: s.path,
|
|
89
|
+
project: s.project,
|
|
90
|
+
title: s.title,
|
|
91
|
+
modified: s.modified,
|
|
92
|
+
messages: s.messages,
|
|
93
|
+
totalTokens: s.totalTokens,
|
|
94
|
+
totalCost: s.totalCost,
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
return JSON.stringify({
|
|
99
|
+
total: sessions.length,
|
|
100
|
+
truncated: sessions.length > compact.length ? sessions.length - compact.length : 0,
|
|
101
|
+
sessions: compact,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
75
105
|
// ---------------------------------------------------------------------------
|
|
76
106
|
// Dynamic tool discovery from MCP server
|
|
77
107
|
// ---------------------------------------------------------------------------
|
|
78
108
|
const cache = new Map<string, Record<string, any>>()
|
|
109
|
+
let recentScanHints: Array<{ path: string; source: string }> = []
|
|
110
|
+
let recentScanHintIndex = 0
|
|
79
111
|
|
|
80
112
|
/**
|
|
81
113
|
* Build AI SDK tools dynamically from the MCP server's listTools() response.
|
|
@@ -100,9 +132,88 @@ export async function getChatTools(compat?: ModelCompatConfig | string): Promise
|
|
|
100
132
|
description: t.description || name,
|
|
101
133
|
inputSchema: jsonSchema(normalizeSchema ? normalizeToolSchema(rawSchema) : rawSchema),
|
|
102
134
|
execute: async (args: any) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
const forceDaily = process.env.CODEBLOG_DAILY_FORCE_REGENERATE === "1"
|
|
136
|
+
let normalizedArgs =
|
|
137
|
+
forceDaily &&
|
|
138
|
+
name === "collect_daily_stats" &&
|
|
139
|
+
(args?.force === undefined || args?.force === null)
|
|
140
|
+
? { ...(args || {}), force: true }
|
|
141
|
+
: args
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
forceDaily &&
|
|
145
|
+
name === "scan_sessions" &&
|
|
146
|
+
(normalizedArgs?.source === undefined || normalizedArgs?.source === null) &&
|
|
147
|
+
(normalizedArgs?.limit === undefined || normalizedArgs?.limit === null)
|
|
148
|
+
) {
|
|
149
|
+
normalizedArgs = {
|
|
150
|
+
...(normalizedArgs || {}),
|
|
151
|
+
source: "codex",
|
|
152
|
+
limit: 8,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
name === "analyze_session" &&
|
|
158
|
+
(!normalizedArgs?.path || !normalizedArgs?.source) &&
|
|
159
|
+
recentScanHints.length > 0
|
|
160
|
+
) {
|
|
161
|
+
const hint = recentScanHints[Math.min(recentScanHintIndex, recentScanHints.length - 1)]
|
|
162
|
+
recentScanHintIndex += 1
|
|
163
|
+
normalizedArgs = {
|
|
164
|
+
...(normalizedArgs || {}),
|
|
165
|
+
path: hint?.path,
|
|
166
|
+
source: hint?.source,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Guard: preview_post requires title + content + mode. If the model
|
|
171
|
+
// calls it with empty/missing params, return an actionable error instead
|
|
172
|
+
// of letting MCP throw -32602 which derails the whole flow.
|
|
173
|
+
if (name === "preview_post") {
|
|
174
|
+
const a = normalizedArgs || {}
|
|
175
|
+
const missing: string[] = []
|
|
176
|
+
if (!a.title) missing.push("title")
|
|
177
|
+
if (!a.content) missing.push("content")
|
|
178
|
+
if (!a.mode) missing.push("mode")
|
|
179
|
+
if (missing.length > 0) {
|
|
180
|
+
const msg = `ERROR: preview_post requires these parameters: ${missing.join(", ")}. ` +
|
|
181
|
+
"You must provide title (string), content (markdown string, must NOT start with the title), " +
|
|
182
|
+
"and mode ('manual' or 'auto'). For daily reports also include category='day-in-code' and tags=['day-in-code']. " +
|
|
183
|
+
"Please call preview_post again with all required parameters."
|
|
184
|
+
log.warn("preview_post called with missing params", { missing, args: normalizedArgs })
|
|
185
|
+
return msg
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Guard: confirm_post requires post_id from preview_post result.
|
|
190
|
+
if (name === "confirm_post") {
|
|
191
|
+
const a = normalizedArgs || {}
|
|
192
|
+
if (!a.post_id && !a.postId && !a.id) {
|
|
193
|
+
const msg = "ERROR: confirm_post requires post_id (the ID returned by preview_post). " +
|
|
194
|
+
"Please call confirm_post with the post_id from the preview_post result."
|
|
195
|
+
log.warn("confirm_post called without post_id", { args: normalizedArgs })
|
|
196
|
+
return msg
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
log.info("execute tool", { name, args: normalizedArgs })
|
|
201
|
+
const result = await mcp(name, clean(normalizedArgs))
|
|
202
|
+
const summarizedScan = name === "scan_sessions" ? summarizeScanSessionsResult(result) : null
|
|
203
|
+
const resultStr = summarizedScan || (typeof result === "string" ? result : JSON.stringify(result))
|
|
204
|
+
if (name === "scan_sessions") {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = summarizedScan ? JSON.parse(summarizedScan) : null
|
|
207
|
+
const sessions = Array.isArray(parsed?.sessions) ? parsed.sessions : []
|
|
208
|
+
recentScanHints = sessions
|
|
209
|
+
.map((s: any) => ({ path: s?.path, source: s?.source }))
|
|
210
|
+
.filter((s: any) => typeof s.path === "string" && s.path && typeof s.source === "string" && s.source)
|
|
211
|
+
recentScanHintIndex = 0
|
|
212
|
+
} catch {
|
|
213
|
+
recentScanHints = []
|
|
214
|
+
recentScanHintIndex = 0
|
|
215
|
+
}
|
|
216
|
+
}
|
|
106
217
|
log.info("execute tool result", { name, resultType: typeof result, resultLength: resultStr.length, resultPreview: resultStr.slice(0, 300) })
|
|
107
218
|
// Truncate very large tool results to avoid overwhelming the LLM context
|
|
108
219
|
if (resultStr.length > 8000) {
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -440,6 +440,7 @@ export function Home(props: {
|
|
|
440
440
|
const CHECK_INTERVAL_MS = 30 * 60 * 1000 // 30 minutes
|
|
441
441
|
const DAILY_REPORT_MAX_ATTEMPTS = 3
|
|
442
442
|
const DAILY_REPORT_RETRY_COOLDOWN_MS = 60 * 60 * 1000 // 1 hour
|
|
443
|
+
const FORCE_DAILY_REGENERATE = process.env.CODEBLOG_DAILY_FORCE_REGENERATE === "1"
|
|
443
444
|
let dailyReportCompletedDate: string | null = null
|
|
444
445
|
const dailyReportAttempts = new Map<string, number>()
|
|
445
446
|
const dailyReportLastAttemptAt = new Map<string, number>()
|
|
@@ -479,6 +480,23 @@ export function Home(props: {
|
|
|
479
480
|
}
|
|
480
481
|
}
|
|
481
482
|
|
|
483
|
+
const parseCollectStatsResult = (
|
|
484
|
+
raw: string | undefined,
|
|
485
|
+
): { already_exists?: boolean; no_activity?: boolean } | null => {
|
|
486
|
+
if (!raw) return null
|
|
487
|
+
try {
|
|
488
|
+
const parsed = JSON.parse(raw)
|
|
489
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null
|
|
490
|
+
const data = parsed as { already_exists?: boolean; no_activity?: boolean }
|
|
491
|
+
return {
|
|
492
|
+
already_exists: !!data.already_exists,
|
|
493
|
+
no_activity: !!data.no_activity,
|
|
494
|
+
}
|
|
495
|
+
} catch {
|
|
496
|
+
return null
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
482
500
|
const checkDailyReport = async () => {
|
|
483
501
|
if (dailyReportCheckRunning) return
|
|
484
502
|
if (!props.hasAI || !props.loggedIn) return
|
|
@@ -496,10 +514,12 @@ export function Home(props: {
|
|
|
496
514
|
if (dailyReportCompletedDate === today) return
|
|
497
515
|
if (now.getHours() < reportHour) return
|
|
498
516
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
517
|
+
if (!FORCE_DAILY_REGENERATE) {
|
|
518
|
+
const currentStatus = await fetchDailyReportStatus(today)
|
|
519
|
+
if (currentStatus === "exists") {
|
|
520
|
+
dailyReportCompletedDate = today
|
|
521
|
+
return
|
|
522
|
+
}
|
|
503
523
|
}
|
|
504
524
|
|
|
505
525
|
const attempts = dailyReportAttempts.get(today) || 0
|
|
@@ -522,21 +542,61 @@ export function Home(props: {
|
|
|
522
542
|
)
|
|
523
543
|
|
|
524
544
|
// Use the same prompt as the /daily command
|
|
525
|
-
await send(
|
|
526
|
-
"Generate my 'Day in Code' daily report. " +
|
|
527
|
-
"
|
|
528
|
-
"
|
|
529
|
-
"
|
|
530
|
-
"
|
|
531
|
-
"
|
|
532
|
-
"
|
|
533
|
-
"
|
|
534
|
-
"
|
|
535
|
-
"
|
|
536
|
-
"
|
|
537
|
-
|
|
545
|
+
const runResult = await send(
|
|
546
|
+
"Generate my 'Day in Code' daily report. Follow these steps EXACTLY in order:\n\n" +
|
|
547
|
+
"STEP 1: Call collect_daily_stats to get today's coding activity data.\n" +
|
|
548
|
+
"- If it returns already_exists=true" + (FORCE_DAILY_REGENERATE ? " (force regenerate is enabled, so IGNORE already_exists and continue)" : ", STOP here — report already done") + ".\n" +
|
|
549
|
+
"- If it returns no_activity=true, STOP here — nothing to report.\n" +
|
|
550
|
+
"- Save the date, timezone, and _rawStats from the result — you need them for save_daily_report later.\n\n" +
|
|
551
|
+
"STEP 2: Call scan_sessions with source='codex' and limit=8.\n" +
|
|
552
|
+
"- From the results, pick the top 2-3 sessions by activity.\n\n" +
|
|
553
|
+
"STEP 3: Call analyze_session on each picked session. Pass BOTH path and source EXACTLY from scan_sessions results.\n\n" +
|
|
554
|
+
"STEP 4: Write the blog post content (DO NOT call any tool yet).\n" +
|
|
555
|
+
"- Write as the AI agent in first person — tell the story of your day collaborating with the user.\n" +
|
|
556
|
+
"- The narrative is the main content. Stats are supporting context woven into the story.\n" +
|
|
557
|
+
"- Use concise markdown tables in a data-summary section, but do not make the post only tables.\n" +
|
|
558
|
+
"- Do NOT include any source code or file paths.\n" +
|
|
559
|
+
"- Prepare a catchy title (string), the full content (markdown, must NOT start with the title), and a summary.\n\n" +
|
|
560
|
+
"STEP 5: Call preview_post with ALL of these parameters:\n" +
|
|
561
|
+
" - mode: 'manual'\n" +
|
|
562
|
+
" - title: your catchy title (string, REQUIRED)\n" +
|
|
563
|
+
" - content: your full markdown content (string, REQUIRED, must NOT start with the title)\n" +
|
|
564
|
+
" - category: 'day-in-code'\n" +
|
|
565
|
+
" - tags: ['day-in-code']\n" +
|
|
566
|
+
" CRITICAL: Do NOT call preview_post with empty or missing title/content — it will fail.\n\n" +
|
|
567
|
+
"STEP 6: Call confirm_post with the post_id returned by preview_post. This is scheduled auto mode — do NOT wait for user confirmation.\n\n" +
|
|
568
|
+
"STEP 7: Call save_daily_report with:\n" +
|
|
569
|
+
" - date and timezone from collect_daily_stats result\n" +
|
|
570
|
+
" - post_id from confirm_post result\n" +
|
|
571
|
+
" - _rawStats from collect_daily_stats result\n\n" +
|
|
572
|
+
"FORBIDDEN: Do NOT call post_to_codeblog or auto_post. Only use preview_post + confirm_post.\n" +
|
|
573
|
+
(FORCE_DAILY_REGENERATE
|
|
574
|
+
? "FORCE MODE: When calling collect_daily_stats, set force=true.\n"
|
|
575
|
+
: "") +
|
|
576
|
+
"This scheduled task MUST complete all 7 steps in this single run.",
|
|
577
|
+
{
|
|
578
|
+
display: "Auto-generating daily report (Day in Code)",
|
|
579
|
+
maxSteps: 16,
|
|
580
|
+
idleTimeoutMs: 240_000,
|
|
581
|
+
toolTimeoutMs: 60_000,
|
|
582
|
+
freshContext: true,
|
|
583
|
+
},
|
|
538
584
|
)
|
|
539
585
|
|
|
586
|
+
const collectStats = parseCollectStatsResult(
|
|
587
|
+
runResult?.toolResults.find((t) => t.name === "collect_daily_stats")?.result,
|
|
588
|
+
)
|
|
589
|
+
if (collectStats?.already_exists && !FORCE_DAILY_REGENERATE) {
|
|
590
|
+
dailyReportCompletedDate = today
|
|
591
|
+
showMsg("Today's Day in Code report already exists.", theme.colors.success)
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
if (collectStats?.no_activity) {
|
|
595
|
+
dailyReportCompletedDate = today
|
|
596
|
+
showMsg("No coding activity detected today. Skipping daily report auto-run.", theme.colors.warning)
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
|
|
540
600
|
const afterStatus = await fetchDailyReportStatus(today)
|
|
541
601
|
if (afterStatus === "exists") {
|
|
542
602
|
dailyReportCompletedDate = today
|
|
@@ -677,7 +737,10 @@ export function Home(props: {
|
|
|
677
737
|
setInput((s) => s + text)
|
|
678
738
|
})
|
|
679
739
|
|
|
680
|
-
async function send(
|
|
740
|
+
async function send(
|
|
741
|
+
text: string,
|
|
742
|
+
options?: { display?: string; maxSteps?: number; idleTimeoutMs?: number; toolTimeoutMs?: number; freshContext?: boolean },
|
|
743
|
+
): Promise<{ toolResults: Array<{ name: string; result: string }>; assistantContent?: string } | undefined> {
|
|
681
744
|
if (!text.trim() || streaming()) return
|
|
682
745
|
ensureSession()
|
|
683
746
|
const prompt = text.trim()
|
|
@@ -689,6 +752,8 @@ export function Home(props: {
|
|
|
689
752
|
abortByUser = false
|
|
690
753
|
const assembler = new TuiStreamAssembler()
|
|
691
754
|
const toolResults: Array<{ name: string; result: string }> = []
|
|
755
|
+
let assistantContent: string | undefined
|
|
756
|
+
let streamErrorHandled = false
|
|
692
757
|
const flushMs = 60
|
|
693
758
|
let flushTimer: ReturnType<typeof setTimeout> | undefined
|
|
694
759
|
|
|
@@ -696,7 +761,8 @@ export function Home(props: {
|
|
|
696
761
|
if (force) {
|
|
697
762
|
if (flushTimer) clearTimeout(flushTimer)
|
|
698
763
|
flushTimer = undefined
|
|
699
|
-
|
|
764
|
+
const nextText = assembler.getText()
|
|
765
|
+
queueMicrotask(() => setStreamText(nextText))
|
|
700
766
|
return
|
|
701
767
|
}
|
|
702
768
|
if (flushTimer) return
|
|
@@ -713,14 +779,19 @@ export function Home(props: {
|
|
|
713
779
|
const { resolveModelFromConfig } = await import("../../ai/models")
|
|
714
780
|
const cfg = await Config.load()
|
|
715
781
|
const mid = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
|
|
716
|
-
const
|
|
782
|
+
const modelInput = options?.freshContext ? [userMsg] : [...prev, userMsg]
|
|
783
|
+
const allMsgs = modelInput
|
|
717
784
|
.filter((m): m is ChatMsg & { role: "user" | "assistant" } => m.role === "user" || m.role === "assistant")
|
|
718
785
|
.map((m) => ({ role: m.role, content: m.modelContent || m.content }))
|
|
719
786
|
abortCtrl = new AbortController()
|
|
720
787
|
|
|
721
788
|
let hasToolCalls = false
|
|
722
789
|
let finalizedText = ""
|
|
723
|
-
for await (const event of AIChat.streamEvents(allMsgs, mid, abortCtrl.signal, {
|
|
790
|
+
for await (const event of AIChat.streamEvents(allMsgs, mid, abortCtrl.signal, {
|
|
791
|
+
maxSteps: options?.maxSteps ?? 12,
|
|
792
|
+
idleTimeoutMs: options?.idleTimeoutMs,
|
|
793
|
+
toolTimeoutMs: options?.toolTimeoutMs,
|
|
794
|
+
})) {
|
|
724
795
|
if (event.type === "text-delta") {
|
|
725
796
|
assembler.pushDelta(event.text, event.seq)
|
|
726
797
|
flushStream()
|
|
@@ -731,11 +802,14 @@ export function Home(props: {
|
|
|
731
802
|
hasToolCalls = true
|
|
732
803
|
const partial = assembler.getText().trim()
|
|
733
804
|
if (partial) {
|
|
734
|
-
|
|
805
|
+
queueMicrotask(() => {
|
|
806
|
+
setMessages((p) => [...p, { role: "assistant", content: partial }])
|
|
807
|
+
})
|
|
735
808
|
assembler.reset()
|
|
736
809
|
setStreamText("")
|
|
737
810
|
}
|
|
738
|
-
|
|
811
|
+
const toolMsg: ChatMsg = { role: "tool", content: TOOL_LABELS[event.name] || event.name, toolName: event.name, toolCallID: event.callID, toolStatus: "running" }
|
|
812
|
+
queueMicrotask(() => setMessages((p) => [...p, toolMsg]))
|
|
739
813
|
continue
|
|
740
814
|
}
|
|
741
815
|
|
|
@@ -744,32 +818,40 @@ export function Home(props: {
|
|
|
744
818
|
const str = typeof event.result === "string" ? event.result : JSON.stringify(event.result)
|
|
745
819
|
toolResults.push({ name: event.name, result: str.slice(0, 1200) })
|
|
746
820
|
} catch {}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
821
|
+
const evtCallID = event.callID
|
|
822
|
+
const evtName = event.name
|
|
823
|
+
queueMicrotask(() => {
|
|
824
|
+
setMessages((p) => {
|
|
825
|
+
let matched = false
|
|
826
|
+
const next = p.map((m) => {
|
|
827
|
+
if (m.role !== "tool" || m.toolStatus !== "running") return m
|
|
828
|
+
if (m.toolCallID !== evtCallID) return m
|
|
829
|
+
matched = true
|
|
830
|
+
return { ...m, toolStatus: "done" as const }
|
|
831
|
+
})
|
|
832
|
+
if (matched) return next
|
|
833
|
+
return p.map((m) =>
|
|
834
|
+
m.role === "tool" && m.toolName === evtName && m.toolStatus === "running"
|
|
835
|
+
? { ...m, toolStatus: "done" as const }
|
|
836
|
+
: m
|
|
837
|
+
)
|
|
754
838
|
})
|
|
755
|
-
if (matched) return next
|
|
756
|
-
return p.map((m) =>
|
|
757
|
-
m.role === "tool" && m.toolName === event.name && m.toolStatus === "running"
|
|
758
|
-
? { ...m, toolStatus: "done" as const }
|
|
759
|
-
: m
|
|
760
|
-
)
|
|
761
839
|
})
|
|
762
840
|
continue
|
|
763
841
|
}
|
|
764
842
|
|
|
765
843
|
if (event.type === "error") {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
844
|
+
if (streamErrorHandled) continue
|
|
845
|
+
streamErrorHandled = true
|
|
846
|
+
queueMicrotask(() => {
|
|
847
|
+
setMessages((p) => {
|
|
848
|
+
const updated = p.map((m) =>
|
|
849
|
+
m.role === "tool" && m.toolStatus === "running"
|
|
850
|
+
? { ...m, toolStatus: "error" as const }
|
|
851
|
+
: m
|
|
852
|
+
)
|
|
853
|
+
return [...updated, { role: "assistant" as const, content: `Error: ${event.error.message}` }]
|
|
854
|
+
})
|
|
773
855
|
})
|
|
774
856
|
continue
|
|
775
857
|
}
|
|
@@ -784,7 +866,10 @@ export function Home(props: {
|
|
|
784
866
|
hasToolCalls,
|
|
785
867
|
toolResults,
|
|
786
868
|
})
|
|
787
|
-
if (content)
|
|
869
|
+
if (content) {
|
|
870
|
+
assistantContent = content
|
|
871
|
+
queueMicrotask(() => setMessages((p) => [...p, { role: "assistant", content }]))
|
|
872
|
+
}
|
|
788
873
|
}
|
|
789
874
|
}
|
|
790
875
|
} catch (err) {
|
|
@@ -797,6 +882,7 @@ export function Home(props: {
|
|
|
797
882
|
setStreaming(false)
|
|
798
883
|
saveChat()
|
|
799
884
|
}
|
|
885
|
+
return { toolResults, assistantContent }
|
|
800
886
|
}
|
|
801
887
|
|
|
802
888
|
const aiProviderFiltered = createMemo(() => {
|