From 45eac589f8f26fcaab8341382044684697278694 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 27 Apr 2026 16:25:37 -0400 Subject: [PATCH] fix(tui): preserve Zed context on terminal focus (#24662) --- .../src/cli/cmd/tui/context/editor-zed.ts | 19 +++++++++++---- .../test/cli/tui/editor-context.test.ts | 23 ++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts b/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts index 9a776f2506..40063dc70a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts +++ b/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 +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, diff --git a/packages/opencode/test/cli/tui/editor-context.test.ts b/packages/opencode/test/cli/tui/editor-context.test.ts index 770e850d2d..767eeb8ec1 100644 --- a/packages/opencode/test/cli/tui/editor-context.test.ts +++ b/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()