浏览代码

fix(tui): preserve Zed context on terminal focus (#24662)

Kit Langton 3 月之前
父节点
当前提交
45eac589f8

+ 14 - 5
packages/opencode/src/cli/cmd/tui/context/editor-zed.ts

@@ -6,7 +6,8 @@ import { Filesystem } from "@/util/filesystem"
 import type { EditorSelection } from "./editor"
 
 const ZedEditorRowSchema = z.object({
-  editor_id: z.number(),
+  item_kind: z.string(),
+  editor_id: z.number().nullable(),
   workspace_id: z.number(),
   workspace_paths: z.string().nullable(),
   timestamp: z.string(),
@@ -20,6 +21,7 @@ const ZedEditorContentsSchema = z.object({
 })
 
 type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
+type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
 
 export type ZedSelectionResult =
   | { type: "selection"; selection: EditorSelection }
@@ -64,8 +66,9 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
     const raw = db
       .query(
         `select
+          i.kind as item_kind,
           e.item_id as editor_id,
-          e.workspace_id as workspace_id,
+          i.workspace_id as workspace_id,
           w.paths as workspace_paths,
           w.timestamp as timestamp,
           e.buffer_path as buffer_path,
@@ -74,9 +77,9 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
         from items i
         join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id
         join workspaces w on w.workspace_id = i.workspace_id
-        join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id
+        left join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id
         left join editor_selections s on s.editor_id = e.item_id and s.workspace_id = e.workspace_id
-        where i.active = 1 and p.active = 1 and i.kind = 'Editor' and e.buffer_path is not null
+        where i.active = 1 and p.active = 1
         order by w.timestamp desc`,
       )
       .all()
@@ -93,6 +96,8 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
       .filter((entry) => entry.score > 0)
       .sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row
     if (!row) return { type: "empty" as const }
+    if (row.item_kind !== "Editor") return { type: "unavailable" as const }
+    if (!isZedActiveEditorRow(row)) return { type: "empty" as const }
     return { type: "row" as const, row }
   } catch {
     return { type: "unavailable" as const }
@@ -101,7 +106,7 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
   }
 }
 
-function queryZedEditorContents(dbPath: string, row: ZedEditorRow) {
+function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
   let db: Database | undefined
   try {
     db = new Database(dbPath, { readonly: true })
@@ -123,6 +128,10 @@ function queryZedEditorContents(dbPath: string, row: ZedEditorRow) {
   }
 }
 
+function isZedActiveEditorRow(row: ZedEditorRow): row is ZedActiveEditorRow {
+  return row.item_kind === "Editor" && row.editor_id != null
+}
+
 export function resolveZedDbPath() {
   const candidates = [
     process.env.OPENCODE_ZED_DB,

+ 17 - 6
packages/opencode/test/cli/tui/editor-context.test.ts

@@ -6,6 +6,8 @@ import { tmpdir } from "../../fixture/fixture"
 
 type ZedFixtureOptions = {
   workspacePaths?: string | null
+  itemKind?: string
+  editor?: boolean
   selectionStart?: number | null
   selectionEnd?: number | null
 }
@@ -23,12 +25,14 @@ async function writeZedFixture(dir: string, options: ZedFixtureOptions = {}) {
   db.run("create table editor_selections (editor_id integer, workspace_id integer, start integer, end integer)")
   db.run("insert into workspaces values (1, ?, ?)", [options.workspacePaths ?? JSON.stringify([dir]), "2026-04-27"])
   db.run("insert into panes values (1, 1, 1)")
-  db.run("insert into items values (1, 1, 1, 1, 'Editor')")
-  db.run("insert into editors values (1, 1, ?, ?)", [filePath, "one\ntwo\nthree"])
-  db.run("insert into editor_selections values (1, 1, ?, ?)", [
-    options.selectionStart === undefined ? 4 : options.selectionStart,
-    options.selectionEnd === undefined ? 7 : options.selectionEnd,
-  ])
+  db.run("insert into items values (1, 1, 1, 1, ?)", [options.itemKind ?? "Editor"])
+  if (options.editor !== false) {
+    db.run("insert into editors values (1, 1, ?, ?)", [filePath, "one\ntwo\nthree"])
+    db.run("insert into editor_selections values (1, 1, ?, ?)", [
+      options.selectionStart === undefined ? 4 : options.selectionStart,
+      options.selectionEnd === undefined ? 7 : options.selectionEnd,
+    ])
+  }
   db.close()
 
   return { dbPath, filePath }
@@ -68,6 +72,13 @@ test("resolveZedSelection returns empty when no workspace matches", async () =>
   expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({ type: "empty" })
 })
 
+test("resolveZedSelection returns unavailable when a Zed terminal is active", async () => {
+  await using tmp = await tmpdir()
+  const fixture = await writeZedFixture(tmp.path, { itemKind: "Terminal", editor: false })
+
+  expect(await resolveZedSelection(fixture.dbPath, tmp.path)).toEqual({ type: "unavailable" })
+})
+
 test("resolveZedSelection returns unavailable when the database cannot be queried", async () => {
   await using tmp = await tmpdir()