Skip to content

Commit 58fe982

Browse files
committed
feat(core): restore session move and disable it for models only
The fork removed upstream's session move in 9c9ef04 when it added session roots. Move is part of upstream's core session engine (a saved `session.moved` event, a `move` inbox item, runner and execution handoff, bus routing to both locations), so it cannot live in a plugin, and upstream keeps changing it. In the last upstream merge about 12 of 27 conflicts came from the removal. Roots and move do not compete: roots add folders a session can use, move changes its working folder. This restores move exactly as upstream has it and keeps roots on top, so the fork now only adds to upstream code in this area. Restored from upstream (files now match upstream/dev) - core: session/move.ts, runner/index.ts + runner/llm.ts (DrainResult.Moved, move delivered at a safe step boundary), execution.ts (re-drain at the new Location), inbox.ts (moveIDs), websearch.ts (forget affinity on move) - core tests: session-move, session-runner move scenarios, subagent-lifetime, websearch - schema: session-inbox.ts (move item no longer "replay-only") - plugin-browser connection.ts (close on session.moved), sdk `sessions.move` + its test - app: move-based recovery on the missing-location screen, en.ts strings, `session.moved` activity, and the two e2e specs - gui-plugins details menu ("Move session to") and its i18n strings - tui: dialog-open (keeps moved sessions in the open list), location-missing recovery by moving, storybook story, working-directory actions, tests for dialog-open and config - script/drive/subagent-eviction.ts Merged by hand (move added back next to the fork's roots code) - schema/session-event.ts: `Moved` is back in `Definitions` (a public event again); event-manifest test matches upstream - core/session.ts: `move: moves.move` and SessionMove.node next to addRoot/removeRoot and SessionDestination. destination.ts now imports the three Destination*Error classes from move.ts instead of defining duplicates with the same tags. - tool/plugin/opencode.ts: upstream's `session_move` tool definition is back next to `session_root_add` / `session_root_remove`. The system hint still points models at roots. - protocol + server: `POST /api/session/:id/move` and its handler are back next to the root endpoints. Regenerated protocol openapi.json and the client (move and root methods both present). - plugin API (effect/promise SessionDomain, promise adapter), plugin host, test host: `move` restored next to `root` - client/solid/data.ts: upstream's `session.moved` reducer restored next to the root reducers - tui: `session.moved` handlers in app.tsx and session-tabs.tsx, "Manage workspaces" command and shell `cd` moving an existing session (prompt/index.tsx), `session.move` keybind (v2 and v1 map), app-lifecycle and session-tabs tests - tui workspaces dialog: `ctrl+m` moves again (`dialog.move_session.move`); adding a root moves to `ctrl+y` (`dialog.workspaces.add_root`). prompt-move.test is upstream's plus a ctrl+y add-root test. data.test keeps the roots test and gets upstream's moved test back. keybinds.mdx lists both. Disabled for models only - New fork plugin `custom.tool.session-move.disable` (plugin/custom/disable-session-move.ts) removes the `opencode_session_move` Code Mode tool. Custom plugins load after the builtin opencode tools, so the removal wins. Users can still move sessions from the TUI, app, API and SDK. A config permission rule would also work, but a user rule could override it; this plugin cannot be overridden by config. - Test in tool-opencode.test.ts: the tool exists after the opencode tools load, is gone after the plugin loads, and session_root_add stays. make tests - Added session-move.test.ts and tool-opencode.test.ts to CORE_FORK_TESTS. - `make tests` now runs each package's `test` script instead of bare `bun test`. For core that is script/test.ts, which gives tests a temporary HOME. Bare `bun test` loaded the real ~/.config/opencode and ~/.claude skills, and with the fork's custom plugins active that extra background work sometimes queried a test database after it had closed ("RangeError: Cannot use a closed database"), failing a random Session.move test. The same pair of files passed 5/5 with the isolated script and failed about half the time without it. Docs - PATCH.md: "Session Roots (Move Removed)" becomes "Session Roots", describes move as upstream's and unchanged plus the ctrl+m / ctrl+y keys, and adds "Session Move Disabled for Models". - tools.mdx keeps describing the root tools, since models in this fork cannot see session_move. Not changed on purpose - `session.fork({ directory })` stays (fork feature, additive). - The GUI details menu now moves again; it no longer adds roots. Roots are still available from the TUI, Code Mode tools, API, client and plugin APIs. Verification - bun turbo typecheck: 35/35 packages - oxlint: 0 errors (its unrelated --fix rewrites were reverted) - bun script/migration.ts --check: no schema changes - make tests: tui 186, core 446, schema 7, sdk 3 pass, 0 fail (core list run 2 more times: 446/0 each)
1 parent 5898181 commit 58fe982

124 files changed

Lines changed: 2317 additions & 230 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Makefile‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ TUI_FORK_TESTS := test/ui/directory-link.test.ts test/ui/directory-markdown.test
99
CORE_FORK_TESTS := test/plugin/custom.test.ts test/plugin/supervisor.test.ts test/tool-cancel.test.ts \
1010
test/session-create.test.ts test/session-roots.test.ts test/session-skill.test.ts test/session-generate.test.ts \
1111
test/session-owned.test.ts test/session-runner.test.ts test/tool-skill.test.ts test/file-access.test.ts \
12-
test/bus-session-routing.test.ts test/subagent-lifetime.test.ts test/websearch.test.ts test/v1-migration.test.ts
12+
test/bus-session-routing.test.ts test/subagent-lifetime.test.ts test/websearch.test.ts test/v1-migration.test.ts \
13+
test/session-move.test.ts test/tool-opencode.test.ts
1314
SCHEMA_FORK_TESTS := test/event-manifest.test.ts
1415
SDK_FORK_TESTS := test/instances-effect.test.ts
1516

@@ -23,10 +24,10 @@ build-single: typecheck
2324
$(BUN) run --cwd packages/cli build --single
2425

2526
tests: install-deps
26-
cd packages/tui && $(BUN) test $(TUI_FORK_TESTS)
27-
cd packages/core && $(BUN) test $(CORE_FORK_TESTS)
28-
cd packages/schema && $(BUN) test $(SCHEMA_FORK_TESTS)
29-
cd packages/sdk && $(BUN) test $(SDK_FORK_TESTS)
27+
cd packages/tui && $(BUN) run test $(TUI_FORK_TESTS)
28+
cd packages/core && $(BUN) run test $(CORE_FORK_TESTS)
29+
cd packages/schema && $(BUN) run test $(SCHEMA_FORK_TESTS)
30+
cd packages/sdk && $(BUN) run test $(SDK_FORK_TESTS)
3031

3132
generate: install-deps
3233
./script/generate.ts

‎PATCH.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,19 @@ Plugin (`custom.pull-requests` + TUI `custom.sidebar.pull-requests`): the sideba
137137

138138
`cancel` tool: agents cancel a background `shell` or `subagent` they started by passing the shell ID or subagent `sessionID` from its `background: true` result (which now names the id); jobs owned by another session are refused.
139139

140-
## Session Roots (Move Removed)
140+
## Session Roots
141141

142142
Sessions keep one working directory (`location`) plus optional `roots`: extra directories that are in scope like the working directory, so reading and editing inside them skips `external_directory` approval. Add and remove roots with `session_root_add` / `session_root_remove` (Code Mode `opencode` namespace), `POST`/`DELETE /api/session/:sessionID/root`, or `session.root.add` / `session.root.remove` on the client and plugin APIs. Roots are event-sourced (`session.root.added` / `session.root.removed`), inherited by child sessions and forks, and listed in the system prompt; commands still run in the working directory.
143143

144144
Roots are first-class context. `SessionRoots` (Location-scoped) discovers each root's instruction files (walked up to the root's project root, minus files the primary Location already loads and global config files) and skills (`.opencode/skill(s)`, `.claude/skills`, `.agents/skills`). `SessionContext.select` adds the root instructions as one `core/roots` instruction source and merges root skills into the available-skills list. The skill tool, `/skill` mentions, and `Session.skill` resolve skills through `SessionRoots.skill`: the primary Location wins an ID collision, then earlier roots. Discovery is not watched; it re-reads the filesystem on each step. A root's own `opencode.json` (config, `instructions`, `skills` entries, plugins) is not loaded.
145145

