fix(app): better optimistic prompt submit (#17337)
This commit is contained in:
@@ -7,12 +7,16 @@ const createdClients: string[] = []
|
|||||||
const createdSessions: string[] = []
|
const createdSessions: string[] = []
|
||||||
const enabledAutoAccept: Array<{ sessionID: string; directory: string }> = []
|
const enabledAutoAccept: Array<{ sessionID: string; directory: string }> = []
|
||||||
const optimistic: Array<{
|
const optimistic: Array<{
|
||||||
|
directory?: string
|
||||||
|
sessionID?: string
|
||||||
message: {
|
message: {
|
||||||
agent: string
|
agent: string
|
||||||
model: { providerID: string; modelID: string }
|
model: { providerID: string; modelID: string }
|
||||||
variant?: string
|
variant?: string
|
||||||
}
|
}
|
||||||
}> = []
|
}> = []
|
||||||
|
const optimisticSeeded: boolean[] = []
|
||||||
|
const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
|
||||||
const sentShell: string[] = []
|
const sentShell: string[] = []
|
||||||
const syncedDirectories: string[] = []
|
const syncedDirectories: string[] = []
|
||||||
|
|
||||||
@@ -28,7 +32,12 @@ const clientFor = (directory: string) => {
|
|||||||
session: {
|
session: {
|
||||||
create: async () => {
|
create: async () => {
|
||||||
createdSessions.push(directory)
|
createdSessions.push(directory)
|
||||||
return { data: { id: `session-${createdSessions.length}` } }
|
return {
|
||||||
|
data: {
|
||||||
|
id: `session-${createdSessions.length}`,
|
||||||
|
title: `New session ${createdSessions.length}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
shell: async () => {
|
shell: async () => {
|
||||||
sentShell.push(directory)
|
sentShell.push(directory)
|
||||||
@@ -129,9 +138,16 @@ beforeAll(async () => {
|
|||||||
session: {
|
session: {
|
||||||
optimistic: {
|
optimistic: {
|
||||||
add: (value: {
|
add: (value: {
|
||||||
|
directory?: string
|
||||||
|
sessionID?: string
|
||||||
message: { agent: string; model: { providerID: string; modelID: string }; variant?: string }
|
message: { agent: string; model: { providerID: string; modelID: string }; variant?: string }
|
||||||
}) => {
|
}) => {
|
||||||
optimistic.push(value)
|
optimistic.push(value)
|
||||||
|
optimisticSeeded.push(
|
||||||
|
!!value.directory &&
|
||||||
|
!!value.sessionID &&
|
||||||
|
!!storedSessions[value.directory]?.find((item) => item.id === value.sessionID)?.title,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
remove: () => undefined,
|
remove: () => undefined,
|
||||||
},
|
},
|
||||||
@@ -144,7 +160,21 @@ beforeAll(async () => {
|
|||||||
useGlobalSync: () => ({
|
useGlobalSync: () => ({
|
||||||
child: (directory: string) => {
|
child: (directory: string) => {
|
||||||
syncedDirectories.push(directory)
|
syncedDirectories.push(directory)
|
||||||
return [{}, () => undefined]
|
storedSessions[directory] ??= []
|
||||||
|
return [
|
||||||
|
{ session: storedSessions[directory] },
|
||||||
|
(...args: unknown[]) => {
|
||||||
|
if (args[0] !== "session") return
|
||||||
|
const next = args[1]
|
||||||
|
if (typeof next === "function") {
|
||||||
|
storedSessions[directory] = next(storedSessions[directory]) as Array<{ id: string; title?: string }>
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Array.isArray(next)) {
|
||||||
|
storedSessions[directory] = next as Array<{ id: string; title?: string }>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
@@ -170,11 +200,13 @@ beforeEach(() => {
|
|||||||
createdSessions.length = 0
|
createdSessions.length = 0
|
||||||
enabledAutoAccept.length = 0
|
enabledAutoAccept.length = 0
|
||||||
optimistic.length = 0
|
optimistic.length = 0
|
||||||
|
optimisticSeeded.length = 0
|
||||||
params = {}
|
params = {}
|
||||||
sentShell.length = 0
|
sentShell.length = 0
|
||||||
syncedDirectories.length = 0
|
syncedDirectories.length = 0
|
||||||
selected = "/repo/worktree-a"
|
selected = "/repo/worktree-a"
|
||||||
variant = undefined
|
variant = undefined
|
||||||
|
for (const key of Object.keys(storedSessions)) delete storedSessions[key]
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("prompt submit worktree selection", () => {
|
describe("prompt submit worktree selection", () => {
|
||||||
@@ -207,7 +239,7 @@ describe("prompt submit worktree selection", () => {
|
|||||||
expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
||||||
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
|
expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("applies auto-accept to newly created sessions", async () => {
|
test("applies auto-accept to newly created sessions", async () => {
|
||||||
@@ -271,4 +303,32 @@ describe("prompt submit worktree selection", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("seeds new sessions before optimistic prompts are added", async () => {
|
||||||
|
const submit = createPromptSubmit({
|
||||||
|
info: () => undefined,
|
||||||
|
imageAttachments: () => [],
|
||||||
|
commentCount: () => 0,
|
||||||
|
autoAccept: () => false,
|
||||||
|
mode: () => "normal",
|
||||||
|
working: () => false,
|
||||||
|
editor: () => undefined,
|
||||||
|
queueScroll: () => undefined,
|
||||||
|
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
|
||||||
|
addToHistory: () => undefined,
|
||||||
|
resetHistoryNavigation: () => undefined,
|
||||||
|
setMode: () => undefined,
|
||||||
|
setPopover: () => undefined,
|
||||||
|
newSessionWorktree: () => selected,
|
||||||
|
onNewSessionWorktreeReset: () => undefined,
|
||||||
|
onSubmit: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const event = { preventDefault: () => undefined } as unknown as Event
|
||||||
|
|
||||||
|
await submit.handleSubmit(event)
|
||||||
|
|
||||||
|
expect(storedSessions["/repo/worktree-a"]).toEqual([{ id: "session-1", title: "New session 1" }])
|
||||||
|
expect(optimisticSeeded).toEqual([true])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Message } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import type { Accessor } from "solid-js"
|
import type { Accessor } from "solid-js"
|
||||||
import type { FileSelection } from "@/context/file"
|
import type { FileSelection } from "@/context/file"
|
||||||
@@ -266,6 +267,20 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seed = (dir: string, info: Session) => {
|
||||||
|
const [, setStore] = globalSync.child(dir)
|
||||||
|
setStore("session", (list: Session[]) => {
|
||||||
|
const result = Binary.search(list, info.id, (item) => item.id)
|
||||||
|
const next = [...list]
|
||||||
|
if (result.found) {
|
||||||
|
next[result.index] = info
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
next.splice(result.index, 0, info)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
@@ -341,7 +356,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
|
|
||||||
let session = input.info()
|
let session = input.info()
|
||||||
if (!session && isNewSession) {
|
if (!session && isNewSession) {
|
||||||
session = await client.session
|
const created = await client.session
|
||||||
.create()
|
.create()
|
||||||
.then((x) => x.data ?? undefined)
|
.then((x) => x.data ?? undefined)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -351,7 +366,9 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||||||
})
|
})
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
if (session) {
|
if (created) {
|
||||||
|
seed(sessionDirectory, created)
|
||||||
|
session = created
|
||||||
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
||||||
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
||||||
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||||
import { applyOptimisticAdd, applyOptimisticRemove } from "./sync"
|
import { applyOptimisticAdd, applyOptimisticRemove, mergeOptimisticPage } from "./sync"
|
||||||
|
|
||||||
|
type Text = Extract<Part, { type: "text" }>
|
||||||
|
|
||||||
const userMessage = (id: string, sessionID: string): Message => ({
|
const userMessage = (id: string, sessionID: string): Message => ({
|
||||||
id,
|
id,
|
||||||
@@ -11,7 +13,7 @@ const userMessage = (id: string, sessionID: string): Message => ({
|
|||||||
model: { providerID: "openai", modelID: "gpt" },
|
model: { providerID: "openai", modelID: "gpt" },
|
||||||
})
|
})
|
||||||
|
|
||||||
const textPart = (id: string, sessionID: string, messageID: string): Part => ({
|
const textPart = (id: string, sessionID: string, messageID: string): Text => ({
|
||||||
id,
|
id,
|
||||||
sessionID,
|
sessionID,
|
||||||
messageID,
|
messageID,
|
||||||
@@ -53,4 +55,69 @@ describe("sync optimistic reducers", () => {
|
|||||||
expect(draft.part.msg_1).toBeUndefined()
|
expect(draft.part.msg_1).toBeUndefined()
|
||||||
expect(draft.part.msg_2).toHaveLength(1)
|
expect(draft.part.msg_2).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("mergeOptimisticPage keeps pending messages in fetched timelines", () => {
|
||||||
|
const sessionID = "ses_1"
|
||||||
|
const page = mergeOptimisticPage(
|
||||||
|
{
|
||||||
|
session: [userMessage("msg_1", sessionID)],
|
||||||
|
part: [{ id: "msg_1", part: [textPart("prt_1", sessionID, "msg_1")] }],
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
[{ message: userMessage("msg_2", sessionID), parts: [textPart("prt_2", sessionID, "msg_2")] }],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(page.session.map((x) => x.id)).toEqual(["msg_1", "msg_2"])
|
||||||
|
expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_2"])
|
||||||
|
expect(page.confirmed).toEqual([])
|
||||||
|
expect(page.complete).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mergeOptimisticPage keeps missing optimistic parts until the server has them", () => {
|
||||||
|
const sessionID = "ses_1"
|
||||||
|
const page = mergeOptimisticPage(
|
||||||
|
{
|
||||||
|
session: [userMessage("msg_2", sessionID)],
|
||||||
|
part: [{ id: "msg_2", part: [textPart("prt_2", sessionID, "msg_2")] }],
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
message: userMessage("msg_2", sessionID),
|
||||||
|
parts: [textPart("prt_1", sessionID, "msg_2"), textPart("prt_2", sessionID, "msg_2")],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(page.part.find((x) => x.id === "msg_2")?.part.map((x) => x.id)).toEqual(["prt_1", "prt_2"])
|
||||||
|
expect(page.confirmed).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mergeOptimisticPage confirms echoed messages once all parts arrive", () => {
|
||||||
|
const sessionID = "ses_1"
|
||||||
|
const page = mergeOptimisticPage(
|
||||||
|
{
|
||||||
|
session: [userMessage("msg_2", sessionID)],
|
||||||
|
part: [
|
||||||
|
{
|
||||||
|
id: "msg_2",
|
||||||
|
part: [{ ...textPart("prt_1", sessionID, "msg_2"), text: "server" }, textPart("prt_2", sessionID, "msg_2")],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
message: userMessage("msg_2", sessionID),
|
||||||
|
parts: [textPart("prt_1", sessionID, "msg_2"), textPart("prt_2", sessionID, "msg_2")],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(page.confirmed).toEqual(["msg_2"])
|
||||||
|
expect(page.part.find((x) => x.id === "msg_2")?.part).toMatchObject([
|
||||||
|
{ id: "prt_1", type: "text", text: "server" },
|
||||||
|
{ id: "prt_2", type: "text", text: "prt_2" },
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,6 +54,67 @@ type OptimisticRemoveInput = {
|
|||||||
messageID: string
|
messageID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OptimisticItem = {
|
||||||
|
message: Message
|
||||||
|
parts: Part[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessagePage = {
|
||||||
|
session: Message[]
|
||||||
|
part: { id: string; part: Part[] }[]
|
||||||
|
cursor?: string
|
||||||
|
complete: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasParts = (parts: Part[] | undefined, want: Part[]) => {
|
||||||
|
if (!parts) return want.length === 0
|
||||||
|
return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeParts = (parts: Part[] | undefined, want: Part[]) => {
|
||||||
|
if (!parts) return sortParts(want)
|
||||||
|
const next = [...parts]
|
||||||
|
let changed = false
|
||||||
|
for (const part of want) {
|
||||||
|
const result = Binary.search(next, part.id, (item) => item.id)
|
||||||
|
if (result.found) continue
|
||||||
|
next.splice(result.index, 0, part)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (!changed) return parts
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
||||||
|
if (items.length === 0) return { ...page, confirmed: [] as string[] }
|
||||||
|
|
||||||
|
const session = [...page.session]
|
||||||
|
const part = new Map(page.part.map((item) => [item.id, sortParts(item.part)]))
|
||||||
|
const confirmed: string[] = []
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const result = Binary.search(session, item.message.id, (message) => message.id)
|
||||||
|
const found = result.found
|
||||||
|
if (!found) session.splice(result.index, 0, item.message)
|
||||||
|
|
||||||
|
const current = part.get(item.message.id)
|
||||||
|
if (found && hasParts(current, item.parts)) {
|
||||||
|
confirmed.push(item.message.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
part.set(item.message.id, mergeParts(current, item.parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursor: page.cursor,
|
||||||
|
complete: page.complete,
|
||||||
|
session,
|
||||||
|
part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, part]) => ({ id, part })),
|
||||||
|
confirmed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
||||||
const messages = draft.message[input.sessionID]
|
const messages = draft.message[input.sessionID]
|
||||||
if (messages) {
|
if (messages) {
|
||||||
@@ -121,6 +182,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const inflight = new Map<string, Promise<void>>()
|
const inflight = new Map<string, Promise<void>>()
|
||||||
const inflightDiff = new Map<string, Promise<void>>()
|
const inflightDiff = new Map<string, Promise<void>>()
|
||||||
const inflightTodo = new Map<string, Promise<void>>()
|
const inflightTodo = new Map<string, Promise<void>>()
|
||||||
|
const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
||||||
const maxDirs = 30
|
const maxDirs = 30
|
||||||
const seen = new Map<string, Set<string>>()
|
const seen = new Map<string, Set<string>>()
|
||||||
const [meta, setMeta] = createStore({
|
const [meta, setMeta] = createStore({
|
||||||
@@ -137,6 +199,33 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => {
|
||||||
|
const key = keyFor(directory, sessionID)
|
||||||
|
const list = optimistic.get(key)
|
||||||
|
if (list) {
|
||||||
|
list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => {
|
||||||
|
const key = keyFor(directory, sessionID)
|
||||||
|
if (!messageID) {
|
||||||
|
optimistic.delete(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = optimistic.get(key)
|
||||||
|
if (!list) return
|
||||||
|
list.delete(messageID)
|
||||||
|
if (list.size === 0) optimistic.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOptimistic = (directory: string, sessionID: string) => [
|
||||||
|
...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []),
|
||||||
|
]
|
||||||
|
|
||||||
const seenFor = (directory: string) => {
|
const seenFor = (directory: string) => {
|
||||||
const existing = seen.get(directory)
|
const existing = seen.get(directory)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -159,6 +248,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
||||||
if (sessionIDs.length === 0) return
|
if (sessionIDs.length === 0) return
|
||||||
|
for (const sessionID of sessionIDs) {
|
||||||
|
clearOptimistic(directory, sessionID)
|
||||||
|
}
|
||||||
setMeta(
|
setMeta(
|
||||||
produce((draft) => {
|
produce((draft) => {
|
||||||
for (const sessionID of sessionIDs) {
|
for (const sessionID of sessionIDs) {
|
||||||
@@ -232,8 +324,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
|
|
||||||
setMeta("loading", key, true)
|
setMeta("loading", key, true)
|
||||||
await fetchMessages(input)
|
await fetchMessages(input)
|
||||||
.then((next) => {
|
.then((page) => {
|
||||||
if (!tracked(input.directory, input.sessionID)) return
|
if (!tracked(input.directory, input.sessionID)) return
|
||||||
|
const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID))
|
||||||
|
for (const messageID of next.confirmed) {
|
||||||
|
clearOptimistic(input.directory, input.sessionID, messageID)
|
||||||
|
}
|
||||||
const [store] = globalSync.child(input.directory, { bootstrap: false })
|
const [store] = globalSync.child(input.directory, { bootstrap: false })
|
||||||
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
||||||
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
||||||
@@ -290,11 +386,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
get: getSession,
|
get: getSession,
|
||||||
optimistic: {
|
optimistic: {
|
||||||
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
||||||
|
const directory = input.directory ?? sdk.directory
|
||||||
const [, setStore] = target(input.directory)
|
const [, setStore] = target(input.directory)
|
||||||
|
setOptimistic(directory, input.sessionID, { message: input.message, parts: input.parts })
|
||||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
|
setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
|
||||||
},
|
},
|
||||||
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
||||||
|
const directory = input.directory ?? sdk.directory
|
||||||
const [, setStore] = target(input.directory)
|
const [, setStore] = target(input.directory)
|
||||||
|
clearOptimistic(directory, input.sessionID, input.messageID)
|
||||||
setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
|
setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -316,6 +416,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
variant: input.variant,
|
variant: input.variant,
|
||||||
}
|
}
|
||||||
const [, setStore] = target()
|
const [, setStore] = target()
|
||||||
|
setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts })
|
||||||
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
message,
|
message,
|
||||||
|
|||||||
Reference in New Issue
Block a user