diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index e372c59b99..157ca20582 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -33,6 +33,28 @@ export type WorkspaceSelection = type WorkspaceSelectValue = WorkspaceSelection | { type: "existing-list" } type ExistingWorkspaceSelectValue = { workspace: Workspace } +export function recentConnectedWorkspaces(input: { + sessions: readonly { workspaceID?: string; time: { updated: number } }[] + get: (workspaceID: string) => WorkspaceInfo | undefined + status: (workspaceID: string) => string | undefined + limit?: number +}) { + const workspaces = input.sessions + .toSorted((a, b) => b.time.updated - a.time.updated) + .flatMap((session) => { + const workspace = session.workspaceID ? input.get(session.workspaceID) : undefined + return workspace && input.status(workspace.id) === "connected" ? [workspace] : [] + }) + .filter((workspace, index, list) => list.findIndex((item) => item.id === workspace.id) === index) + const recent = workspaces.slice(0, input.limit ?? 3) + + return { recent, hasMore: recent.length < workspaces.length } +} + +export function warpReminderText(dir: string) { + return `The user has changed the current working directory to "${dir}". This is still the same project but at a possibly new location; take this into account when working with any files from now on.` +} + async function loadWorkspaceAdapters(input: { sdk: ReturnType sync: ReturnType @@ -77,7 +99,7 @@ export async function warpWorkspaceSession(input: { }): Promise { const result = await input.sdk.client.experimental.workspace .warp({ - id: input.workspaceID ?? undefined, + id: input.workspaceID, sessionID: input.sessionID, }) .catch(() => undefined) @@ -93,10 +115,30 @@ export async function warpWorkspaceSession(input: { await input.sync.bootstrap({ fatal: false }).catch(() => undefined) + const dir = input.project.instance.directory() || input.sync.path.directory + if (dir) { + await input.sdk.client.session + .promptAsync({ + sessionID: input.sessionID, + workspace: input.workspaceID ?? undefined, + noReply: true, + parts: [ + { + type: "text", + text: warpReminderText(dir), + synthetic: true, + }, + ], + }) + .catch(() => undefined) + } + await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]) - input.done?.() - if (input.done) return true + if (input.done) { + input.done() + return true + } input.dialog.clear() return true } @@ -125,15 +167,11 @@ export function DialogWorkspaceSelect(props: { const options = createMemo[]>(() => { const list = adapters() if (!list) return [] - const recent = sync.data.session - .toSorted((a, b) => b.time.updated - a.time.updated) - .flatMap((session) => (session.workspaceID ? [session.workspaceID] : [])) - .filter((workspaceID, index, list) => list.indexOf(workspaceID) === index) - .flatMap((workspaceID) => { - const workspace = project.workspace.get(workspaceID) - return workspace && project.workspace.status(workspace.id) === "connected" ? [workspace] : [] - }) - .slice(0, 3) + const { recent, hasMore } = recentConnectedWorkspaces({ + sessions: sync.data.session, + get: project.workspace.get, + status: project.workspace.status, + }) return [ ...list.map((adapter) => ({ title: adapter.name, @@ -158,12 +196,16 @@ export function DialogWorkspaceSelect(props: { }, category: "Choose workspace", })), - { - title: "View all workspaces", - value: { type: "existing-list" as const }, - description: "Choose from all workspaces", - category: "Choose workspace", - }, + ...(hasMore + ? [ + { + title: "View all workspaces", + value: { type: "existing-list" as const }, + description: "Choose from all workspaces", + category: "Choose workspace", + }, + ] + : []), ] }) diff --git a/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts b/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts new file mode 100644 index 0000000000..7d051923f6 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "bun:test" +import { recentConnectedWorkspaces } from "../../../../src/cli/cmd/tui/component/dialog-workspace-create" + +describe("recentConnectedWorkspaces", () => { + test("returns unique connected workspaces after filtering missing and inactive entries", () => { + const workspaces = [ + { id: "wrk_a", name: "alpha" }, + { id: "wrk_b", name: "beta" }, + { id: "wrk_c", name: "gamma" }, + { id: "wrk_d", name: "delta" }, + { id: "wrk_e", name: "epsilon" }, + ] + const status = { + wrk_a: "connected", + wrk_b: "disconnected", + wrk_c: "error", + wrk_d: "connected", + wrk_e: "connected", + } as const + + const { recent } = recentConnectedWorkspaces({ + sessions: [ + { time: { updated: 900 } }, + { workspaceID: "wrk_b", time: { updated: 800 } }, + { workspaceID: "wrk_a", time: { updated: 700 } }, + { workspaceID: "wrk_a", time: { updated: 600 } }, + { workspaceID: "wrk_missing", time: { updated: 500 } }, + { workspaceID: "wrk_c", time: { updated: 400 } }, + { workspaceID: "wrk_d", time: { updated: 300 } }, + { workspaceID: "wrk_e", time: { updated: 200 } }, + ], + get: (workspaceID) => workspaces.find((workspace) => workspace.id === workspaceID), + status: (workspaceID) => status[workspaceID as keyof typeof status], + }) + + expect(recent.map((workspace) => workspace.id)).toEqual(["wrk_a", "wrk_d", "wrk_e"]) + }) +})