146-
Moving a session is gone: the `session_move` tool, `/api/session/:sessionID/move`, the `SessionMove` service, and every UI entry point. `session.moved` and inbox `move` items stay decodable for old logs; the runner consumes legacy move items as no-ops. A session whose directory is missing is recovered by forking it into another directory (`session.fork` takes an optional `directory`).
146+
Session move is upstream's and unchanged: the `SessionMove` service, `/api/session/:sessionID/move`, the TUI and app entry points, and missing-directory recovery by moving. Roots and move are independent: a move changes `location` and keeps `roots`. `session.fork` also takes an optional `directory` that places the fork in another directory.
147+
148+
In the TUI workspaces dialog, `ctrl+m` moves the session and `ctrl+y` (`dialog.workspaces.add_root`) adds the selected directory as a root.
149+
150+
## Session Move Disabled for Models
151+
152+
Plugin (`custom.tool.session-move.disable`): removes the `opencode_session_move` Code Mode tool after the builtin opencode tools register, so models cannot move a session. Users can still move sessions from the TUI, app, API, and SDK. The tool definition in `tool/plugin/opencode.ts` stays as upstream has it.
147153

148154
## Instruction Filenames
149155

‎packages/app/e2e/regression/session-location-sync.spec.ts‎

Lines changed: 258 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,262 @@ for (const endpoint of ["/api/location", "/api/agent"]) {
6666
}
6767
}
6868

