Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions packages/app/src/context/global-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type LspStatus,
type VcsInfo,
type PermissionRequest,
type ModeSwitchRequest,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
Expand Down Expand Up @@ -48,6 +49,9 @@ type State = {
permission: {
[sessionID: string]: PermissionRequest[]
}
modeswitch: {
[sessionID: string]: ModeSwitchRequest[]
}
mcp: {
[name: string]: McpStatus
}
Expand Down Expand Up @@ -96,6 +100,7 @@ function createGlobalSync() {
session_diff: {},
todo: {},
permission: {},
modeswitch: {},
mcp: {},
lsp: [],
vcs: undefined,
Expand Down Expand Up @@ -205,6 +210,38 @@ function createGlobalSync() {
}
})
}),
sdk.modeswitch.list().then((x) => {
const grouped: Record<string, ModeSwitchRequest[]> = {}
for (const req of x.data ?? []) {
if (!req?.id || !req.sessionID) continue
const existing = grouped[req.sessionID]
if (existing) {
existing.push(req)
continue
}
grouped[req.sessionID] = [req]
}

batch(() => {
for (const sessionID of Object.keys(store.modeswitch)) {
if (grouped[sessionID]) continue
setStore("modeswitch", sessionID, [])
}
for (const [sessionID, requests] of Object.entries(grouped)) {
setStore(
"modeswitch",
sessionID,
reconcile(
requests
.filter((r) => !!r?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
}
})
}),
]).then(() => {
setStore("status", "complete")
})
Expand Down Expand Up @@ -393,6 +430,43 @@ function createGlobalSync() {
)
break
}
case "modeswitch.asked": {
const sessionID = event.properties.sessionID
const requests = store.modeswitch[sessionID]
if (!requests) {
setStore("modeswitch", sessionID, [event.properties])
break
}

const result = Binary.search(requests, event.properties.id, (r) => r.id)
if (result.found) {
setStore("modeswitch", sessionID, result.index, reconcile(event.properties))
break
}

setStore(
"modeswitch",
sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties)
}),
)
break
}
case "modeswitch.replied": {
const requests = store.modeswitch[event.properties.sessionID]
if (!requests) break
const result = Binary.search(requests, event.properties.requestID, (r) => r.id)
if (!result.found) break
setStore(
"modeswitch",
event.properties.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
break
}
case "lsp.updated": {
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
Expand Down
62 changes: 39 additions & 23 deletions packages/app/src/pages/directory-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createMemo, Show, type ParentProps } from "solid-js"
import { useNavigate, useParams } from "@solidjs/router"
import { SDKProvider, useSDK } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
import { LocalProvider, useLocal } from "@/context/local"

import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
Expand All @@ -18,30 +18,46 @@ export default function Layout(props: ParentProps) {
<Show when={params.dir} keyed>
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {
const sync = useSync()
const sdk = useSDK()
const respond = (input: {
sessionID: string
permissionID: string
response: "once" | "always" | "reject"
}) => sdk.client.permission.respond(input)
<LocalProvider>
{iife(() => {
const sync = useSync()
const sdk = useSDK()
const local = useLocal()
const respond = (input: {
sessionID: string
permissionID: string
response: "once" | "always" | "reject"
}) => sdk.client.permission.respond(input)

const navigateToSession = (sessionID: string) => {
navigate(`/${params.dir}/session/${sessionID}`)
}
const respondToModeSwitch = (input: {
sessionID: string
requestID: string
response: "approve" | "reject"
targetMode?: string
}) => {
sdk.client.modeswitch.reply({ requestID: input.requestID, reply: input.response })
if (input.response === "approve" && input.targetMode) {
local.agent.set(input.targetMode)
}
}

return (
<DataProvider
data={sync.data}
directory={directory()}
onPermissionRespond={respond}
onNavigateToSession={navigateToSession}
>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
})}
const navigateToSession = (sessionID: string) => {
navigate(`/${params.dir}/session/${sessionID}`)
}

return (
<DataProvider
data={sync.data}
directory={directory()}
onPermissionRespond={respond}
onModeSwitchRespond={respondToModeSwitch}
onNavigateToSession={navigateToSession}
>
{props.children}
</DataProvider>
)
})}
</LocalProvider>
</SyncProvider>
</SDKProvider>
</Show>
Expand Down
42 changes: 42 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
Command,
PermissionRequest,
QuestionRequest,
ModeSwitchRequest,
LspStatus,
McpStatus,
McpResource,
Expand Down Expand Up @@ -46,6 +47,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
question: {
[sessionID: string]: QuestionRequest[]
}
modeswitch: {
[sessionID: string]: ModeSwitchRequest[]
}
config: Config
session: Session[]
session_status: {
Expand Down Expand Up @@ -85,6 +89,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
agent: [],
permission: {},
question: {},
modeswitch: {},
command: [],
provider: [],
provider_default: {},
Expand Down Expand Up @@ -185,6 +190,43 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
break
}

case "modeswitch.replied": {
const requests = store.modeswitch[event.properties.sessionID]
if (!requests) break
const match = Binary.search(requests, event.properties.requestID, (r) => r.id)
if (!match.found) break
setStore(
"modeswitch",
event.properties.sessionID,
produce((draft) => {
draft.splice(match.index, 1)
}),
)
break
}

case "modeswitch.asked": {
const request = event.properties
const requests = store.modeswitch[request.sessionID]
if (!requests) {
setStore("modeswitch", request.sessionID, [request])
break
}
const match = Binary.search(requests, request.id, (r) => r.id)
if (match.found) {
setStore("modeswitch", request.sessionID, match.index, reconcile(request))
break
}
setStore(
"modeswitch",
request.sessionID,
produce((draft) => {
draft.splice(match.index, 0, request)
}),
)
break
}

case "todo.updated":
setStore("todo", event.properties.sessionID, event.properties.todos)
break
Expand Down
17 changes: 15 additions & 2 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { useExit } from "../../context/exit"
import { Filesystem } from "@/util/filesystem"
import { PermissionPrompt } from "./permission"
import { QuestionPrompt } from "./question"
import { ModeSwitchPrompt } from "./modeswitch"
import { DialogExportOptions } from "../../ui/dialog-export-options"
import { formatTranscript } from "../../util/transcript"

Expand Down Expand Up @@ -126,6 +127,10 @@ export function Session() {
if (session()?.parentID) return []
return children().flatMap((x) => sync.data.question[x.id] ?? [])
})
const modeswitches = createMemo(() => {
if (session()?.parentID) return []
return children().flatMap((x) => sync.data.modeswitch[x.id] ?? [])
})

const pending = createMemo(() => {
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
Expand Down Expand Up @@ -1011,8 +1016,16 @@ export function Session() {
<Show when={permissions().length === 0 && questions().length > 0}>
<QuestionPrompt request={questions()[0]} />
</Show>
<Show when={permissions().length === 0 && questions().length === 0 && modeswitches().length > 0}>
<ModeSwitchPrompt request={modeswitches()[0]} />
</Show>
<Prompt
visible={!session()?.parentID && permissions().length === 0 && questions().length === 0}
visible={
!session()?.parentID &&
permissions().length === 0 &&
questions().length === 0 &&
modeswitches().length === 0
}
ref={(r) => {
prompt = r
promptRef.set(r)
Expand All @@ -1021,7 +1034,7 @@ export function Session() {
r.set(route.initialPrompt)
}
}}
disabled={permissions().length > 0 || questions().length > 0}
disabled={permissions().length > 0 || questions().length > 0 || modeswitches().length > 0}
onSubmit={() => {
toBottom()
}}
Expand Down
Loading
Loading