+
{label}
{(sel) => (
diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts
index 3e5f0ff1a4d..7b412cd2450 100644
--- a/packages/app/src/components/prompt-input/submit.test.ts
+++ b/packages/app/src/components/prompt-input/submit.test.ts
@@ -26,6 +26,22 @@ let selected = "/repo/worktree-a"
let variant: string | undefined
const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
+const prompt = {
+ ready: () => Object.assign(() => true, { promise: Promise.resolve(true) }),
+ current: () => promptValue,
+ cursor: () => 0,
+ dirty: () => true,
+ reset: () => undefined,
+ set: () => undefined,
+ context: {
+ add: () => undefined,
+ remove: () => undefined,
+ removeComment: () => undefined,
+ updateComment: () => undefined,
+ replaceComments: () => undefined,
+ items: () => [],
+ },
+}
const clientFor = (directory: string) => {
createdClients.push(directory)
@@ -73,6 +89,7 @@ beforeAll(async () => {
}))
mock.module("@opencode-ai/ui/toast", () => ({
+ Toast: { Region: () => null },
showToast: () => 0,
}))
@@ -116,16 +133,7 @@ beforeAll(async () => {
}))
mock.module("@/context/prompt", () => ({
- usePrompt: () => ({
- current: () => promptValue,
- reset: () => undefined,
- set: () => undefined,
- context: {
- add: () => undefined,
- remove: () => undefined,
- items: () => [],
- },
- }),
+ usePrompt: () => prompt,
}))
mock.module("@/context/layout", () => ({
@@ -147,12 +155,12 @@ beforeAll(async () => {
return clientFor(opts.directory)
},
}
- return sdk
+ return () => sdk
},
}))
mock.module("@/context/sync", () => ({
- useSync: () => ({
+ useSync: () => () => ({
data: { command: [] },
session: {
optimistic: {
@@ -176,7 +184,7 @@ beforeAll(async () => {
}))
mock.module("@/context/server-sync", () => ({
- useServerSync: () => ({
+ useServerSync: () => () => ({
child: (directory: string) => {
syncedDirectories.push(directory)
storedSessions[directory] ??= []
@@ -232,6 +240,7 @@ beforeEach(() => {
describe("prompt submit worktree selection", () => {
test("reads the latest worktree accessor value per submit", async () => {
const submit = createPromptSubmit({
+ prompt,
info: () => undefined,
imageAttachments: () => [],
commentCount: () => 0,
@@ -269,6 +278,7 @@ describe("prompt submit worktree selection", () => {
test("applies auto-accept to newly created sessions", async () => {
const submit = createPromptSubmit({
+ prompt,
info: () => undefined,
imageAttachments: () => [],
commentCount: () => 0,
@@ -299,6 +309,7 @@ describe("prompt submit worktree selection", () => {
variant = "high"
const submit = createPromptSubmit({
+ prompt,
info: () => ({ id: "session-1" }),
imageAttachments: () => [],
commentCount: () => 0,
@@ -330,6 +341,7 @@ describe("prompt submit worktree selection", () => {
test("seeds new sessions before optimistic prompts are added", async () => {
const submit = createPromptSubmit({
+ prompt,
info: () => undefined,
imageAttachments: () => [],
commentCount: () => 0,
diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts
index bd4d9850885..a95f0a60307 100644
--- a/packages/app/src/components/prompt-input/submit.ts
+++ b/packages/app/src/components/prompt-input/submit.ts
@@ -7,14 +7,14 @@ import { batch, type Accessor } from "solid-js"
import type { FileSelection } from "@/context/file"
import { useServer } from "@/context/server"
import { useTabs } from "@/context/tabs"
-import { useServerSync } from "@/context/server-sync"
+import { useServerSync, type ServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local"
import { usePermission } from "@/context/permission"
-import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
-import { useSDK } from "@/context/sdk"
-import { useSync } from "@/context/sync"
+import { type ContextItem, type ImageAttachmentPart, type Prompt, type usePrompt } from "@/context/prompt"
+import { useSDK, type DirectorySDK } from "@/context/sdk"
+import { useSync, type DirectorySync } from "@/context/sync"
import { Identifier } from "@/utils/id"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { buildRequestParts } from "./build-request-parts"
@@ -40,9 +40,9 @@ export type FollowupDraft = {
}
type FollowupSendInput = {
- client: ReturnType["client"]
- serverSync: ReturnType
- sync: ReturnType
+ client: DirectorySDK["client"]
+ serverSync: ServerSync
+ sync: DirectorySync
draft: FollowupDraft
messageID?: string
optimisticBusy?: boolean
@@ -174,6 +174,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
}
type PromptSubmitInput = {
+ prompt: ReturnType
info: Accessor<{ id: string } | undefined>
imageAttachments: Accessor
commentCount: Accessor
@@ -211,14 +212,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const serverSync = useServerSync()
const local = useLocal()
const permission = usePermission()
- const prompt = usePrompt()
+ const prompt = input.prompt
const layout = useLayout()
const language = useLanguage()
const params = useParams()
const [search] = useSearchParams<{ draftId?: string }>()
const server = useServer()
const tabs = useTabs()
- const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID)
+ const pendingKey = (sessionID: string) => ScopedKey.from(sdk().scope, sessionID)
const errorMessage = (err: unknown) => {
if (err && typeof err === "object" && "data" in err) {
@@ -233,8 +234,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const sessionID = params.id
if (!sessionID) return Promise.resolve()
- serverSync.todo.set(sessionID, [])
- const [, setStore] = serverSync.child(sdk.directory)
+ serverSync().todo.set(sessionID, [])
+ const [, setStore] = serverSync().child(sdk().directory)
setStore("todo", sessionID, [])
input.onAbort?.()
@@ -247,8 +248,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
pending.delete(key)
return Promise.resolve()
}
- return sdk.client.session
- .abort({
+ return sdk()
+ .client.session.abort({
sessionID,
})
.catch(() => {})
@@ -281,7 +282,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}
const seed = (dir: string, info: Session) => {
- const [, setStore] = serverSync.child(dir)
+ const [, setStore] = serverSync().child(dir)
setStore("session", (list: Session[]) => {
const result = Binary.search(list, info.id, (item) => item.id)
const next = [...list]
@@ -321,13 +322,13 @@ export function createPromptSubmit(input: PromptSubmitInput) {
input.addToHistory(currentPrompt, mode)
input.resetHistoryNavigation()
- const projectDirectory = sdk.directory
+ const projectDirectory = sdk().directory
const isNewSession = !params.id
const shouldAutoAccept = isNewSession && input.autoAccept()
const worktreeSelection = input.newSessionWorktree?.() || "main"
let sessionDirectory = projectDirectory
- let client = sdk.client
+ let client = sdk().client
if (isNewSession) {
if (worktreeSelection === "create") {
@@ -349,7 +350,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
})
return
}
- WorktreeState.pending(sdk.scope, createdWorktree.directory)
+ WorktreeState.pending(sdk().scope, createdWorktree.directory)
sessionDirectory = createdWorktree.directory
}
@@ -358,11 +359,11 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}
if (sessionDirectory !== projectDirectory) {
- client = sdk.createClient({
+ client = sdk().createClient({
directory: sessionDirectory,
throwOnError: true,
})
- serverSync.child(sessionDirectory)
+ serverSync().child(sessionDirectory)
}
input.onNewSessionWorktreeReset?.()
@@ -470,7 +471,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
if (text.startsWith("/")) {
const [cmdName, ...args] = text.split(" ")
const commandName = cmdName.slice(1)
- const customCommand = sync.data.command.find((c) => c.name === commandName)
+ const customCommand = sync().data.command.find((c) => c.name === commandName)
if (customCommand) {
clearInput()
client.session
@@ -504,7 +505,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const messageID = Identifier.ascending("message")
const removeOptimisticMessage = () => {
- sync.session.optimistic.remove({
+ sync().session.optimistic.remove({
directory: sessionDirectory,
sessionID: session.id,
messageID,
@@ -515,17 +516,17 @@ export function createPromptSubmit(input: PromptSubmitInput) {
clearInput()
const waitForWorktree = async () => {
- const worktree = WorktreeState.get(sdk.scope, sessionDirectory)
+ const worktree = WorktreeState.get(sdk().scope, sessionDirectory)
if (!worktree || worktree.status !== "pending") return true
if (sessionDirectory === projectDirectory) {
- sync.set("session_status", session.id, { type: "busy" })
+ sync().set("session_status", session.id, { type: "busy" })
}
const controller = new AbortController()
const cleanup = () => {
if (sessionDirectory === projectDirectory) {
- sync.set("session_status", session.id, { type: "idle" })
+ sync().set("session_status", session.id, { type: "idle" })
}
removeOptimisticMessage()
restoreCommentItems(commentItems)
@@ -559,12 +560,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}, timeoutMs)
})
- const result = await Promise.race([WorktreeState.wait(sdk.scope, sessionDirectory), abortWait, timeout]).finally(
- () => {
- if (timer.id === undefined) return
- clearTimeout(timer.id)
- },
- )
+ const result = await Promise.race([
+ WorktreeState.wait(sdk().scope, sessionDirectory),
+ abortWait,
+ timeout,
+ ]).finally(() => {
+ if (timer.id === undefined) return
+ clearTimeout(timer.id)
+ })
pending.delete(pendingKey(session.id))
if (controller.signal.aborted) return false
if (result.status === "failed") throw new Error(result.message)
@@ -573,8 +576,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
void sendFollowupDraft({
client,
- sync,
- serverSync,
+ sync: sync(),
+ serverSync: serverSync(),
draft,
messageID,
optimisticBusy: sessionDirectory === projectDirectory,
@@ -582,7 +585,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}).catch((err) => {
pending.delete(pendingKey(session.id))
if (sessionDirectory === projectDirectory) {
- sync.set("session_status", session.id, { type: "idle" })
+ sync().set("session_status", session.id, { type: "idle" })
}
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx
index 1f65e9adb3c..c61afb262c0 100644
--- a/packages/app/src/components/session-context-usage.tsx
+++ b/packages/app/src/components/session-context-usage.tsx
@@ -42,7 +42,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
pathFromTab: file.pathFromTab,
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
})
- const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
+ const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
const usd = createMemo(
() =>
diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx
index 88c3889858c..a601928de48 100644
--- a/packages/app/src/components/session/session-context-tab.tsx
+++ b/packages/app/src/components/session/session-context-tab.tsx
@@ -96,13 +96,13 @@ export function SessionContextTab() {
const providers = useProviders()
const { params, view } = useSessionLayout()
- const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
+ const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
const messages = createMemo(
() => {
const id = params.id
if (!id) return emptyMessages
- return (sync.data.message[id] ?? []) as Message[]
+ return (sync().data.message[id] ?? []) as Message[]
},
emptyMessages,
{ equals: same },
@@ -180,7 +180,7 @@ export function SessionContextTab() {
if (!c?.input) return []
return estimateSessionContextBreakdown({
messages: messages(),
- parts: sync.data.part as Record,
+ parts: sync().data.part as Record,
input: c.input,
systemPrompt: systemPrompt(),
})
@@ -221,7 +221,7 @@ export function SessionContextTab() {
let scroll: HTMLDivElement | undefined
let frame: number | undefined
let pending: { x: number; y: number } | undefined
- const getParts = (id: string) => (sync.data.part[id] ?? []) as Part[]
+ const getParts = (id: string) => (sync().data.part[id] ?? []) as Part[]
const restoreScroll = () => {
const el = scroll
diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
index 2d58b5f5633..db1990dfaa7 100644
--- a/packages/app/src/components/session/session-header.tsx
+++ b/packages/app/src/components/session/session-header.tsx
@@ -229,7 +229,7 @@ export function SessionHeader() {
)
const opening = createMemo(() => openRequest.app !== undefined)
const tint = createMemo(() =>
- messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
+ messageAgentColor(params.id ? sync().data.message[params.id] : undefined, sync().data.agent),
)
const v2ActionsState = createMemo(() => ({
statusVisible: status(),
diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx
index 36c1eb42c31..dba0560425e 100644
--- a/packages/app/src/components/session/session-new-view.tsx
+++ b/packages/app/src/components/session/session-new-view.tsx
@@ -20,24 +20,24 @@ export function NewSessionView(props: NewSessionViewProps) {
const sdk = useSDK()
const language = useLanguage()
- const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
+ const sandboxes = createMemo(() => sync().project?.sandboxes ?? [])
const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
const current = createMemo(() => {
const selection = props.worktree
if (options().includes(selection)) return selection
return MAIN_WORKTREE
})
- const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory)
+ const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory)
const isWorktree = createMemo(() => {
- const project = sync.project
+ const project = sync().project
if (!project) return false
- return sdk.directory !== project.worktree
+ return sdk().directory !== project.worktree
})
const label = (value: string) => {
if (value === MAIN_WORKTREE) {
if (isWorktree()) return language.t("session.new.worktree.main")
- const branch = sync.data.vcs?.branch
+ const branch = sync().data.vcs?.branch
if (branch) return language.t("session.new.worktree.mainWithBranch", { branch })
return language.t("session.new.worktree.main")
}
@@ -69,7 +69,7 @@ export function NewSessionView(props: NewSessionViewProps) {
{label(current())}