Repository navigation
feat(desktop): open sessions in dedicated windows - #6093
YayoiNanoka wants to merge 14 commits into
Conversation
Coordinate exclusive session-group ownership across independent windows, preserve workspace drafts and resources through transfers, and cover lifecycle and recovery regressions. Generated-by: OpenAI Codex
Remove workspace SQLite persistence and periodic autosave, reuse the existing draft and attachment lifecycle, and restore ordinary cache and layout behavior. Release temporary snapshots after adoption or failed transfers and cover cancellation, timeout, and multi-host cleanup. Generated-by: OpenAI Codex
Keep the handoff regression fixture within the renderer architecture import boundary. Generated-by: OpenAI Codex
Use an inward arrow icon and place the action before the workbar toggle in both visual and keyboard order. Generated-by: OpenAI Codex
Place new session windows on the main window's current display with a 32-DIP offset and clamp their bounds to its work area. Preserve existing-window placement and cover display edges, scaling, small screens, and moves during creation. Generated-by: OpenAI Codex
Regenerate the Astryx file inventory and include desktop TSX test entries in Knip analysis. Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
First review of d14c0c54a0ba78756de9ae032b208fed5f0684ef (10 commits on merge-base b9cac4e3d; +6346/-589, 132 files). There are no prior findings.
Scope
| Area | Files | Lines |
|---|---|---|
Main-process window management (registry, coordinator, group resolver, main-window.ts, quit/PiP/notifications) |
12 | +981 / -129 |
IPC/bridge (*-ipc-main.ts gates, preload, shared contract, runtime-host-boot.ts wiring) |
15 | +403 / -54 |
Renderer (apps/desktop/src/renderer, packages/ui, stories) |
59 | +1335 / -356 |
| Storage/state handoff (workspace state store, attachment recovery, draft/side-chat handoff contracts, composer draft/attachment hooks) | 14 | +926 / -25 |
| Tests (unit + 1 E2E spec) | 25 | +2654 / -12 |
| Docs/inventory/config (surface inventory, renderer-architecture, e2e-budget, knip) | 5 | +39 / -9 |
| Smoke scripts | 2 | +8 / -4 |
| Licenses | 0 | 0 |
I found no issue or Discussion where a maintainer asked for multi-window sessions. The PR links no issue, and searches of issues and Discussions for "new window", "multiple windows", "separate window" and "detach" returned nothing relevant. For a feature of this size, a short design discussion first would help, covering ownership model, draft custody and browser policy.
A minimal first version needs: the workspace window registry and multi-window-aware controller (broadcasts, dialog parents); the coordinator's group ownership and route/focus logic; session window creation and the "Open in new window" / "Open in main window" entries; and ownership gates on the existing per-session IPC. In-memory draft, attachment and side-chat handoff, PiP re-parenting and browser-view transfer can all follow later. A first version could refuse to detach while the source holds an unsent draft.
Suggested split (each piece can merge on its own):
- Refactor only, no behavior change:
workspace-window-registry, theMainWindowController/WorkspaceWindowControllersplit, broadcast to all workspace renderers,showOpenDialog(options, sender)parent resolution and quitisQuitting(). - Coordinator, group resolver, session-window renderer root, menu entries, and shell-run and browser ownership gates. Drafts stay with the source.
- Draft, attachment and side-chat handoff: the state store, recovery transfer, composer/staging hooks and
sessions:branchFromTurnrecovery IDs. - Native resources: PiP owner rebind and browser view transfer/parking, with an explicit background-browser policy (see finding 1).
Findings
-
P1: Browser views of any session that was ever moved between windows can be driven in the background, including clicks and typing, until the Host retires.
transferSessionsandreleaseRendereradd the session todetachedSessions, andcanRunInBackgroundnow returnsisHostActive(ref)for those sessions (browser-ipc-main.ts:107).createBrowserViewHost.canDrivecheckscanRunInBackgroundfirst, sobrowserActionAllowed's "shown + live viewport" rule is skipped for bothobserveandmutate.beginActionalso takes the background path. OnlyretireTargetremoves entries from the set. Before this PR, only WorkHub coordination sessions could run hidden. With the PR, the exemption covers:- a session moved back to the main window while the user views another session;
- a minimized session window;
- a session window that was closed, whose page is now parked on the invisible
parkingParent.
The PR flips the existing assertion in
browser-ipc-main.test.tsfromcanDrive(mainKey, 'observe') === falsetotrueafter all owning windows close, so the change is deliberate. The PR description does not mention it, though, and it removes the "a backgrounded conversation is never drivable" invariant documented inautomation-host.ts. Suggested fix: keep detached sessions under the normal visibility rule, using their owning window's visibility. Only keep a page alive while parked, not drivable. -
P2: A workspace window (main or session) cannot be closed while its draft holds a file-picker attachment more than 30 minutes old, or one whose file has since been deleted. The coordinator's
closehandler (session-window-coordinator.ts:103-124) always callspreventDefault()on a ready window. It then flushes and saves, and destroys the window only if the save succeeds.SessionWorkspaceStateStore.#prepareDraftre-resolves everyapprovalattachment throughprepareIngestItems/peekApproval, and that fails withsource_expiredafterATTACHMENT_APPROVAL_TTL_MS(30 min) oritem_unreadableif the file is gone. The store test "a handoff snapshot does not extend source approval…" asserts exactly this rejection. On failure, the close handler resumes the window and shows an error dialog. The test "failed handoff … and failed normal close" assertsh.main.destroyed === false. Every later close attempt fails the same way until the user removes the attachment or quits through the menu or tray. On Windows/Linux without a tray, the window close button is the normal quit path. Before this PR, closing never depended on the draft being transferable. Suggested fix: make the final close best-effort. Drop or degrade attachments that cannot be transferred (or keep them only as names), but never block the close on custody of the draft. -
P2: Every
sessions:changedevent now triggers a full Host session-catalog read, even when no session window exists.emitSessionsChangedcallssessionWindows.refreshGroup(...)for every non-delete reason (runtime-host-boot.ts:2271). That leads toresolveGroup, which runsstate.candidate.client.listSessions()(runtime-host-boot.ts:898), a paginated full catalog read that can fail withcatalog_unstablewhile the catalog changes. During a run, the observer emitsstatus-changeon each snapshot change andmessage-appendedon eachtool_resultand turn start/end (runtime-host-session-observer.ts:838-873). Each one now adds a catalog read on top of the renderer's own refresh, so cost grows with catalog size × run activity for all users, not only those using session windows. Suggested fix: skip the refresh when no session window is open, or when the session is already a known member. Otherwise, derive membership from the changed session's ancestry instead of re-listing.
P3 (not verified at runtime or no runtime impact)
- Ownership is enforced only for shell runs and the embedded browser.
sessions:send, stop and edit IPC accept any workspace renderer. Today only routing in the UI keeps two windows from acting on one session. canUseSessionis synchronous and only knows a group's members afterrefreshGroup. A side chat or subagent created inside a session window is briefly treated as main-owned, so terminal and browser calls from that window are rejected until the asynchronous refresh lands.- The coordinator calls
preventDefault()beforemain-window.ts's owncloselistener runs, so that listener returns early, anddestroy()emits nocloseevent. As a result the final bounds write andclearShowFallbackTimerare skipped on a user close; only the 400 ms debounced save persists bounds. - A hung (unresponsive but not crashed) renderer makes close wait for the 20 s flush timeout and then stay open.
readyWaitersfor a closed window only clear on timeout. Thegroups/memberGroupsmaps grow for the whole process lifetime. - Closing the main window no longer disposes browser views. They stay parked in memory until Host retirement.
onMainWindowCloseno longer tears down the Computer Use overlay or PiP on macOS. - Native Windows/Linux runs were not done, per the PR body. Session windows reuse the main window's frame and titlebar overlay options, but the Windows maximize sync and titlebar overlay were only exercised through CI packaging.
Epoch: no packages/runtime-host protocol files are touched and the PR claims no epoch change, so there is no collision with the 221/222 claims. Note that origin/main is already at RUNTIME_HOST_COMPATIBILITY_EPOCH = 221.
Mergeability/CI: the PR is CONFLICTING. The only conflicting file is docs/astryx-surface-file-inventory.md (the totals line: main has 337 files, the PR has 340), which is trivial to resolve. On the head, test (run 38520095937), package (Release Windows, run 38520095905) and label all pass.
| }; | ||
| const canRunInBackground = (sessionId: string): boolean => { | ||
| const ref = parseDesktopSessionResourceKey(sessionId); | ||
| if (detachedSessions.has(sessionId)) return deps.isHostActive(ref); |
There was a problem hiding this comment.
P1: any session ever transferred or parked stays in detachedSessions until retireTarget, so canDrive/beginAction admit observe and mutate actions while it is hidden, minimized, back in main behind another session, or parked after its window closed. This removes the shown+viewport requirement that only WorkHub coordination sessions were exempt from. Keep parked pages alive, but leave them under the normal visibility rule.
| }); | ||
| entry.window.on('close', (event) => { | ||
| if (deps.isQuitting() || !ready.has(windowId)) return; | ||
| event.preventDefault(); |
There was a problem hiding this comment.
P2: close is always prevented and only completes if flush+save succeed. #prepareDraft rejects approval attachments older than the 30-minute approval TTL (source_expired) or whose file is gone. The window then resumes and refuses every later close until the attachment is removed. Final close should be best-effort and not depend on custody of the draft.
| ...(extra?.turnId ? { turnId: extra.turnId } : {}), | ||
| }; | ||
| if (sessionId && reason !== 'deleted') { | ||
| void sessionWindows.refreshGroup(desktopSessionKey({ ...scope, sessionId })).catch(() => undefined); |
There was a problem hiding this comment.
P2: this runs a full client.listSessions() catalog read (line 898) for every non-delete sessions:changed, including per-run status-change/message-appended events, even when no session window exists. Gate it on session windows being open or the session being a new unknown member.
Keep draft, side-chat, browser, and terminal ownership aligned with the owning session window. Preserve recoverable close state, roll back partial copy adoption, and reject queued commands after closing starts. Generated-by: OpenAI Codex
Preserve upstream internal links and session-window routing, and keep the parent-session breadcrumb in the navigation feature. Generated-by: OpenAI Codex
Astro-Han
left a comment
There was a problem hiding this comment.
Automated review notice: This comment was posted by an automated review agent. It is not an independent human review and does not replace one.
At 56741b71a4b47737a1e96219e033944989279eb1, the previous browser-permission P1 and both previous P2 findings are addressed. I found one new P2 in the generated surface inventory. This follow-up covers the changes since d14c0c54, including author commit 3971ba2a and the subsequent merge of main 1751c399; it is not a new full review of the original feature.
[P2] Regenerate the surface inventory totals. docs/astryx-surface-file-inventory.md:9 still says 340 files and 336 aligned files, although the five added surface rows bring the actual totals to 345 and 341. With freshly installed pinned dependencies, npm run astryx:surface-inventory exits 1 because the Markdown differs from the generator output. I checked an isolated export of the actual main parent with the same generator and dependencies: main passes with 340 files, while this head fails with 345. The totals line is the only generated difference. The CI workflow runs this check before Build (.github/workflows/ci.yml:276–281), so this head introduces a reproducible validation failure even though its remote CI job has not started. Run npm run astryx:surface-inventory:write and commit the regenerated artifact.
The previous findings are resolved as follows:
- Transferred browsers no longer receive an unrestricted background exemption. The foreground-permission test passes after transfer for a visible owner, rejects observation and mutation when that owner is minimized or hidden, and permits use again after it becomes visible. Loading the previous browser module makes the minimized-owner assertion fail. The existing test also verifies denial after the last owning window closes.
- Expired or missing picker attachments no longer trap ordinary window close. Valid attachments and unsent text are retained; unavailable items are reported, cancellation resumes the window, and explicit confirmation closes it through the native close event. Loading the previous state-store or coordinator module makes the corresponding regression fail.
- Session-change events no longer refresh every window group and fetch the full Host catalog. Executing the actual boot functions with instrumented dependencies gives zero group refreshes for 100 events and zero full-catalog reads for 100 known-session resolutions; the previous functions perform 100 of each. A missing child uses one targeted session read. This does not claim that the existing catalog's own background synchronization performs no reads.
The clean workspace build, four Desktop type checks, normal renderer build, 210 focused tests, five URI-parser tests, strict renderer-architecture check with its 168 tests, 23 inventory-contract tests, and E2E-budget check pass. Both unmodified native Electron session-window tests pass on Linux: native browser identity and minimized-owner restoration survive detach/return, and closing/recreating Main preserves the child while restart opens only Main. I did not rerun the full repository suites or native Windows/macOS tests.
There are no Runtime Host protocol changes against main 1751c399; epoch 222 is unchanged. GitHub reports the branch as mergeable but blocked. The Windows packaging check passed; CI run 38767026059 remains queued. Those checks do not establish a green result for this head.
| | `apps/desktop/src/renderer/features/session-navigation/services-context.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | | ||
| | `apps/desktop/src/renderer/features/session-navigation/ui/archived-task-scope.tsx` | shell-chrome-or-panel | Button, HStack, Selector, StackItem, Text, TextInput, VStack | aligned — uses Astryx (Button, HStack, Selector, StackItem, Text, TextInput, VStack) | aligned | | ||
| | `apps/desktop/src/renderer/features/session-navigation/ui/session-history-navigation.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | | ||
| | `apps/desktop/src/renderer/features/session-navigation/ui/session-navigation-commands.tsx` | shell-chrome-or-panel | none | aligned — no raw controls; no Astryx JSX usage | aligned | |
There was a problem hiding this comment.
[P2] Regenerate the inventory totals with these new rows
These five new surfaces are present in both the path inventory and the generated rows, but the totals at line 9 remain at the main parent's 340 files / 336 aligned instead of 345 / 341. npm run astryx:surface-inventory therefore exits 1 on this head; the actual main parent passes with the same generator and pinned dependencies. This check runs before Build in CI. Please regenerate and commit the artifact with npm run astryx:surface-inventory:write.
Generated-by: OpenAI Codex
Generated-by: OpenAI Codex
Problem
Maka's single workspace window makes it difficult to keep separate tasks visible at the same time. Opening additional windows must avoid two editors owning the same session, losing unsent content during a handoff, or cleaning up resources that another window has taken over.
Solution
Result
Users can work on separate tasks side by side while each session group has one active window. Detaching the current session leaves Main blank; returning it preserves its draft and workspace. Closing Main leaves session windows running. Closing a session window retains recoverable drafts while the desktop process remains alive; restarting opens only Main and does not restore child windows or handoff drafts.
This change adds no SQLite database or tables, durable draft storage, attachment lifetime extension, or Runtime Host execution/message protocol.
Screenshots
Production shell components rendered with Storybook fixture data.
Main window with the session sidebar:
Dedicated session window; the return-to-main icon sits before the workbar expansion button:
Validation
1751c399, app-shell hooks, locale hygiene, Windows test inventory, E2E budget, and desktop/UI Knip passed.AI use
Tool(s) and scope: OpenAI Codex implemented the feature and tests, ran validation, and performed independent subagent review. Commits include the
Generated-by: OpenAI Codextrailer. Human review and the merge decision remain pending.Checklist
点击展开中文
问题
Maka 只有一个工作区窗口,不便于同时查看不同任务。增加窗口需要避免两个编辑界面同时持有一个 session、交接时丢失待发送内容,以及原窗口清理已被其他窗口接管的资源。
解决方式
实现效果
用户可以并排处理不同任务,每个 session 组仍只有一个活动窗口。移出当前 session 后,主窗口显示空白页;移回时保留草稿和工作区内容。关闭主窗口不会关闭其他 session 窗口。关闭 session 窗口后,只要桌面进程仍在运行,就保留可恢复草稿;重启仅打开主窗口,不恢复子窗口或交接草稿。
本次改动不新增 SQLite 数据库或表、草稿持久化、附件有效期延长,或 Runtime Host 的任务执行与消息协议。
截图
使用 Storybook 示例数据渲染实际产品组件。
包含左侧 session 列表的主窗口:
独立 session 窗口;返回主窗口图标位于工作栏展开按钮左侧:
验证
1751c399的 renderer 架构、app-shell hooks、语言文案、Windows 测试清单、E2E 预算及 desktop/UI Knip 检查通过。AI 使用
OpenAI Codex 完成功能实现、测试、验证及独立 subagent review。提交包含
Generated-by: OpenAI Codex标记。人工审查和合入决定尚未完成。检查清单