James Long преди 3 месеца
родител
ревизия
b7b57a0172

+ 36 - 108
packages/opencode/migration/20260504145000_add_sync_owner/snapshot.json

@@ -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": []
-}
+}

+ 10 - 8
packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

@@ -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
         }}
       />

+ 40 - 28
packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx

@@ -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") {

+ 19 - 17
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -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
           }}
         />

+ 1 - 5
packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts

@@ -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)
   }),
 )

+ 0 - 1
packages/opencode/test/control-plane/workspace.test.ts

@@ -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", () => {

+ 74 - 0
packages/sdk/openapi.json

@@ -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",