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

This commit is contained in:
Kit Langton
2026-04-27 16:25:37 -04:00
committed by GitHub
parent fab1768826
commit 45eac589f8
2 changed files with 31 additions and 11 deletions
@@ -6,7 +6,8 @@ import { Filesystem } from "@/util/filesystem"
import type { EditorSelection } from "./editor" import type { EditorSelection } from "./editor"
const ZedEditorRowSchema = z.object({ const ZedEditorRowSchema = z.object({
editor_id: z.number(), item_kind: z.string(),
editor_id: z.number().nullable(),
workspace_id: z.number(), workspace_id: z.number(),
workspace_paths: z.string().nullable(), workspace_paths: z.string().nullable(),
timestamp: z.string(), timestamp: z.string(),
@@ -20,6 +21,7 @@ const ZedEditorContentsSchema = z.object({
}) })
type ZedEditorRow = z.infer<typeof ZedEditorRowSchema> type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
export type ZedSelectionResult = export type ZedSelectionResult =
| { type: "selection"; selection: EditorSelection } | { type: "selection"; selection: EditorSelection }
@@ -64,8 +66,9 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
const raw = db const raw = db
.query( .query(
`select `select
i.kind as item_kind,
e.item_id as editor_id, e.item_id as editor_id,
e.workspace_id as workspace_id, i.workspace_id as workspace_id,
w.paths as workspace_paths, w.paths as workspace_paths,
w.timestamp as timestamp, w.timestamp as timestamp,
e.buffer_path as buffer_path, e.buffer_path as buffer_path,
@@ -74,9 +77,9 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
from items i from items i
join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id 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 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 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`, order by w.timestamp desc`,
) )
.all() .all()
@@ -93,6 +96,8 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
.filter((entry) => entry.score > 0) .filter((entry) => entry.score > 0)
.sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row .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) 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 } return { type: "row" as const, row }
} catch { } catch {
return { type: "unavailable" as const } 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 let db: Database | undefined
try { try {
db = new Database(dbPath, { readonly: true }) 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() { export function resolveZedDbPath() {
const candidates = [ const candidates = [
process.env.OPENCODE_ZED_DB, process.env.OPENCODE_ZED_DB,
@@ -6,6 +6,8 @@ import { tmpdir } from "../../fixture/fixture"
type ZedFixtureOptions = { type ZedFixtureOptions = {
workspacePaths?: string | null workspacePaths?: string | null
itemKind?: string
editor?: boolean
selectionStart?: number | null selectionStart?: number | null
selectionEnd?: 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("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 workspaces values (1, ?, ?)", [options.workspacePaths ?? JSON.stringify([dir]), "2026-04-27"])
db.run("insert into panes values (1, 1, 1)") db.run("insert into panes values (1, 1, 1)")
db.run("insert into items values (1, 1, 1, 1, 'Editor')") db.run("insert into items values (1, 1, 1, 1, ?)", [options.itemKind ?? "Editor"])
db.run("insert into editors values (1, 1, ?, ?)", [filePath, "one\ntwo\nthree"]) if (options.editor !== false) {
db.run("insert into editor_selections values (1, 1, ?, ?)", [ db.run("insert into editors values (1, 1, ?, ?)", [filePath, "one\ntwo\nthree"])
options.selectionStart === undefined ? 4 : options.selectionStart, db.run("insert into editor_selections values (1, 1, ?, ?)", [
options.selectionEnd === undefined ? 7 : options.selectionEnd, options.selectionStart === undefined ? 4 : options.selectionStart,
]) options.selectionEnd === undefined ? 7 : options.selectionEnd,
])
}
db.close() db.close()
return { dbPath, filePath } 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" }) 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 () => { test("resolveZedSelection returns unavailable when the database cannot be queried", async () => {
await using tmp = await tmpdir() await using tmp = await tmpdir()