Repository navigation
Fix split-view sending and prioritize New session - #4491
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
3 Skipped Deployments
|
Pull request was closed
There was a problem hiding this comment.
Warden security clearance: clear. No blocking findings (security or desktop↔den sync) in this diff (18ec83ec816c06cd930fecf355e30aca65eed522). Automated clearance satisfies the required-review gate only — a human still reviews and merges. Analysis run
There was a problem hiding this comment.
Warden security clearance: clear. No blocking findings (security or desktop↔den sync) in this diff (bb65500b47ac4e7f18e133e5123c602d3ebfb7ab). Automated clearance satisfies the required-review gate only — a human still reviews and merges. Analysis run
Test evidence — new-split-creates-fresh-same-workspace-secondary-sessions-without-moving-the-pri — ✅ passedSHA bb65500 · engine v1 ✅ 0/2 screenshots passed · 4 assertions · 4 expectations passed · 0 failed · 0 pending [world] mock(mcp) · den(daytona) · desktop(as admin) · workspace(/tmp/openwork-new-split-session-1788618291701) · [seed:raw] evalIn() ×3 · session("Split switch target") · session("New split primary") ℹ️ ASSERTION — 1. Switching sessions during a pending send does not block the new session or cross replies{"pendingSessionId":"ses_f8e0aa614ffeQI0kJY9XWHnERH","switchedSessionId":"ses_f8e0ab001ffejrTYyNaNnbx5WP"}
ℹ️ ASSERTION — 2. Both split composers send and receive replies without crossing sessions{"primarySessionId":"ses_f8e0aa614ffeQI0kJY9XWHnERH","secondarySessionId":"ses_f8e0a8c62ffejxDSBl10jyi17y"}
ℹ️ ASSERTION — 3. Desktop split context reaches the agent through the server mailboxThe server returned ok=true with kind=split, primarySessionId=ses_f8e0aa614ffeQI0kJY9XWHnERH, secondarySessionId=ses_f8e0a665effe84wcK4KqzwlEDy, and focused=secondary, all matching the rendered split.
ℹ️ ASSERTION — 4. New splits preserve the primary, and New session replaces only the focused paneContext-menu and command-palette splits each created one distinct same-workspace secondary session. The focused-secondary and focused-primary New session actions each preserved the opposite pane and created exactly one session.
⚪ UNVALIDATED — 5. new-split-creates-fresh-same-workspace-secondary-sessions-without-moving-the-pri artifact 1
⚪ UNVALIDATED — 6. new-split-creates-fresh-same-workspace-secondary-sessions-without-moving-the-pri artifact 4
Test run created 2026-09-05T14:21:08.438Z · Source: |
- Isolate the standalone approval server's runtime DB so a host machine's persisted managed policy cannot short-circuit writes with 403. - Target the sidebar control by its current label (renamed in #4491).
…loads (#4757) * fix(app): seed a created session's snapshot so its composer can send immediately A freshly created session had no cached snapshot, so deriveSessionRenderModel reported "switching" while the first snapshot round trip ran and the composer disabled Run task. On Daytona that window is ~0.9 s and a click or Enter inside it was silently dropped with the draft intact. Seed the snapshot cache from the create response (stale, so the real fetch still runs as a background refresh). * fix(app): let a send that settled with a server failure recover instead of wedging A prompt_async answered with 5xx/408 is an uncertain admission (#4635), but the hold it created could never end: the acceptance check treated absence as no evidence, Stop refused while the admission was unknown, and the failure body was dropped, so the person saw a paused composer with no explanation. - Keep the settled response body on PromptAdmissionUnknownError and show it as the session error card right away; the hold still prevents any resend. - readPromptAdmission: after the exact-message GET misses, list the idle conversation; when native lists it without the message the POST is settled and did not admit, so the drain receives admission_rejected and halts like any definite failure (explicit retry only, never an automatic resend). - On release the pending row is removed and the submitted composer is kept as an unsent message to restore; on confirmed acceptance the stale failure card is cleared. * test(evals): read an empty Lexical composer as an empty draft Lexical keeps one empty paragraph in an empty editor, which innerText reports as "\n". readComposerState now drops that trailing line break so draftText matches what the person sees; user.see("composer", { text }) already trimmed. * test(evals): open the fresh conversation through the sidebar's New session button The sidebar button was renamed from "New task" to "New session" in #4491 (Sep 5) and the "new" scenario added in #4631 (Sep 8) still targeted the old label, so it could never find the button. Same user action, current label.
…ollow-up (#4786) * fix(app): seed the delegated-task eval with a running parent thread The eval.task_activity.seed hook rendered an in-flight child under a parent whose thread status stayed "ready": it only marked the activity store busy. Since #4635 MessageList reads that as a stopped parent and the unobserved child shows "Waiting for task result" instead of the Working shimmer, so task-activity-shimmer and parent-child-permission-approval lost their [data-subagent-activity="shimmer"] row. A delegating parent is mid-run; mark the eval thread streaming like the chat-loading seed already does. * fix(app): seed a created session's snapshot so its composer can send immediately A freshly created session had no cached snapshot, so deriveSessionRenderModel reported "switching" while the first snapshot round trip ran and the composer disabled Run task. On Daytona that window is ~0.9 s and a click or Enter inside it was silently dropped with the draft intact. Seed the snapshot cache from the create response (stale, so the real fetch still runs as a background refresh). * fix(app): let a send that settled with a server failure recover instead of wedging A prompt_async answered with 5xx/408 is an uncertain admission (#4635), but the hold it created could never end: the acceptance check treated absence as no evidence, Stop refused while the admission was unknown, and the failure body was dropped, so the person saw a paused composer with no explanation. - Keep the settled response body on PromptAdmissionUnknownError and show it as the session error card right away; the hold still prevents any resend. - readPromptAdmission: after the exact-message GET misses, list the idle conversation; when native lists it without the message the POST is settled and did not admit, so the drain receives admission_rejected and halts like any definite failure (explicit retry only, never an automatic resend). - On release the pending row is removed and the submitted composer is kept as an unsent message to restore; on confirmed acceptance the stale failure card is cleared. * test(evals): read an empty Lexical composer as an empty draft Lexical keeps one empty paragraph in an empty editor, which innerText reports as "\n". readComposerState now drops that trailing line break so draftText matches what the person sees; user.see("composer", { text }) already trimmed. * test(evals): open the fresh conversation through the sidebar's New session button The sidebar button was renamed from "New task" to "New session" in #4491 (Sep 5) and the "new" scenario added in #4631 (Sep 8) still targeted the old label, so it could never find the button. Same user action, current label. * fix(app): re-read pending questions when a session reaches a terminal edge An interrupted question tool leaves the engine's pending list through its finalizer without a question.rejected event, so Stop on a delegated child left its question (and option buttons) in the parent view. Reconcile questions alongside permissions on terminal edges, for both engines. * Revert "fix(app): re-read pending questions when a session reaches a terminal edge" This reverts commit 9e70982. * fix(app): withdraw the stopped tree's unanswered questions after Stop OpenCode v1 runs tools on detached fibers: aborting a session marks the question tool part as aborted but leaves its request in GET /question, so a parent that stopped a delegated child kept showing the child's question and its option buttons. After Stop confirms the tree idle, reject the pending questions owned by the stopped sessions; other roots are left untouched.
… person's tab, not agent browser control (#4779) * fix(app): seed a created session's snapshot so its composer can send immediately A freshly created session had no cached snapshot, so deriveSessionRenderModel reported "switching" while the first snapshot round trip ran and the composer disabled Run task. On Daytona that window is ~0.9 s and a click or Enter inside it was silently dropped with the draft intact. Seed the snapshot cache from the create response (stale, so the real fetch still runs as a background refresh). * fix(app): let a send that settled with a server failure recover instead of wedging A prompt_async answered with 5xx/408 is an uncertain admission (#4635), but the hold it created could never end: the acceptance check treated absence as no evidence, Stop refused while the admission was unknown, and the failure body was dropped, so the person saw a paused composer with no explanation. - Keep the settled response body on PromptAdmissionUnknownError and show it as the session error card right away; the hold still prevents any resend. - readPromptAdmission: after the exact-message GET misses, list the idle conversation; when native lists it without the message the POST is settled and did not admit, so the drain receives admission_rejected and halts like any definite failure (explicit retry only, never an automatic resend). - On release the pending row is removed and the submitted composer is kept as an unsent message to restore; on confirmed acceptance the stale failure card is cleared. * test(evals): read an empty Lexical composer as an empty draft Lexical keeps one empty paragraph in an empty editor, which innerText reports as "\n". readComposerState now drops that trailing line break so draftText matches what the person sees; user.see("composer", { text }) already trimmed. * test(evals): open the fresh conversation through the sidebar's New session button The sidebar button was renamed from "New task" to "New session" in #4491 (Sep 5) and the "new" scenario added in #4631 (Sep 8) still targeted the old label, so it could never find the button. Same user action, current label. * test(evals): open the saved-app verification return tab as a person's new tab returnVerification drove the agent browser entry point (browser.openUrl, the IPC behind browser.open_url) with no requesting conversation. Since #4434 the task host refuses that, and it never accepted an about:blank destination. The tab only stands in for the person's own browser so the openwork:// return link can be navigated without OS protocol registration, so create it the way a person opens a new tab and find its CDP target by diffing the page targets.
…re the spec acts on it (#4794) * fix(app): seed a created session's snapshot so its composer can send immediately A freshly created session had no cached snapshot, so deriveSessionRenderModel reported "switching" while the first snapshot round trip ran and the composer disabled Run task. On Daytona that window is ~0.9 s and a click or Enter inside it was silently dropped with the draft intact. Seed the snapshot cache from the create response (stale, so the real fetch still runs as a background refresh). * fix(app): let a send that settled with a server failure recover instead of wedging A prompt_async answered with 5xx/408 is an uncertain admission (#4635), but the hold it created could never end: the acceptance check treated absence as no evidence, Stop refused while the admission was unknown, and the failure body was dropped, so the person saw a paused composer with no explanation. - Keep the settled response body on PromptAdmissionUnknownError and show it as the session error card right away; the hold still prevents any resend. - readPromptAdmission: after the exact-message GET misses, list the idle conversation; when native lists it without the message the POST is settled and did not admit, so the drain receives admission_rejected and halts like any definite failure (explicit retry only, never an automatic resend). - On release the pending row is removed and the submitted composer is kept as an unsent message to restore; on confirmed acceptance the stale failure card is cleared. * test(evals): read an empty Lexical composer as an empty draft Lexical keeps one empty paragraph in an empty editor, which innerText reports as "\n". readComposerState now drops that trailing line break so draftText matches what the person sees; user.see("composer", { text }) already trimmed. * test(evals): open the fresh conversation through the sidebar's New session button The sidebar button was renamed from "New task" to "New session" in #4491 (Sep 5) and the "new" scenario added in #4631 (Sep 8) still targeted the old label, so it could never find the button. Same user action, current label. * test(evals): open the saved-app verification return tab as a person's new tab returnVerification drove the agent browser entry point (browser.openUrl, the IPC behind browser.open_url) with no requesting conversation. Since #4434 the task host refuses that, and it never accepted an about:blank destination. The tab only stands in for the person's own browser so the openwork:// return link can be navigated without OS protocol registration, so create it the way a person opens a new tab and find its CDP target by diffing the page targets. * test(evals): temporary Z2 diagnostics around dashboard delete route change * test(evals): saved-apps world.open returns once the destination page root has rendered * test(evals): remove temporary Z2 diagnostics from saved-app-creation
…-status flip on reload (#4797) * fix(app): seed a created session's snapshot so its composer can send immediately A freshly created session had no cached snapshot, so deriveSessionRenderModel reported "switching" while the first snapshot round trip ran and the composer disabled Run task. On Daytona that window is ~0.9 s and a click or Enter inside it was silently dropped with the draft intact. Seed the snapshot cache from the create response (stale, so the real fetch still runs as a background refresh). * fix(app): let a send that settled with a server failure recover instead of wedging A prompt_async answered with 5xx/408 is an uncertain admission (#4635), but the hold it created could never end: the acceptance check treated absence as no evidence, Stop refused while the admission was unknown, and the failure body was dropped, so the person saw a paused composer with no explanation. - Keep the settled response body on PromptAdmissionUnknownError and show it as the session error card right away; the hold still prevents any resend. - readPromptAdmission: after the exact-message GET misses, list the idle conversation; when native lists it without the message the POST is settled and did not admit, so the drain receives admission_rejected and halts like any definite failure (explicit retry only, never an automatic resend). - On release the pending row is removed and the submitted composer is kept as an unsent message to restore; on confirmed acceptance the stale failure card is cleared. * test(evals): read an empty Lexical composer as an empty draft Lexical keeps one empty paragraph in an empty editor, which innerText reports as "\n". readComposerState now drops that trailing line break so draftText matches what the person sees; user.see("composer", { text }) already trimmed. * test(evals): open the fresh conversation through the sidebar's New session button The sidebar button was renamed from "New task" to "New session" in #4491 (Sep 5) and the "new" scenario added in #4631 (Sep 8) still targeted the old label, so it could never find the button. Same user action, current label. * test(evals): open sidebar New session as the empty composer in workspace-engine-upgrade Since #4577 the sidebar New session controls open the workspace's empty New task composer at once and the session is created by the first send, so waiting for a ses_ route on click never resolves. Re-express the three sidebar steps: the click lands on the empty New task route without a session surface and a prompt-ready composer; the send must land in a session no earlier turn used (same claim shape as bench-openwork-app-v1 in #4738). Palette steps still create their session up front. * fix(app): send the New task prompt under v2 chat routing The empty New task composer creates its session on submit and hands the prompt over through a scoped auto-send mark. The mark was keyed with the workspace's default endpoint URL (always /opencode), while the session surface under OpenCode v2 routing owns the session as /opencode2, so the lookup missed and the prompt silently vanished: the person landed on an empty new session. Key the mark with the endpoint that created the session (createRouteSessionWithEndpoint), which is the URL the surface will own it under. * test(app): reproduce the snapshot owner flip after a v2 session reload The surface mounts under the v1 URL before chat routing resolves; the owned read 404s, parks for its retry, and the routing flip changes the owner while it is in flight. The read rejects with 'Session snapshot owner changed' and the (workspace, session) query never refetches, so the transcript stays empty. * fix(app): re-read a session snapshot when its owning endpoint changes The snapshot query is keyed on (workspace, session) with retry off on the desktop loopback path. When chat routing resolves to /opencode2 after boot (or the draft scope resolves once identity verifies) while the owned read is in flight, the read rejects with 'Session snapshot owner changed' and nothing refetched, leaving the error card over an empty transcript. Cancel the doomed read and refetch under the new owner; a bare invalidate would only join the in-flight promise because the first load has no data yet.
…ed permission requests after Stop (#4798) * fix(app): seed the delegated-task eval with a running parent thread The eval.task_activity.seed hook rendered an in-flight child under a parent whose thread status stayed "ready": it only marked the activity store busy. Since #4635 MessageList reads that as a stopped parent and the unobserved child shows "Waiting for task result" instead of the Working shimmer, so task-activity-shimmer and parent-child-permission-approval lost their [data-subagent-activity="shimmer"] row. A delegating parent is mid-run; mark the eval thread streaming like the chat-loading seed already does. * fix(app): seed a created session's snapshot so its composer can send immediately A freshly created session had no cached snapshot, so deriveSessionRenderModel reported "switching" while the first snapshot round trip ran and the composer disabled Run task. On Daytona that window is ~0.9 s and a click or Enter inside it was silently dropped with the draft intact. Seed the snapshot cache from the create response (stale, so the real fetch still runs as a background refresh). * fix(app): let a send that settled with a server failure recover instead of wedging A prompt_async answered with 5xx/408 is an uncertain admission (#4635), but the hold it created could never end: the acceptance check treated absence as no evidence, Stop refused while the admission was unknown, and the failure body was dropped, so the person saw a paused composer with no explanation. - Keep the settled response body on PromptAdmissionUnknownError and show it as the session error card right away; the hold still prevents any resend. - readPromptAdmission: after the exact-message GET misses, list the idle conversation; when native lists it without the message the POST is settled and did not admit, so the drain receives admission_rejected and halts like any definite failure (explicit retry only, never an automatic resend). - On release the pending row is removed and the submitted composer is kept as an unsent message to restore; on confirmed acceptance the stale failure card is cleared. * test(evals): read an empty Lexical composer as an empty draft Lexical keeps one empty paragraph in an empty editor, which innerText reports as "\n". readComposerState now drops that trailing line break so draftText matches what the person sees; user.see("composer", { text }) already trimmed. * test(evals): open the fresh conversation through the sidebar's New session button The sidebar button was renamed from "New task" to "New session" in #4491 (Sep 5) and the "new" scenario added in #4631 (Sep 8) still targeted the old label, so it could never find the button. Same user action, current label. * test(evals): create the empty-session workspace at its declared tmp path The emptySession world declared /tmp/openwork-empty-session-<ts> but seed.workspace() adopted the existing first-launch default workspace instead ("OpenWork Chat" under the dev profile home). On Daytona that profile lives inside the repo checkout, so the engine merged /workspace/.opencode/opencode.json ("permission": "allow") over the workspace's own catch-all and composer-run-mode never saw the workspace MCP rule take effect. * test(evals): name the server error when a run-mode read is not 200 * fix(app): reuse the running OpenWork server when the selected workspace is server-owned Workspaces created from the app live only in the server registry, so a renderer reload reaches startServerWithoutDesktopWorkspace and restarted the embedded server (and its engine) although it was healthy and already serving that workspace. Reuse it when it is ready with the preferred remote-access mode; only restart when nothing usable is running. * fix(app): re-read pending questions when a session reaches a terminal edge An interrupted question tool leaves the engine's pending list through its finalizer without a question.rejected event, so Stop on a delegated child left its question (and option buttons) in the parent view. Reconcile questions alongside permissions on terminal edges, for both engines. * Revert "fix(app): re-read pending questions when a session reaches a terminal edge" This reverts commit 9e70982. * fix(app): withdraw the stopped tree's unanswered questions after Stop OpenCode v1 runs tools on detached fibers: aborting a session marks the question tool part as aborted but leaves its request in GET /question, so a parent that stopped a delegated child kept showing the child's question and its option buttons. After Stop confirms the tree idle, reject the pending questions owned by the stopped sessions; other roots are left untouched. * test(evals): create the permission-stop-recovery workspace at its declared tmp path Test 2 of parent-child-permission-approval ran bash without asking although the world wrote permission.bash="ask" to the workspace opencode.json. The world called seed.workspace() without create, so it adopted the first-launch default workspace under the dev profile, which on Daytona lives inside the repo checkout; the engine merges /workspace/.opencode/opencode.json ("permission": "allow") after the workspace file and the agent ruleset ends as [* allow, bash ask, * allow] (last match wins; permissions/effective reported shell allow, source engine, workspace file .../home/OpenWork Chat/opencode.jsonc). Same trap and same remedy as #4747 for emptySession; opt-in so the other splitPaneQuestions worlds keep their arrangement. * fix(app): withdraw the stopped tree's unanswered permission requests after Stop OpenCode v1 runs tools on detached fibers: aborting a session marks the tool part as aborted but leaves its Permission.ask pending in /permission, exactly as #4786 found for questions. The stopped tree would keep showing Allow once / Deny in every pane and after reload although nothing can consume the answer. After stopForegroundTree confirms every stopped session idle, also list pending permissions and reject the ones owned by the stopped tree; other roots are never touched and each withdrawal kind is best-effort on its own.
… the composer recovers (attachment-upload-loading-state) (#4811) * fix(app): seed a created session's snapshot so its composer can send immediately A freshly created session had no cached snapshot, so deriveSessionRenderModel reported "switching" while the first snapshot round trip ran and the composer disabled Run task. On Daytona that window is ~0.9 s and a click or Enter inside it was silently dropped with the draft intact. Seed the snapshot cache from the create response (stale, so the real fetch still runs as a background refresh). * fix(app): let a send that settled with a server failure recover instead of wedging A prompt_async answered with 5xx/408 is an uncertain admission (#4635), but the hold it created could never end: the acceptance check treated absence as no evidence, Stop refused while the admission was unknown, and the failure body was dropped, so the person saw a paused composer with no explanation. - Keep the settled response body on PromptAdmissionUnknownError and show it as the session error card right away; the hold still prevents any resend. - readPromptAdmission: after the exact-message GET misses, list the idle conversation; when native lists it without the message the POST is settled and did not admit, so the drain receives admission_rejected and halts like any definite failure (explicit retry only, never an automatic resend). - On release the pending row is removed and the submitted composer is kept as an unsent message to restore; on confirmed acceptance the stale failure card is cleared. * test(evals): read an empty Lexical composer as an empty draft Lexical keeps one empty paragraph in an empty editor, which innerText reports as "\n". readComposerState now drops that trailing line break so draftText matches what the person sees; user.see("composer", { text }) already trimmed. * test(evals): open the fresh conversation through the sidebar's New session button The sidebar button was renamed from "New task" to "New session" in #4491 (Sep 5) and the "new" scenario added in #4631 (Sep 8) still targeted the old label, so it could never find the button. Same user action, current label. * test(evals): open sidebar New session as the empty composer in workspace-engine-upgrade Since #4577 the sidebar New session controls open the workspace's empty New task composer at once and the session is created by the first send, so waiting for a ses_ route on click never resolves. Re-express the three sidebar steps: the click lands on the empty New task route without a session surface and a prompt-ready composer; the send must land in a session no earlier turn used (same claim shape as bench-openwork-app-v1 in #4738). Palette steps still create their session up front. * fix(app): send the New task prompt under v2 chat routing The empty New task composer creates its session on submit and hands the prompt over through a scoped auto-send mark. The mark was keyed with the workspace's default endpoint URL (always /opencode), while the session surface under OpenCode v2 routing owns the session as /opencode2, so the lookup missed and the prompt silently vanished: the person landed on an empty new session. Key the mark with the endpoint that created the session (createRouteSessionWithEndpoint), which is the URL the surface will own it under. * test(app): reproduce the snapshot owner flip after a v2 session reload The surface mounts under the v1 URL before chat routing resolves; the owned read 404s, parks for its retry, and the routing flip changes the owner while it is in flight. The read rejects with 'Session snapshot owner changed' and the (workspace, session) query never refetches, so the transcript stays empty. * fix(app): re-read a session snapshot when its owning endpoint changes The snapshot query is keyed on (workspace, session) with retry off on the desktop loopback path. When chat routing resolves to /opencode2 after boot (or the draft scope resolves once identity verifies) while the owned read is in flight, the read rejects with 'Session snapshot owner changed' and nothing refetched, leaving the error card over an empty transcript. Cancel the doomed read and refetch under the new owner; a bare invalidate would only join the in-flight promise because the first load has no data yet.
… route-commit wait in world open() (#4810) * fix(app): seed the delegated-task eval with a running parent thread The eval.task_activity.seed hook rendered an in-flight child under a parent whose thread status stayed "ready": it only marked the activity store busy. Since #4635 MessageList reads that as a stopped parent and the unobserved child shows "Waiting for task result" instead of the Working shimmer, so task-activity-shimmer and parent-child-permission-approval lost their [data-subagent-activity="shimmer"] row. A delegating parent is mid-run; mark the eval thread streaming like the chat-loading seed already does. * fix(app): seed a created session's snapshot so its composer can send immediately A freshly created session had no cached snapshot, so deriveSessionRenderModel reported "switching" while the first snapshot round trip ran and the composer disabled Run task. On Daytona that window is ~0.9 s and a click or Enter inside it was silently dropped with the draft intact. Seed the snapshot cache from the create response (stale, so the real fetch still runs as a background refresh). * fix(app): let a send that settled with a server failure recover instead of wedging A prompt_async answered with 5xx/408 is an uncertain admission (#4635), but the hold it created could never end: the acceptance check treated absence as no evidence, Stop refused while the admission was unknown, and the failure body was dropped, so the person saw a paused composer with no explanation. - Keep the settled response body on PromptAdmissionUnknownError and show it as the session error card right away; the hold still prevents any resend. - readPromptAdmission: after the exact-message GET misses, list the idle conversation; when native lists it without the message the POST is settled and did not admit, so the drain receives admission_rejected and halts like any definite failure (explicit retry only, never an automatic resend). - On release the pending row is removed and the submitted composer is kept as an unsent message to restore; on confirmed acceptance the stale failure card is cleared. * test(evals): read an empty Lexical composer as an empty draft Lexical keeps one empty paragraph in an empty editor, which innerText reports as "\n". readComposerState now drops that trailing line break so draftText matches what the person sees; user.see("composer", { text }) already trimmed. * test(evals): open the fresh conversation through the sidebar's New session button The sidebar button was renamed from "New task" to "New session" in #4491 (Sep 5) and the "new" scenario added in #4631 (Sep 8) still targeted the old label, so it could never find the button. Same user action, current label. * test(evals): create the empty-session workspace at its declared tmp path The emptySession world declared /tmp/openwork-empty-session-<ts> but seed.workspace() adopted the existing first-launch default workspace instead ("OpenWork Chat" under the dev profile home). On Daytona that profile lives inside the repo checkout, so the engine merged /workspace/.opencode/opencode.json ("permission": "allow") over the workspace's own catch-all and composer-run-mode never saw the workspace MCP rule take effect. * test(evals): name the server error when a run-mode read is not 200 * fix(app): reuse the running OpenWork server when the selected workspace is server-owned Workspaces created from the app live only in the server registry, so a renderer reload reaches startServerWithoutDesktopWorkspace and restarted the embedded server (and its engine) although it was healthy and already serving that workspace. Reuse it when it is ready with the preferred remote-access mode; only restart when nothing usable is running. * test(evals): open the saved-app verification return tab as a person's new tab returnVerification drove the agent browser entry point (browser.openUrl, the IPC behind browser.open_url) with no requesting conversation. Since #4434 the task host refuses that, and it never accepted an about:blank destination. The tab only stands in for the person's own browser so the openwork:// return link can be navigated without OS protocol registration, so create it the way a person opens a new tab and find its CDP target by diffing the page targets. * fix(app): re-read pending questions when a session reaches a terminal edge An interrupted question tool leaves the engine's pending list through its finalizer without a question.rejected event, so Stop on a delegated child left its question (and option buttons) in the parent view. Reconcile questions alongside permissions on terminal edges, for both engines. * Revert "fix(app): re-read pending questions when a session reaches a terminal edge" This reverts commit 9e70982. * fix(app): withdraw the stopped tree's unanswered questions after Stop OpenCode v1 runs tools on detached fibers: aborting a session marks the question tool part as aborted but leaves its request in GET /question, so a parent that stopped a delegated child kept showing the child's question and its option buttons. After Stop confirms the tree idle, reject the pending questions owned by the stopped sessions; other roots are left untouched. * test(evals): temporary Z2 diagnostics around dashboard delete route change * test(evals): saved-apps world.open returns once the destination page root has rendered * test(evals): create the permission-stop-recovery workspace at its declared tmp path Test 2 of parent-child-permission-approval ran bash without asking although the world wrote permission.bash="ask" to the workspace opencode.json. The world called seed.workspace() without create, so it adopted the first-launch default workspace under the dev profile, which on Daytona lives inside the repo checkout; the engine merges /workspace/.opencode/opencode.json ("permission": "allow") after the workspace file and the agent ruleset ends as [* allow, bash ask, * allow] (last match wins; permissions/effective reported shell allow, source engine, workspace file .../home/OpenWork Chat/opencode.jsonc). Same trap and same remedy as #4747 for emptySession; opt-in so the other splitPaneQuestions worlds keep their arrangement. * fix(app): withdraw the stopped tree's unanswered permission requests after Stop OpenCode v1 runs tools on detached fibers: aborting a session marks the tool part as aborted but leaves its Permission.ask pending in /permission, exactly as #4786 found for questions. The stopped tree would keep showing Allow once / Deny in every pane and after reload although nothing can consume the answer. After stopForegroundTree confirms every stopped session idle, also list pending permissions and reject the ones owned by the stopped tree; other roots are never touched and each withdrawal kind is best-effort on its own. * test(evals): remove temporary Z2 diagnostics from saved-app-creation * test(evals): make locate() misses list every candidate of the requested role plus route and page roots A miss used to quote only the first 8 visible buttons in DOM order, which are always the shell rail, so every miss read as "the page has no buttons" (#4794's diagnosis detour). The browser-side report now lists every rendered element of the requested role (buttons/links for role-less targets), the current route/hash, and whether the [data-app-header] / [data-dashboard-page] roots are mounted. The Node side caps the list at 40 and says how many the page really had. Matching is unchanged. * test(evals): run the serialized locate() miss callback against a fake page DOM Proves the browser-side branch the way Chromium will execute it: the report lists every rendered menuitem on a dashboard whose shell rail has nine buttons, names the route and mounted page roots, and a role-less miss lists all nine buttons instead of the first eight.


Overlapping messages in split view shared a workspace-level submission promise, allowing one draft to be reported as sent without submitting it. Ungated messages now send independently, and each pane tracks its own pending request so sending on one side does not make the other side queue messages.
Same-workspace split panes also now subscribe to live session updates immediately, preventing a new pane from staying on Thinking after its message completes.
Pending requests are scoped to the session, including when the user switches the session displayed in a pane before its previous send finishes. The desktop journey verifies that the replacement session can send and receive its own reply while the original request is held open.
Make New session the highlighted first sidebar action, followed by a quieter Split view action. Shorten Create new session to New session in the command palette.
Verification:
new-split-sessiondesktop journey now sends through both panes while holding the primary HTTP acknowledgement, verifies reply ownership, and checks session creation and focus. Passed: 1 test, 0 failures, 0 skips on commitbb65500b4. Placement:daytona (daytona CLI authenticated).Reproduce:
OPENWORK_EVAL_DAYTONA_REF=fix/split-message-actions OPENWORK_EVAL_REF=fix/split-message-actions pnpm evals:e2e new-split-session.Warden reviewed
bb65500b4: 0 findings, 0 blocking findings, clearance approved (run).