69+
test("replaces the composer when the location is not found and recovers by moving to another worktree", async ({
70+
page,
71+
}) => {
72+
const directory = "/projects/deleted-worktree"
73+
const destination = "/projects/existing-worktree"
74+
const sessionID = "ses_location_not_found"
75+
const session = { id: sessionID, projectID: fixture.project.id, directory, title: "Missing location" }
76+
const moves: unknown[] = []
77+
const transport = await installSseTransport(page, { server: fixture.serverKey })
78+
await mockOpenCodeServer(page, {
79+
directory: fixture.directory,
80+
project: fixture.project,
81+
provider: fixture.provider,
82+
sessions: [session],
83+
fileList: () => [],
84+
worktrees: [{ directory: fixture.directory }, { directory: destination, strategy: "git" }],
85+
pageMessages: () => ({
86+
items: [{ id: "msg_saved", type: "user", text: "Keep this session history", time: { created: 1 } }],
87+
}),
88+
})
89+
let requests = 0
90+
await page.route("**/api/location?**", (route) => {
91+
if (new URL(route.request().url()).searchParams.get("location[directory]") !== directory) return route.fallback()
92+
requests++
93+
94+
return route.fulfill({
95+
status: 404,
96+
// SAFETY: the server's wire body for a missing Location, which the client decodes into its own error.
97+
// oxlint-disable-next-line anti-slop-effect/no-manual-tagged-construction -- see SAFETY above
98+
json: { _tag: "LocationNotFoundError", location: { directory }, message: `Location not found: ${directory}` },
99+
headers: { "access-control-allow-origin": "*" },
100+
})
101+
})
102+
await page.route(`**/api/session/${sessionID}/move`, (route) => {
103+
if (route.request().method() !== "POST") return route.fallback()
104+
moves.push(route.request().postDataJSON())
105+
106+
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
107+
})
108+
109+
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
110+
const status = page.getByRole("group", { name: "Session location unavailable", exact: true })
111+
await expect(status).toHaveAccessibleDescription(`${directory} Choose another directory to continue this session.`)
112+
await expect(status).toContainText(directory)
113+
await expect(page.getByText("Keep this session history", { exact: true })).toBeVisible()
114+
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toHaveCount(0)
115+
// A typed not-found answer is final; retrying cannot succeed until the folder changes.
116+
expect(requests).toBe(1)
117+
118+
await transport.waitForConnection()
119+
await page.getByRole("button", { name: "Choose worktree", exact: true }).click()
120+
await page.getByRole("menuitem", { name: "existing-worktree", exact: true }).click()
121+
await expect.poll(() => moves).toEqual([{ directory: destination }])
122+
123+
session.directory = destination
124+
await transport.send({
125+
id: "evt_location_not_found_moved",
126+
type: "session.moved",
127+
created: 2,
128+
durable: { aggregateID: sessionID, seq: 1, version: 1 },
129+
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
130+
})
131+
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
132+
await expect(status).toHaveCount(0)
133+
await expect(page.getByText("Keep this session history", { exact: true })).toBeVisible()
134+
})
135+
136+
test("follows a live session move while the agent catalog is still loading", async ({ page }) => {
137+
const recovery = recoveryRequests(page)
138+
const directory = "/projects/old-tree"
139+
const destination = "/projects/current-tree"
140+
const sessionID = "ses_location_moved_while_loading"
141+
const session = { id: sessionID, projectID: fixture.project.id, directory, title: "Moved session" }
142+
const requested = Promise.withResolvers<void>()
143+
const release = Promise.withResolvers<void>()
144+
await page.addInitScript(() => {
145+
localStorage.setItem(
146+
"settings.v3",
147+
JSON.stringify({ general: { timelineDetail: { notices: { placement: "separate" } } } }),
148+
)
149+
})
150+
const transport = await installSseTransport(page, { server: fixture.serverKey })
151+
await mockOpenCodeServer(page, {
152+
directory: fixture.directory,
153+
project: fixture.project,
154+
provider: fixture.provider,
155+
sessions: [session],
156+
fileList: () => [],
157+
pageMessages: () => ({
158+
items: [{ id: "msg_saved", type: "user", text: "Follow the session move", time: { created: 1 } }],
159+
}),
160+
})
161+
await page.route("**/api/**", async (route) => {
162+
const url = new URL(route.request().url())
163+
164+
if (url.pathname === "/api/agent" && url.searchParams.get("location[directory]") === directory) {
165+
requested.resolve()
166+
await release.promise
167+
}
168+
169+
return route.fallback()
170+
})
171+
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
172+
await requested.promise
173+
const prompt = page.getByRole("textbox", { name: "Prompt", exact: true })
174+
await expect(prompt).toBeEditable()
175+
await prompt.fill("Keep this draft")
176+
await expect(page.getByText("Session location unavailable", { exact: true })).toHaveCount(0)
177+
await transport.waitForConnection()
178+
179+
const resolved = page.waitForResponse((response) => {
180+
const url = new URL(response.url())
181+
182+
return url.pathname === "/api/agent" && url.searchParams.get("location[directory]") === destination && response.ok()
183+
})
184+
185+
session.directory = destination
186+
await transport.send({
187+
id: "evt_location_moved_while_loading",
188+
type: "session.moved",
189+
created: 2,
190+
durable: { aggregateID: sessionID, seq: 1, version: 1 },
191+
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
192+
})
193+
await resolved
194+
195+
const delayed = page.waitForResponse((response) => {
196+
const url = new URL(response.url())
197+
198+
return url.pathname === "/api/agent" && url.searchParams.get("location[directory]") === directory && response.ok()
199+
})
200+
201+
release.resolve()
202+
await delayed
203+
await expect(prompt).toBeEditable()
204+
await expect(prompt).toHaveText("Keep this draft")
205+
await expect(page.getByText("Follow the session move", { exact: true })).toBeVisible()
206+
await expect(page.locator('[data-type="location-switched"]').getByText(destination, { exact: true })).toBeVisible()
207+
await expect(page.getByText("Session location unavailable", { exact: true })).toHaveCount(0)
208+
await expect(page.getByRole("button", { name: "Choose directory", exact: true })).toHaveCount(0)
209+
expect(recovery).toEqual([])
210+
})
211+
212+
test("keeps the side panel's tabs and selection when the session moves", async ({ page }) => {
213+
const directory = "/projects/tabs-before-move"
214+
const destination = "/projects/tabs-after-move"
215+
const sessionID = "ses_location_moved_tabs"
216+
const session = { id: sessionID, projectID: fixture.project.id, directory, title: "Moved tabs" }
217+
const transport = await installSseTransport(page, { server: fixture.serverKey })
218+
await mockOpenCodeServer(page, {
219+
directory: fixture.directory,
220+
project: fixture.project,
221+
provider: fixture.provider,
222+
sessions: [session],
223+
fileList: () => [],
224+
findFiles: (input) => ["README.md", "NOTES.md"].filter((name) => name.startsWith(input.query)),
225+
fileContent: (path) => ({ type: "text", content: `contents:${session.directory}:${path}` }),
226+
pageMessages: () => ({
227+
items: [{ id: "msg_saved", type: "user", text: "Keep my tabs", time: { created: 1 } }],
228+
}),
229+
})
230+
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
231+
await expect(page.getByRole("textbox", { name: "Prompt", exact: true })).toBeEditable()
232+
233+
const panel = page.getByRole("complementary", { name: "Review and files" })
234+
const files = panel.getByRole("tab", { name: /\.md$/ })
235+
const notes = panel.getByRole("tab", { name: "NOTES.md", exact: true })
236+
237+
// A lost selection would fall back to the first file tab, README.md, so NOTES.md stays selected only if it carries.
238+
for (const name of ["README", "NOTES"]) {
239+
await page.keyboard.press("ControlOrMeta+Shift+P")
240+
241+
const dialog = page.getByRole("dialog")
242+
const search = dialog.getByRole("combobox")
243+
await search.fill(name)
244+
await expect(dialog.getByRole("option", { name: `/ ${name}.md`, exact: true })).toHaveAttribute(
245+
"aria-selected",
246+
"true",
247+
)
248+
await search.press("Enter")
249+
await expect(dialog).toHaveCount(0)
250+
await expect(panel.getByRole("tab", { name: `${name}.md`, exact: true })).toHaveAttribute("aria-selected", "true")
251+
}
252+
253+
await expect(files).toHaveText(["README.md", "NOTES.md"])
254+
await expect(panel.getByText(`contents:${directory}:NOTES.md`, { exact: true })).toBeVisible()
255+
256+
await transport.waitForConnection()
257+
258+
const resolved = page.waitForResponse((response) => {
259+
const url = new URL(response.url())
260+
261+
return (
262+
url.pathname === "/api/location" && url.searchParams.get("location[directory]") === destination && response.ok()
263+
)
264+
})
265+
266+
session.directory = destination
267+
await transport.send({
268+
id: "evt_location_moved_tabs",
269+
type: "session.moved",
270+
created: 2,
271+
durable: { aggregateID: sessionID, seq: 1, version: 1 },
272+
data: { sessionID, location: { directory: destination }, projectID: fixture.project.id },
273+
})
274+
await resolved
275+
await expect(page.locator('[data-type="location-switched"]').getByText(destination, { exact: true })).toBeVisible()
276+
await expect(files).toHaveText(["README.md", "NOTES.md"])
277+
await expect(notes).toHaveAttribute("aria-selected", "true")
278+
// The carried tab shows the file in the new directory.
279+
await expect(panel.getByText(`contents:${destination}:NOTES.md`, { exact: true })).toBeVisible()
280+
})
281+
282+
test("refreshes a session moved during disconnection without losing the draft", async ({ page }) => {
283+
const recovery = recoveryRequests(page)
284+
const directory = "/projects/before-reconnect"
285+
const destination = "/projects/after-reconnect"
286+
const sessionID = "ses_location_reconnect"
287+
const session = { id: sessionID, projectID: fixture.project.id, directory, title: "Reconnected session" }
288+
const transport = await installSseTransport(page, { server: fixture.serverKey })
289+
await mockOpenCodeServer(page, {
290+
directory: fixture.directory,
291+
project: fixture.project,
292+
provider: fixture.provider,
293+
sessions: [session],
294+
fileList: () => [],
295+
pageMessages: () => ({
296+
items: [{ id: "msg_saved", type: "user", text: "Resume in the current worktree", time: { created: 1 } }],
297+
}),
298+
})
299+
await page.goto(`/server/${base64Encode(fixture.serverKey)}/session/${sessionID}`)
300+
const prompt = page.getByRole("textbox", { name: "Prompt", exact: true })
301+
await expect(prompt).toBeEditable()
302+
await prompt.fill("Draft before disconnect")
303+
const connection = await transport.waitForConnection()
304+
305+
const resolved = page.waitForResponse((response) => {
306+
const url = new URL(response.url())
307+
308+
return (
309+
url.pathname === "/api/location" && url.searchParams.get("location[directory]") === destination && response.ok()
310+
)
311+
})
312+
313+
session.directory = destination
314+
await transport.close()
315+
await transport.waitForConnection({ after: connection.id })
316+
await resolved
317+
await expect(prompt).toBeEditable()
318+
await expect(prompt).toHaveText("Draft before disconnect")
319+
await expect(page.getByText("Resume in the current worktree", { exact: true })).toBeVisible()
320+
await expect(page.getByText("Session location unavailable", { exact: true })).toHaveCount(0)
321+
await expect(page.getByRole("button", { name: "Choose directory", exact: true })).toHaveCount(0)
322+
expect(recovery).toEqual([])
323+
})
324+
69325
test("ignores an old failed location read after reconnecting", async ({ page }) => {
70326
const recovery = recoveryRequests(page)
71327
const directory = "/projects/reconnected-tree"
@@ -125,14 +381,14 @@ test("ignores an old failed location read after reconnecting", async ({ page })
125381
expect(recovery).toEqual([])
126382
})
127383

128-
// Creating a worktree is recovery. Showing a resolved Location also refreshes its project's
384+
// Relocation: moving the session or creating a worktree. Showing a resolved Location also refreshes its project's
129385
// worktree inventory (`/api/worktree/refresh`), which is routine discovery, not recovery.
130386
function recoveryRequests(page: Page) {
131387
const requests: string[] = []
132388
page.on("request", (request) => {
133389
const path = new URL(request.url()).pathname
134390

135-
if (request.method() === "POST" && path === "/api/worktree") requests.push(path)
391+
if (request.method() === "POST" && /^\/api\/(session\/[^/]+\/move|worktree)$/.test(path)) requests.push(path)
136392
})
137393

138394
return requests

‎packages/app/e2e/regression/session-summary.spec.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,50 @@ test("a subagent shows its details but cannot move to another worktree", async (
218218
await expect(location.getByRole("menuitem", { name: "feature", exact: true })).toBeDisabled()
219219
})
220220

221+
test("the details follow the routed session when it moves to another worktree", async ({ page }) => {
222+
const root = "C:/OpenCode/SmokeWorktrees"
223+
224+
const sessions = fixture.sessions.map((item) =>
225+
item.id === fixture.targetID ? { ...item, directory: `${root}/feature` } : { ...item },
226+
)
227+
228+
const mock = await mockStressTimeline(page, {
229+
sessions,
230+
worktrees: [
231+
{ directory: fixture.directory },
232+
{ directory: `${root}/feature`, strategy: "git" },
233+
{ directory: `${root}/other`, strategy: "git" },
234+
],
235+
})
236+
237+
await page.goto(sessionHref(fixture.targetID))
238+
const trigger = page.locator('[data-slot="session-location-trigger"]')
239+
const summary = page.locator('[data-slot="session-location-card"]')
240+
const location = (name: string) => summary.getByText(name, { exact: true })
241+
await trigger.click()
242+
await expect(location("feature")).toBeVisible()
243+
await page.keyboard.press("Escape")
244+
await expect(summary).toBeHidden()
245+
246+
// The server moves the session; the screen stays mounted while its plugins read the new worktree.
247+
const moved = sessions.find((item) => item.id === fixture.targetID)
248+
249+
if (moved) moved.directory = `${root}/other`
250+
await mock.push([
251+
{
252+
id: "evt_details_session_moved",
253+
type: "session.moved",
254+
created: 2,
255+
durable: { aggregateID: fixture.targetID, seq: 1, version: 1 },
256+
data: { sessionID: fixture.targetID, location: { directory: `${root}/other` }, projectID: fixture.project.id },
257+
},
258+
])
259+
await expect(trigger.locator('[data-slot="session-location-worktree"]')).toHaveText("other")
260+
await trigger.click()
261+
await expect(location("other")).toBeVisible()
262+
await expect(location("feature")).toHaveCount(0)
263+
})
264+
221265
for (const direction of ["ltr", "rtl"] as const) {
222266
test(`summary overlays the view and submenus follow ${direction}`, async ({ page }) => {
223267
await page.setViewportSize({ width: 1440, height: 900 })

‎packages/app/src/runtime/i18n/en.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ export const dict = {
2020
"voice.failure.connection": "Couldn’t connect the call.",
2121
...DESKTOP_NATIVE_ENGLISH,
2222
"session.location.unavailable": "Session location unavailable",
23-
"session.location.description": "Fork this session into another directory to continue.",
23+
"session.location.description": "Choose another directory to continue this session.",
2424
"session.location.choose": "Choose directory",
2525
"session.location.worktree": "Choose worktree",
2626
"session.location.worktreesFailed": "Failed to load worktrees",
2727
"session.rewind.toast.title": "Rewound",
2828
"session.rewind.toast.undo": "Undo",
29-
"session.location.moving": "Forking session…",
30-
"session.location.moveFailed": "Failed to fork session",
29+
"session.location.moving": "Moving session…",
30+
"session.location.moveFailed": "Failed to move session",
3131
"server.action.authenticate": "Authenticate",
3232
"server.status.connecting": "Connecting over SSH…",
3333
"server.thisComputer": "This computer",

0 commit comments

Comments
 (0)