Fix UX issues

This commit is contained in:
James Long 2026-05-04 14:20:50 -04:00
parent d8d7b432ae
commit b7b57a0172
7 changed files with 180 additions and 167 deletions

View file

@ -2,9 +2,7 @@
"version": "7",
"dialect": "sqlite",
"id": "27114226-085b-421a-9a40-29b88747e29a",
"prevIds": [
"aaa2ebeb-caa4-478d-8365-4fc595d16856"
],
"prevIds": ["aaa2ebeb-caa4-478d-8365-4fc595d16856"],
"ddl": [
{
"name": "account_state",
@ -1063,13 +1061,9 @@
"table": "event"
},
{
"columns": [
"active_account_id"
],
"columns": ["active_account_id"],
"tableTo": "account",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "SET NULL",
"nameExplicit": false,
@ -1078,13 +1072,9 @@
"table": "account_state"
},
{
"columns": [
"project_id"
],
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1093,13 +1083,9 @@
"table": "workspace"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1108,13 +1094,9 @@
"table": "message"
},
{
"columns": [
"message_id"
],
"columns": ["message_id"],
"tableTo": "message",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1123,13 +1105,9 @@
"table": "part"
},
{
"columns": [
"project_id"
],
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1138,13 +1116,9 @@
"table": "permission"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1153,13 +1127,9 @@
"table": "session_entry"
},
{
"columns": [
"project_id"
],
"columns": ["project_id"],
"tableTo": "project",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1168,13 +1138,9 @@
"table": "session"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1183,13 +1149,9 @@
"table": "todo"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"tableTo": "session",
"columnsTo": [
"id"
],
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1198,13 +1160,9 @@
"table": "session_share"
},
{
"columns": [
"aggregate_id"
],
"columns": ["aggregate_id"],
"tableTo": "event_sequence",
"columnsTo": [
"aggregate_id"
],
"columnsTo": ["aggregate_id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@ -1213,128 +1171,98 @@
"table": "event"
},
{
"columns": [
"email",
"url"
],
"columns": ["email", "url"],
"nameExplicit": false,
"name": "control_account_pk",
"entityType": "pks",
"table": "control_account"
},
{
"columns": [
"session_id",
"position"
],
"columns": ["session_id", "position"],
"nameExplicit": false,
"name": "todo_pk",
"entityType": "pks",
"table": "todo"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "account_state_pk",
"table": "account_state",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "account_pk",
"table": "account",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "message_pk",
"table": "message",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "part_pk",
"table": "part",
"entityType": "pks"
},
{
"columns": [
"project_id"
],
"columns": ["project_id"],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "session_entry_pk",
"table": "session_entry",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "session_pk",
"table": "session",
"entityType": "pks"
},
{
"columns": [
"session_id"
],
"columns": ["session_id"],
"nameExplicit": false,
"name": "session_share_pk",
"table": "session_share",
"entityType": "pks"
},
{
"columns": [
"aggregate_id"
],
"columns": ["aggregate_id"],
"nameExplicit": false,
"name": "event_sequence_pk",
"table": "event_sequence",
"entityType": "pks"
},
{
"columns": [
"id"
],
"columns": ["id"],
"nameExplicit": false,
"name": "event_pk",
"table": "event",
@ -1498,4 +1426,4 @@
}
],
"renames": []
}
}

View file

@ -12,7 +12,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
import { DialogSessionRename } from "./dialog-session-rename"
import { createDebouncedSignal } from "../util/signal"
import { useToast } from "../ui/toast"
import { DialogWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create"
import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create"
import { Spinner } from "./spinner"
import { errorMessage } from "@/util/error"
import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
@ -101,13 +101,15 @@ export function DialogSessionList() {
return true
}}
onRestore={() => {
dialog.replace(() => (
<DialogWorkspaceSelect
onSelect={(selection) => {
void warp(selection)
}}
/>
))
void openWorkspaceSelect({
dialog,
sdk,
sync,
toast,
onSelect: (selection) => {
void warp(selection)
},
})
return false
}}
/>

View file

