mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
Merge pull request #11433 from Kilo-Org/candy-mouth
fix(agent-manager): stabilize concurrent subagent rendering
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Prevent concurrent subagent updates from blanking the Agent Manager webview.
|
||||
@@ -354,6 +354,7 @@
|
||||
"esbuild-plugin-solid": "^0.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"happy-dom": "20.8.9",
|
||||
"knip": "5.85.0",
|
||||
"prettier": "3.6.2",
|
||||
"qrcode": "^1.5.4",
|
||||
@@ -2204,6 +2205,8 @@
|
||||
|
||||
"@types/vscode": ["@types/vscode@1.120.0", "", {}, "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA=="],
|
||||
|
||||
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
||||
|
||||
"@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
@@ -2486,6 +2489,8 @@
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
|
||||
|
||||
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
|
||||
@@ -3076,6 +3081,8 @@
|
||||
|
||||
"hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||
|
||||
"happy-dom": ["happy-dom@20.10.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
@@ -4270,7 +4277,7 @@
|
||||
|
||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
@@ -4630,6 +4637,8 @@
|
||||
|
||||
"cheerio/undici": ["undici@7.27.2", "", {}, "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA=="],
|
||||
|
||||
"cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||
|
||||
"clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
@@ -1100,6 +1100,7 @@
|
||||
"esbuild-plugin-solid": "^0.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"happy-dom": "20.8.9",
|
||||
"knip": "5.85.0",
|
||||
"prettier": "3.6.2",
|
||||
"qrcode": "^1.5.4",
|
||||
|
||||
@@ -49,6 +49,66 @@ test.describe("webview accessibility ratchet", () => {
|
||||
})
|
||||
}
|
||||
|
||||
test("Agent Manager keeps virtualized transcript fragments laid out", async ({ page }) => {
|
||||
await open(page, "agentmanager--sidebar-search-open")
|
||||
|
||||
const visibility = await page.locator("#storybook-root").evaluate((root) => {
|
||||
const layout = document.createElement("div")
|
||||
layout.className = "am-layout"
|
||||
root.append(layout)
|
||||
const names = ["assistant-message", "tool-trigger", "file", "code", "diff"]
|
||||
const values = names.map((name) => {
|
||||
const node = document.createElement("div")
|
||||
node.dataset.component = name
|
||||
layout.append(node)
|
||||
return getComputedStyle(node).contentVisibility
|
||||
})
|
||||
layout.remove()
|
||||
return values
|
||||
})
|
||||
|
||||
expect(visibility).toEqual(["visible", "visible", "visible", "visible", "visible"])
|
||||
})
|
||||
|
||||
test("Agent Manager avoids generated separator text in updating tool rows", async ({ page }) => {
|
||||
await open(page, "agentmanager--sidebar-search-open")
|
||||
|
||||
const content = await page.locator("#storybook-root").evaluate((root) => {
|
||||
const layout = document.createElement("div")
|
||||
layout.className = "am-layout"
|
||||
const wrapper = document.createElement("div")
|
||||
wrapper.dataset.component = "tool-part-wrapper"
|
||||
wrapper.dataset.partType = "tool"
|
||||
const collapsible = document.createElement("div")
|
||||
collapsible.className = "tool-collapsible"
|
||||
collapsible.dataset.component = "collapsible"
|
||||
const title = document.createElement("span")
|
||||
title.dataset.slot = "basic-tool-tool-title"
|
||||
const subtitle = document.createElement("span")
|
||||
subtitle.dataset.slot = "basic-tool-tool-subtitle"
|
||||
collapsible.append(title, subtitle)
|
||||
wrapper.append(collapsible)
|
||||
layout.append(wrapper)
|
||||
root.append(layout)
|
||||
const value = getComputedStyle(subtitle, "::before").content
|
||||
layout.remove()
|
||||
return value
|
||||
})
|
||||
|
||||
expect(content).toBe("none")
|
||||
})
|
||||
|
||||
test("sidebar keeps transcript announcements while Agent Manager bounds them", async ({ page }) => {
|
||||
await open(page, "chat--chat-view-with-messages")
|
||||
await expect(page.locator(".message-list")).toHaveAttribute("role", "log")
|
||||
await expect(page.locator(".message-list")).toHaveAttribute("aria-live", "polite")
|
||||
|
||||
await open(page, "chat--chat-view-agent-manager-completed")
|
||||
await expect(page.locator(".message-list")).not.toHaveAttribute("role")
|
||||
await expect(page.locator(".message-list")).not.toHaveAttribute("aria-live")
|
||||
await expect(page.locator('.sr-only[role="status"]')).toHaveAttribute("aria-live", "polite")
|
||||
})
|
||||
|
||||
test("Profile login exposes a keyboard-operable named control", async ({ page }) => {
|
||||
await open(page, "profile--not-logged-in")
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import path from "node:path"
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, "../..")
|
||||
|
||||
describe("task tool index identity", () => {
|
||||
it("preserves a rendered child tool row across status updates", () => {
|
||||
const result = Bun.spawnSync(
|
||||
[
|
||||
"bun",
|
||||
"--conditions=browser",
|
||||
"-e",
|
||||
`
|
||||
import { Window } from "happy-dom"
|
||||
|
||||
const window = new Window()
|
||||
globalThis.window = window
|
||||
globalThis.document = window.document
|
||||
globalThis.Node = window.Node
|
||||
|
||||
const { Index, createComponent, createRenderEffect } = await import("solid-js")
|
||||
const { createStore } = await import("solid-js/store")
|
||||
const { render } = await import("solid-js/web")
|
||||
const { reconcileSessionToolParts, upsertSessionToolPart } = await import(
|
||||
"./webview-ui/src/context/session-utils.ts"
|
||||
)
|
||||
|
||||
const running = {
|
||||
id: "tool-1",
|
||||
type: "tool",
|
||||
tool: "read",
|
||||
state: { status: "running", input: { filePath: "src/app.ts" }, title: "Reading" },
|
||||
}
|
||||
const completed = {
|
||||
id: "tool-1",
|
||||
type: "tool",
|
||||
tool: "read",
|
||||
state: { status: "completed", input: { filePath: "src/app.ts" }, title: "Read", output: "done" },
|
||||
}
|
||||
const root = document.createElement("div")
|
||||
const [store, setStore] = createStore({ tools: [running] })
|
||||
const dispose = render(
|
||||
() =>
|
||||
createComponent(Index, {
|
||||
get each() {
|
||||
return store.tools
|
||||
},
|
||||
children: (item) => {
|
||||
const row = document.createElement("div")
|
||||
createRenderEffect(() => {
|
||||
row.textContent = item().state.status
|
||||
})
|
||||
return row
|
||||
},
|
||||
}),
|
||||
root,
|
||||
)
|
||||
const row = root.firstElementChild
|
||||
const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" })
|
||||
setStore("tools", reconcileSessionToolParts(tools))
|
||||
if (root.firstElementChild !== row) throw new Error("rendered tool row was replaced")
|
||||
if (row?.textContent !== "completed") throw new Error("rendered tool row was not updated")
|
||||
dispose()
|
||||
`,
|
||||
],
|
||||
{ cwd: ROOT, stdout: "pipe", stderr: "pipe" },
|
||||
)
|
||||
|
||||
const output = result.stdout.toString() + result.stderr.toString()
|
||||
expect(result.exitCode, output).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -7,6 +7,47 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Virtua already removes off-screen transcript rows. Avoid a second layer of
|
||||
layout skipping, which can leave Chromium accessibility traversing fragments
|
||||
whose layout items were discarded while Agent Manager scrolls. */
|
||||
.am-layout
|
||||
:is(
|
||||
[data-component="assistant-message"],
|
||||
[data-component="tool-trigger"],
|
||||
[data-component="file"],
|
||||
[data-component="code"],
|
||||
[data-component="diff"]
|
||||
) {
|
||||
content-visibility: visible;
|
||||
}
|
||||
|
||||
/* Generated separators become accessibility text nodes. Keep them out of
|
||||
concurrently updating Agent Manager tool rows while preserving sidebar UI. */
|
||||
html[data-theme="kilo-vscode"]
|
||||
.am-layout
|
||||
[data-component="tool-part-wrapper"][data-part-type="tool"]
|
||||
[data-component="collapsible"]
|
||||
[data-slot="basic-tool-tool-title"]
|
||||
+ :is(
|
||||
[data-slot="basic-tool-tool-subtitle"],
|
||||
[data-slot="basic-tool-tool-arg"],
|
||||
[data-slot="message-part-meta-line"],
|
||||
[data-slot="webfetch-meta"],
|
||||
[data-component="shell-submessage"]
|
||||
)::before,
|
||||
html[data-theme="kilo-vscode"]
|
||||
.am-layout
|
||||
[data-component="tool-part-wrapper"][data-part-type="tool"]
|
||||
[data-component="collapsible"]
|
||||
[data-slot="message-part-title-text"]
|
||||
+ :is([data-slot="message-part-meta-line"], [data-slot="basic-tool-tool-subtitle"])::before,
|
||||
html[data-theme="kilo-vscode"]
|
||||
.am-layout
|
||||
[data-component="tool-part-wrapper"][data-part-type="tool"]
|
||||
[data-slot="context-tool-group-summary"]::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.am-sidebar {
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
|
||||
@@ -332,6 +332,7 @@ export const ChatView: Component<ChatViewProps> = (props) => {
|
||||
suggestions={standaloneSuggestions}
|
||||
readonly={props.readonly}
|
||||
emptyState={props.emptyState}
|
||||
announce={isSidebar()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,8 @@ interface MessageListProps {
|
||||
readonly?: boolean
|
||||
/** Optionally replace the standard welcome content while the conversation is empty. */
|
||||
emptyState?: () => JSX.Element
|
||||
/** Announce transcript changes as a live log. Disable for multi-session surfaces with concurrent streams. */
|
||||
announce?: boolean
|
||||
}
|
||||
|
||||
export const MessageList: Component<MessageListProps> = (props) => {
|
||||
@@ -73,6 +75,19 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
const autoScroll = createAutoScroll({
|
||||
working: () => session.status() !== "idle",
|
||||
})
|
||||
const [announcement, setAnnouncement] = createSignal("")
|
||||
createEffect(
|
||||
(prev: { sid?: string; working: boolean }) => {
|
||||
const sid = session.currentSessionID()
|
||||
const working = session.status() !== "idle"
|
||||
if (working && (!prev.working || prev.sid !== sid)) setAnnouncement(language.t("session.status.working"))
|
||||
if (!working && prev.working && prev.sid === sid) {
|
||||
setAnnouncement(language.t("settings.agentBehaviour.editMode.save"))
|
||||
}
|
||||
return { sid, working }
|
||||
},
|
||||
{ sid: undefined, working: false },
|
||||
)
|
||||
|
||||
// Explicit output-producing actions resume auto-scroll before appending.
|
||||
const onResumeAutoScroll = () => autoScroll.resume()
|
||||
@@ -260,13 +275,25 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div class="message-list-container">
|
||||
<Show when={props.announce === false}>
|
||||
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
||||
{announcement()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={isEmpty()}>
|
||||
<div class="welcome-header">
|
||||
<AccountSwitcher class="account-switcher-welcome" />
|
||||
<KiloNotifications />
|
||||
</div>
|
||||
</Show>
|
||||
<div ref={setScrollRef} onScroll={handleScroll} class="message-list" role="log" aria-live="polite">
|
||||
<div
|
||||
ref={setScrollRef}
|
||||
onScroll={handleScroll}
|
||||
class="message-list"
|
||||
role={props.announce === false ? undefined : "log"}
|
||||
aria-live={props.announce === false ? undefined : "polite"}
|
||||
aria-busy={props.announce === false && session.status() !== "idle" ? "true" : undefined}
|
||||
>
|
||||
<div ref={autoScroll.contentRef} class={isEmpty() ? "message-list-content-empty" : "message-list-content"}>
|
||||
<Show when={session.loading()}>
|
||||
<div class="message-list-loading" role="status">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Call registerExpandedTaskTool() once at app startup to activate.
|
||||
*/
|
||||
|
||||
import { Component, createEffect, createMemo, createSignal, For, Show, onCleanup } from "solid-js"
|
||||
import { Component, createEffect, createMemo, createSignal, Index, Show, onCleanup } from "solid-js"
|
||||
import { ToolRegistry, ToolProps, getToolInfo } from "@kilocode/kilo-ui/message-part"
|
||||
import { BasicTool, initialOpen } from "@kilocode/kilo-ui/basic-tool"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
@@ -141,15 +141,13 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={result()}>{(text) => <Markdown text={text()} />}</Show>
|
||||
<For each={childToolParts()}>
|
||||
<Index each={childToolParts()}>
|
||||
{(item) => {
|
||||
const info = createMemo(() => getToolInfo(item.tool, item.state?.input))
|
||||
const info = createMemo(() => getToolInfo(item().tool, item().state?.input))
|
||||
const subtitle = createMemo(() => {
|
||||
if (info().subtitle) return info().subtitle
|
||||
const state = item.state as { status: string; title?: string }
|
||||
if (state.status === "completed" || state.status === "running") {
|
||||
return state.title
|
||||
}
|
||||
const state = item().state as { status: string; title?: string }
|
||||
if (state.status === "completed" || state.status === "running") return state.title
|
||||
return undefined
|
||||
})
|
||||
return (
|
||||
@@ -162,7 +160,7 @@ const TaskToolRenderer: Component<ToolProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Index>
|
||||
</div>
|
||||
</div>
|
||||
</BasicTool>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { reconcile } from "solid-js/store"
|
||||
import type { Message, Part, ToolPart } from "../types/messages"
|
||||
|
||||
export const SNAPSHOT_PROGRESS_TEXT = "Initializing snapshot..."
|
||||
@@ -85,6 +86,10 @@ export function buildSessionToolParts(
|
||||
return tools
|
||||
}
|
||||
|
||||
export function reconcileSessionToolParts(tools: ToolPart[]) {
|
||||
return reconcile(tools, { key: "id" })
|
||||
}
|
||||
|
||||
export function upsertSessionToolPart(
|
||||
current: ToolPart[],
|
||||
part: Part,
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
buildCostBreakdown,
|
||||
buildSessionToolParts,
|
||||
childID,
|
||||
reconcileSessionToolParts,
|
||||
removeSessionToolPart,
|
||||
removeSessionToolPartsForMessage,
|
||||
upsertSessionToolPart,
|
||||
@@ -1290,12 +1291,16 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
return true
|
||||
}
|
||||
|
||||
function setTools(sessionID: string, tools: ToolPart[]) {
|
||||
setStore("toolParts", sessionID, reconcileSessionToolParts(tools))
|
||||
}
|
||||
|
||||
function rebuildToolParts(sessionID: string, messages: Message[], parts?: Record<string, Part[]>) {
|
||||
const tools = buildSessionToolParts(
|
||||
messages,
|
||||
(msg) => parts?.[msg.id] ?? store.parts[msg.id] ?? stash.peek(msg.id) ?? msg.parts,
|
||||
)
|
||||
setStore("toolParts", sessionID, tools)
|
||||
setTools(sessionID, tools)
|
||||
}
|
||||
|
||||
function messageParts(messages: Message[]): Record<string, Part[]> {
|
||||
@@ -1310,16 +1315,17 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
const sid = sessionID ?? part.sessionID
|
||||
if (!sid) return
|
||||
if (part.type !== "tool") return
|
||||
setStore("toolParts", sid, (tools = []) => upsertSessionToolPart(tools, part, { id: messageID, sessionID: sid }))
|
||||
const tools = upsertSessionToolPart(store.toolParts[sid] ?? [], part, { id: messageID, sessionID: sid })
|
||||
setTools(sid, tools)
|
||||
}
|
||||
|
||||
function dropToolPart(sessionID: string | undefined, partID: string) {
|
||||
if (!sessionID) return
|
||||
setStore("toolParts", sessionID, (tools = []) => removeSessionToolPart(tools, partID))
|
||||
setTools(sessionID, removeSessionToolPart(store.toolParts[sessionID] ?? [], partID))
|
||||
}
|
||||
|
||||
function dropMessageTools(sessionID: string, messageID: string) {
|
||||
setStore("toolParts", sessionID, (tools = []) => removeSessionToolPartsForMessage(tools, messageID))
|
||||
setTools(sessionID, removeSessionToolPartsForMessage(store.toolParts[sessionID] ?? [], messageID))
|
||||
}
|
||||
|
||||
function handleMessagesLoaded(
|
||||
|
||||
Reference in New Issue
Block a user