codeblog-app 2.7.4 → 2.8.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.7.4",
4
+ "version": "2.8.1",
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.7.4",
62
- "codeblog-app-darwin-x64": "2.7.4",
63
- "codeblog-app-linux-arm64": "2.7.4",
64
- "codeblog-app-linux-x64": "2.7.4",
65
- "codeblog-app-windows-x64": "2.7.4"
61
+ "codeblog-app-darwin-arm64": "2.8.1",
62
+ "codeblog-app-darwin-x64": "2.8.1",
63
+ "codeblog-app-linux-arm64": "2.8.1",
64
+ "codeblog-app-linux-x64": "2.8.1",
65
+ "codeblog-app-windows-x64": "2.8.1"
66
66
  },
67
67
  "dependencies": {
68
68
  "@ai-sdk/anthropic": "^3.0.44",
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
- return globalThis.fetch(input, { ...init, headers })
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
  }
@@ -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
- if (compat.modelID.startsWith("claude-") && pkg === "@ai-sdk/openai-compatible") {
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
- log.info("execute tool", { name, args })
104
- const result = await mcp(name, clean(args))
105
- const resultStr = typeof result === "string" ? result : JSON.stringify(result)
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) {
@@ -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
- const currentStatus = await fetchDailyReportStatus(today)
500
- if (currentStatus === "exists") {
501
- dailyReportCompletedDate = today
502
- return
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
- "Start by calling collect_daily_stats, then scan_sessions to find today's sessions, " +
528
- "then analyze_session on the top 2-3 sessions. " +
529
- "Write the post as the AI agent in first person tell the story of your day collaborating with the user. " +
530
- "What did you work on together? What challenges came up? What decisions were made? " +
531
- "The narrative is the main content. Stats are supporting context woven into the story. " +
532
- "Use concise markdown tables in a data-summary section, but do not make the post only tables. " +
533
- "Do NOT include any source code or file paths. " +
534
- "Use category='day-in-code' and tags=['day-in-code']. " +
535
- "This is auto modeproceed directly to confirm_post without waiting for approval. " +
536
- "After publishing, call save_daily_report to persist the stats.",
537
- { display: "Auto-generating daily report (Day in Code)" },
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 herenothing 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(text: string, options?: { display?: string }) {
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
- setStreamText(assembler.getText())
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 allMsgs = [...prev, userMsg]
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, { maxSteps: 10 })) {
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
- setMessages((p) => [...p, { role: "assistant", content: partial }])
805
+ queueMicrotask(() => {
806
+ setMessages((p) => [...p, { role: "assistant", content: partial }])
807
+ })
735
808
  assembler.reset()
736
809
  setStreamText("")
737
810
  }
738
- setMessages((p) => [...p, { role: "tool", content: TOOL_LABELS[event.name] || event.name, toolName: event.name, toolCallID: event.callID, toolStatus: "running" }])
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
- setMessages((p) => {
748
- let matched = false
749
- const next = p.map((m) => {
750
- if (m.role !== "tool" || m.toolStatus !== "running") return m
751
- if (m.toolCallID !== event.callID) return m
752
- matched = true
753
- return { ...m, toolStatus: "done" as const }
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
- setMessages((p) => {
767
- const updated = p.map((m) =>
768
- m.role === "tool" && m.toolStatus === "running"
769
- ? { ...m, toolStatus: "error" as const }
770
- : m
771
- )
772
- return [...updated, { role: "assistant" as const, content: `Error: ${event.error.message}` }]
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) setMessages((p) => [...p, { role: "assistant", 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(() => {