@ -30,9 +30,41 @@ export type WorkspaceSelection =
workspaceName: string
}
type WorkspaceSelectValue = WorkspaceSelection | { type: "existing-list" } | { type: "loading" }
type WorkspaceSelectValue = WorkspaceSelection | { type: "existing-list" }
type ExistingWorkspaceSelectValue = { workspace: Workspace }
async function loadWorkspaceAdaptors(input: {
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
toast: ReturnType<typeof useToast>
}) {
const dir = input.sync.path.directory || input.sdk.directory
const url = new URL("/experimental/workspace/adaptor", input.sdk.url)
if (dir) url.searchParams.set("directory", dir)
const res = await input.sdk
.fetch(url)
.then((x) => x.json() as Promise<Adaptor[]>)
.catch(() => undefined)
if (res) return res
input.toast.show({
message: "Failed to load workspace adaptors",
variant: "error",
})
}
export async function openWorkspaceSelect(input: {
dialog: ReturnType<typeof useDialog>
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
toast: ReturnType<typeof useToast>
onSelect: (selection: WorkspaceSelection) => Promise<void> | void
}) {
input.dialog.clear()
const adaptors = await loadWorkspaceAdaptors(input)
if (!adaptors) return
input.dialog.replace(() => <DialogWorkspaceSelect adaptors={adaptors} onSelect={input.onSelect} />)
}
export async function warpWorkspaceSession(input: {
dialog: ReturnType<typeof useDialog>
sdk: ReturnType<typeof useSDK>
@ -77,7 +109,7 @@ export async function warpWorkspaceSession(input: {
}
export function DialogWorkspaceSelect(props: {
current?: WorkspaceSelection
adaptors?: Adaptor[]
onSelect: (selection: WorkspaceSelection) => Promise<void> | void
}) {
const dialog = useDialog()
@ -85,41 +117,21 @@ export function DialogWorkspaceSelect(props: {
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
const [adaptors, setAdaptors] = createSignal<Adaptor[]>()
const [adaptors, setAdaptors] = createSignal<Adaptor[] | undefined>(props.adaptors)
onMount(() => {
dialog.setSize("medium")
void (async () => {
const dir = sync.path.directory || sdk.directory
const url = new URL("/experimental/workspace/adaptor", sdk.url)
if (dir) url.searchParams.set("directory", dir)
const res = await sdk
.fetch(url)
.then((x) => x.json() as Promise<Adaptor[]>)
.catch(() => undefined)
if (!res) {
toast.show({
message: "Failed to load workspace adaptors",
variant: "error",
})
return
}
if (adaptors()) return
const res = await loadWorkspaceAdaptors({ sdk, sync, toast })
if (!res) return
setAdaptors(res)
})()
})
const options = createMemo<DialogSelectOption<WorkspaceSelectValue>[]>(() => {
const list = adaptors()
if (!list) {
return [
{
title: "Loading workspaces...",
value: { type: "loading" as const },
description: "Fetching available workspace adaptors",
category: "New workspace",
},
]
}
if (!list) return []
const recent = sync.data.session
.toSorted((a, b) => b.time.updated - a.time.updated)
.flatMap((session) => (session.workspaceID ? [session.workspaceID] : []))
@ -162,13 +174,13 @@ export function DialogWorkspaceSelect(props: {
]
})
if (!adaptors()) return null
return (
<DialogSelect<WorkspaceSelectValue>
title="Warp"
skipFilter={true}
renderFilter={false}
options={options()}
current={props.current}
onSelect={(option) => {
if (!option.value) return
if (option.value.type === "none") {

View file

@ -41,7 +41,7 @@ import { useKV } from "../../context/kv"
import { createFadeIn } from "../../util/signal"
import { useTextareaKeybindings } from "../textarea-keybindings"
import { DialogSkill } from "../dialog-skill"
import { DialogWorkspaceSelect, warpWorkspaceSession, type WorkspaceSelection } from "../dialog-workspace-create"
import { openWorkspaceSelect, warpWorkspaceSession, type WorkspaceSelection } from "../dialog-workspace-create"
import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
import { useArgs } from "@tui/context/args"
import { Flag } from "@opencode-ai/core/flag/flag"
@ -557,14 +557,15 @@ export function Prompt(props: PromptProps) {
name: "warp",
},
onSelect: (dialog) => {
dialog.replace(() => (
<DialogWorkspaceSelect
current={selectedWorkspace()}
onSelect={(selection) => {
void warpSession(selection)
}}
/>
))
void openWorkspaceSelect({
dialog,
sdk,
sync,
toast,
onSelect: (selection) => {
void warpSession(selection)
},
})
},
},
]
@ -815,14 +816,15 @@ export function Prompt(props: PromptProps) {
dialog.replace(() => (
<DialogWorkspaceUnavailable
onRestore={() => {
dialog.replace(() => (
<DialogWorkspaceSelect
current={selectedWorkspace()}
onSelect={(selection) => {
void warpSession(selection)
}}
/>
))
void openWorkspaceSelect({
dialog,
sdk,
sync,
toast,
onSelect: (selection) => {
void warpSession(selection)
},
})
return false
}}
/>

View file

@ -75,10 +75,6 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
)
})
return handlers
.handle("start", start)
.handle("replay", replay)
.handle("steal", steal)
.handle("history", history)
return handlers.handle("start", start).handle("replay", replay).handle("steal", steal).handle("history", history)
}),
)

View file

@ -321,7 +321,6 @@ describe("workspace-old schemas and exports", () => {
expect(() => WorkspaceOld.CreateInput.zod.parse({ ...input, id: "bad" })).toThrow()
expect(() => WorkspaceOld.CreateInput.zod.parse({ ...input, branch: 1 })).toThrow()
})
})
describe("workspace-old CRUD", () => {

View file

@ -5448,6 +5448,80 @@
]
}
},
"/sync/steal": {
"post": {
"operationId": "sync.steal",
"parameters": [
{
"in": "query",
"name": "directory",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "workspace",
"schema": {
"type": "string"
}
}
],
"summary": "Steal session into workspace",
"description": "Update a session to belong to the current workspace through the sync event system.",
"responses": {
"200": {
"description": "Session stolen into workspace",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"pattern": "^ses.*"
}
},
"required": ["sessionID"]
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"pattern": "^ses.*"
}
},
"required": ["sessionID"]
}
}
}
},
"x-codeSamples": [
{
"lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.sync.steal({\n ...\n})"
}
]
}
},
"/sync/history": {
"post": {
"operationId": "sync.history.list",