refactor(desktop): own Composer readiness, new-task choices and submission below the shell - #5935
Conversation
Move the task-readiness snapshot, its refresh revision and request fence from AppShell into a persistent TaskReadinessProvider in Conversation, mounted beside ComposerStagingProvider. The two Host reads reach the feature through an injected TaskReadinessServices port and a Desktop adapter. The transcript surface reads the notice through TaskReadinessNoticeConsumer instead of receiving it as props. AppShell now passes only the request projection, targets, refresh key and workspace-picker command. The legacy use-task-submission-readiness hook and the task-readiness-notice re-export are retired, and the public entry no longer exports the notice derivation. Refs apache#4582 Generated-by: Claude Opus 5.5
The new chat's Plan toggle, orchestration value and permission choice were AppShell state (two useState calls and useNewTaskChoice). SessionSettingsProvider already decided between the selected Session and the new task for permission writes, so it now holds all three. The shell reads them through the existing useSessionSettingIntent hook and writes them through setNewTaskPlanMode, setNewTaskOrchestrationMode and clearNewTaskPermissionChoice. useNewTaskChoice moves to application contracts, because Session Settings and the Conversation chat-model hook both use it. Refs apache#4582 Generated-by: Claude Opus 5.5
Move the send-pending flag, the edit-and-resend draft and the submit, follow-up and interaction-answer paths out of AppShellContent into a persistent ComposerSubmissionProvider in Conversation. The provider assembles the chat and revision actions, the staged follow-up and createRevisionAwareOnSend. ConversationComposerRegion reads the provider in the Composer slot. The shell keeps one stable command handle, beginEditUserMessage, and supplies only commands it already owns: navigation, catalog refresh, Workbar side chat and form answers, and the selected Session's orchestration write. The Host calls (submitMessage, newTasks.create, sessions.remove, reviseBeforeTurn, abandonSessionCopy, and the sandbox-boundary and question answers) go through ComposerSubmissionServices and a Desktop adapter. Shared helpers move to application contracts or into the feature. The submit construction and unused exports leave the public entry, and the transitional adapter loses the commands only the moved actions used. Refs apache#4582 Generated-by: Claude Opus 5.5
…e Composer owner The Stop and Turn-branch actions move from AppShell-family legacy files into ComposerSubmissionProvider. Their Host calls, sessions.stop and branchFromTurn, now go through ComposerSubmissionServices. The Composer slot reads the published Session's Stop claim and receives onStop/stop from the owner; Stop still notes the stopped Turn for the resume offer. The shell's command handle gains handleTurnFooterAction and receives the Turn-action pending registry as a port, because the shell still renders that registry's mask. SessionLocalMessages, the local delivery-recovery reader, now mounts inside the owner for the published Session. Its recovery policy is unchanged. The transitional adapter drops the Stop claim, the transient add/remove commands and captureSelection, and the README's matching transitional row is removed. Refs apache#4582 Generated-by: Claude Opus 5.5
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed e0b1746b41a7e6e2147798647d0dd38cd217bd56. Scope stated up front: this is a large move (+2396/−806 across 72 files), so I verified the shape of the ownership transfer and the gate rather than reading all 23 modified production files line by line.
The transfer is real, and it is a removal from the shell rather than a parallel copy. app-shell.tsx loses 283 lines and gains 65 (net −218). What it stops importing is exactly the ownership this PR claims to move: useTaskSubmissionReadiness, desktopSlashCommandAvailability / parseDesktopSlashCommand, mergeWorkspaceReferences / rebaseWorkspaceFileReferences, createAppShellChatActions / createAppShellTurnActions / createAppShellStopAction, and useNewTaskChoice; along with the inline state that went with them (newTaskSendPending, newChatPlanModeActive, newChatOrchestrationMode). Nothing equivalent is re-imported into the shell, which is what I would want to see — the shell keeps rendering the feature and no longer owns the logic.
The file accounting matches that reading: 3 files added (all tests — composer-submission-owner.test.ts, task-readiness-owner.test.ts, plus composer-submission-fixture.ts), 23 modified, and 4 renamed — the four contract modules that carried the moved responsibilities. The generated architecture ledger is re-baselined with the budget going down (net −208 in that file), which is the expected direction when ownership concentrates rather than spreads.
Gate on this head: test and label are green.
What I did not judge
- The internal correctness of the relocated logic across the 23 modified files. Green tests are the evidence I have, and for a move I read them as weaker than for a behaviour change: many of the modified files are the
app-shell-*suites themselves, so their assertions moved with the code. The three added modules do add genuine coverage of the new ownership, which is the stronger half of the signal. - No Electron run.
- Whether any downstream reader still depends on the four relocated module paths externally.
I did not approve, request changes, or merge.
Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
|
Thanks for the review. Evidence for the three points it left open:
One correction to the summary: the shell still imports Posted by Claude Code on behalf of the PR author. |
Astro-Han
left a comment
There was a problem hiding this comment.
Independent review of e0b1746b41a7e6e2147798647d0dd38cd217bd56 (base 1a66e4d5e; current main 887924e14 is one commit ahead and the PR merges cleanly onto it).
Verdict: the move looks behaviour-preserving. No P0 or P1 found. One cross-PR knip break (P3, coordination) and three further P3s are below.
What I checked line by line against main:
app-shell-{chat,revision,stop,turn}-actions.ts→features/conversation/controller/*: everywindow.maka.*call now goes throughComposerSubmissionServiceswith the same arguments. The Desktop adapter maps them 1:1. The guards, toasts, copy-attempt identity, rollback and unsent-Session cleanup are unchanged.useTaskSubmissionReadiness: same sequence fence, same reset on a request, target or refresh change, same dependency list. The services come from composition, so they are stable andcheckNowdoes not churn.- The picker gates keep their asymmetry: the context picker is blocked only while editing in the draft's Session, and the directory picker is blocked while any draft is open.
composer-submission-owner.test.tspins both, including the switch to another Session. SessionLocalMessagesreceives the same function identities as before:commands.addTransientMessage/removeTransientMessage,toast.error,queue.restoreDraft. Its effect therefore does not re-run more often.activeIdRefis stillworkspace.publishedSession.- New-task choices:
useNewTaskChoicekeeps the same key (currentNewTaskDraftKey). Plan and orchestration stay unkeyed as before, andopenNewTaskSurfacestill resets only Plan. The shell sees the state through the bridge's layout-effect publish, which is the same pattern the overlays already use, so the update lands before paint. useStableActionsfacades keepreader/shellCommandsstable. They change only whennewTaskSendPendingorrevisionDraftchanges. StrictMode's layout-effect replay re-bindssubmissionBindingscorrectly.- Ledger and gate only tighten: two new
controllerOwners, two private modules, four legacy AppShell action files gone,useStableActions4→1,useState8→4, anduseNewTaskChoiceanduseTaskSubmissionReadinessremoved from the gate.
Local run on this head (Node 22, build:test): composer-submission-owner, task-readiness-owner, session-settings-provider-scope, composer-staging-owner, use-stable-actions and all app-shell-* suites pass (131/131). CI test is green.
P3 (coordination): merging after #5937 breaks knip (CI). (Graded P3: neither PR is wrong on its own; this is a merge-order coordination item, like the protocol-epoch collisions we track across PRs.) This PR copies showSessionWorkspaceUnavailableToast into features/conversation/model/session-workspace-toast.ts. After that, the only importer left for the legacy src/renderer/session-workspace-errors.ts is app-shell-project-actions.ts, and #5937 deletes that file. I merged main + this PR + #5937 locally and ran npx knip --workspace apps/desktop. It reports Unused files (1) apps/desktop/src/renderer/session-workspace-errors.ts, so whichever of the two lands second fails CI's knip step. Suggested fix: move the toast helper into application/contracts/session-workspace-errors.ts, next to isSessionWorkspaceUnavailableError, and import it from there instead of copying it. Then delete the legacy file in whichever PR lands second, including its ledger entry.
P3: test gap, steering Turn id. The owner's getRunningTurnId wiring (use-composer-submission.ts:86) is not covered by any test. I changed it in the built output to always return undefined, which drops hostTurnId from a steering message's transient row, and the full test:dist suite still passed (3,137/3,137). Please add a case to composer-submission-owner.test.ts: with an active Host Turn, a current_turn send publishes the transient row with that hostTurnId.
P3: duplicated helper. session-workspace-toast.ts:27 is a byte-for-byte copy of the legacy helper. The coordination fix above also removes this.
P3: provider value churn. openWorkspacePicker is a new closure on every AppShell render (app-shell.tsx:1241), so TaskReadinessProvider's memoized value, and TaskReadinessNoticeConsumer with it, change on every shell render. This is cheap today. Passing a stable command and the activeSession?.id / canAddProject facts would make the memo meaningful.
Cross-PR / merge order (the four R2 PRs). I ran pairwise git merge-tree against 887924e14:
- After #5936: textual conflicts in
app-shell.tsx,renderer-architecture.json,scripts/check-app-shell-hooks.mjsandcomposer-staging-owner.test.ts. - After #5937: conflicts in
renderer-architecture.jsonand the Astryx inventory, plus the knip break above. - After #5934: no textual conflict, but its checker then fails on the merged tree. I ran it there: there is a stale retained-root row for
useTaskSubmissionReadiness(README:307), five unrecorded new root symbol uses, and nine stalerootSymbolUsesentries. That is the expected--writeplus row-deletion follow-up, and all the additions are new exports, so the ratchet admits them.
This is an automated review by Claude (Anthropic), run on behalf of the maintainer. It is not an approval. Please verify the findings before acting on them.
- Move the missing-working-directory toast into application/contracts/session-workspace-errors.ts. Contracts cannot import copy catalogs, so callers pass the copy. The Conversation copy is deleted, and the chat, revision and Turn actions call the contract. - Pin the steering Turn id: an owner test sends with followUpMode "steer" while a Host Turn runs and checks that the pending row carries that Turn's id before the Host answers. - TaskReadinessProvider takes the recovery Session id and the stable recovery and Add Project commands instead of a closure built on every shell render, and memoizes the picker action. A shell render with the same facts no longer republishes the notice context. Refs apache#4582 Generated-by: Claude Opus 5.5
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed cd3345ceb4b06f26ba52dcf8206b1d30228f3294 (72 files, +2466/−810, 5 commits).
All three P3s I raised last round are fixed, each verified in the code rather than from the reply.
- The duplicated toast helper is gone from the Conversation zone. The definition now lives once in the application contract (
application/contracts/session-workspace-errors.ts), the Conversation copy is deleted outright, and the callers pass the shell copy in as a parameter, so the contract does not reach back into a feature's copy. One copy of the same helper still exists in the legacysrc/renderer/session-workspace-errors.ts, which is exactly the file I flagged as #5937's to remove — and it still has an importer (app-shell-project-actions.ts) only because #5937 has not landed. So this PR did what it could and the residue is correctly attributed to its sibling. - The uncovered steering wiring now has a test.
composer-submission-owner.test.tsadds "steers into the running Host Turn: the pending row names that Turn before the Host answers", assertingpending?.hostTurnId === 'host-turn-1'for afollowUpMode: 'steer'send. That is precisely the assertion my mutation test showed was missing — I had flipped the wiring toreturn undefinedindistand the whole suite still passed. - The per-render closure is gone from the readiness provider.
TaskReadinessProvidernow takesworkspaceRecoverySessionIdand the stableopenSessionWorkspaceRecovery/addProjectcommands and memoizes the picker action on those, so its context value no longer changes on every shell render.
Gate on this head: test and label are green; mergeable is true against the base branch, so this PR can still land on its own. Every commit carries a Generated-by: trailer and there is no Grok involvement.
What I could not judge
- I did not run the suites or the mutation check again; I verified the three fixes by reading the code, the deleted file, and the new test's assertion.
- The render-stability test the reply mentions: I confirmed the memoization change on stable inputs but did not execute its new assertion.
I did not approve, request changes, or merge.
Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
apache#5927 removed the composer's stopped-Turn note so the send slot offers Resume after a manual Stop. The Stop path now lives in the Composer submission owner, so the owner drops its noteUserStoppedTurn shell port, and the Composer's Stop button, Escape and question prompt share one stop action. The owner test now checks that Stop removes the rows the Host retracts. The renderer architecture ledger starts from main's copy, gets the hand-maintained controllerOwners, private modules and retired ownership entries back, and is regenerated. Generated-by: Claude Opus 5.5
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed b4a07eed878642a86a082386f0f84aad2f1c4b93.
The rebase is faithful, so my previous review's conclusions carry over unchanged. A git range-diff between the old and new ranges marks every commit of this PR as = — byte-identical — with the only differences being commits that arrived from main itself. The tree difference I first measured against my previously reviewed head was entirely that base advancement, not a change in this PR's own work. The gate is green on the new head.
Merge order, since the dispatch asked whether the #5934 → #5935 → #5936 → #5937 sequence is still required and which pairs still conflict. I measured the file sets at the current heads. All four now sit on the same base, so they cannot land independently; every pair except #5934's overlaps on real source, and most pairs overlap on app-shell.tsx:
- #5934 ∩ #5935 and #5934 ∩ #5936: only the generated
renderer-architecture.json. - #5934 ∩ #5937: that ledger plus
features/overlays/index.tsandtesting.ts. - #5935 ∩ #5936: twelve files, including
app-shell.tsx,chat-message-surface.tsx,composition/desktop-feature-services.tsx,scripts/check-app-shell-hooks.mjsandfeatures/conversation/testing.ts— the heaviest of the six pairs. - #5935 ∩ #5937: the ledger,
app-shell.tsx, and both inventory documents. - #5936 ∩ #5937: the ledger,
app-shell-effects.tsandapp-shell.tsx.
So the prescribed order still holds, and each landing after the first needs a real merge on app-shell.tsx that should be reviewed rather than trusted, since these are ownership moves in the same file rather than adjacent edits.
What I could not judge
- The rebase was verified by
range-diffand the overlaps by file set; I did not run the suites or produce the merges themselves. - I did not re-audit the moved logic, since the rebase left it untouched.
I did not approve, request changes, or merge.
Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
apache#5934 added the retained-root table and the rootSymbolUses record. This PR removes nine root call sites: - useTaskSubmissionReadiness and useNewTaskChoice; - three useStableActions calls (chat, turn and revision actions); - four useState calls (newTaskSendPending, newChatPlanModeActive, newChatOrchestrationMode, revisionDraft). Their rows are deleted, and the rows whose consumers moved to the Composer submission owner are corrected. rootSymbolUses is regenerated. It gains the five new owner, provider and command-handle exports and drops nine stale uses. Generated-by: Claude Opus 5.5
Brings in apache#5934 and apache#5935. Conflicts were only in generated files: - renderer-architecture.json: taken from main's copy, the workspace-projection ownership entry removed again, and regenerated. The new rootSymbolUses records one admitted appShell use, features/diagnostics: ManualDiagnosticReportConsumer (new export). - docs/astryx-surface-file-inventory.md: regenerated from main's copy. With apache#5935 on main, the legacy src/renderer/session-workspace-errors.ts lost its last importer (the deleted app-shell-project-actions.ts), so it is removed here; knip reports no unused files. Generated-by: Claude Code
…nds (#5951) `e2e/session-workbar.spec.ts:179` ("Terminal survives navigation and reload…") is flaky on main. It always fails at line 224: after `page.reload()`, the owner Session's right Workbar comes back collapsed, so the terminal region never appears. This is a product bug, not a harness problem. A reload can drop every other Session's per-Session Workbar visibility. The cause is a race at startup. After a reload, a `sessions:changed` event from a Session whose turn is still finishing (the test does not wait for the replacement Session's turn to end) is read by the patch drain. `catalog.commitPatch` commits it before `bootstrapSessions()`'s `sessions.list()` does. `commitPatch` moves the catalog `revision` off 0, and `selectAuthoritativeSessionIds` read any `revision > 0` as a membership observation. It therefore published a one-row set. `useWorkbarLayoutState` dispatched `retain-sessions` with it. No Session is active yet at that point, so every other Session's `collapsedBySession` entry was dropped, and the right-visibility effect persisted the loss to `maka-session-workbar-collapsed-v2`. The rate went from 9/40 at 8ad836c to 19/40 at 255ae23 (Fisher p = 0.034) but the root cause is older. Row patches have been able to land before the list since #5532. The range 8ad836c..255ae23 changes nothing on the catalog, Workbar layout, main, preload or runtime path, so #5934/#5935 only moved the timing. Generated-by: Claude Opus 5.5
Summary
This is the first of two PRs that finish R2 M3. It covers the submission side. AppShellContent no longer holds the Composer's readiness snapshot, the new-task mode choices, the send-pending flag, the edit-and-resend draft, or the code that assembles the send, Stop, Turn-branch and interaction paths. Each moves to the owner that already holds the related state, and the Host calls go through injected ports and Desktop adapters. The second PR handles the reader side; see "Left for the M3 reader PR" below.
Refs #4582
The four commits can be reviewed one at a time:
TaskReadinessProvidersits besideComposerStagingProviderand is a registered controller owner.TaskReadinessServicesandcreate-task-readiness-services.ts.ChatMessageSurfacemountsTaskReadinessNoticeConsumer.SessionSettingsProvider.newTaskthrough the existinguseSessionSettingIntentcall, so no hook is added.useNewTaskChoicemoves toapplication/contracts, because Session Settings and the Conversation chat-model hook both use it.ComposerSubmissionProvideris a registered controller owner. It owns the send-pending flag, the edit-and-resend draft and its catalog watch, and the retracted workspace references. It assemblescreateRevisionAwareOnSend(fix(desktop): allow unchanged edit-and-resend #5815), the staged follow-up, and the chat and revision actions.ConversationComposerRegioninjectsonSend,newTaskSendPending, the interaction answers and the revision notice into the persistent Composer slot.beginEditUserMessage), following the refactor(desktop): move Composer staging into a persistent owner #5868 staging-commands pattern.handleTurnFooterActionjoins the command handle.SessionLocalMessages, the local delivery-recovery reader, now mounts inside the owner for the published Session. Its recovery policy is unchanged.Which caller loses which capability
AppShellContentuseNewTaskChoiceand thenewTaskSendPending,newChatPlanModeActive,newChatOrchestrationModeandrevisionDraftstate; the refsrevisionDraftRefandretractedWorkspaceReferencesRef; construction of the chat, revision, Stop and Turn actions, the follow-up andcreateRevisionAwareOnSend; theCatalogRowWatchandSessionLocalMessagesmountsnewTaskprojection; ashellport of commands it already owns (surface ownership, navigation, catalog refresh, execution-boundary reload, the Workbar form answer, side chat, the new-task resolver, the model-setup toast, the Turn-action pending registry, the selected Session's orchestration write); and theComposerSubmissionCommandshandleuseAppShellSessionUiState(transitional adapter)stopPendingClaims,addTransientMessage,updateTransientMessage,removeTransientMessage,settleInteraction,markInteractionChanged,clearMessageLoadError,prepareSend,compactSession,readSelectionRevision,captureSelectionreadMessagesfor Copy/SavecreateRevisionAwareOnSend,RevisionSendPorts,createStagedFollowUp,deriveTaskReadinessNotice,isTaskSubmissionHardBlocked,TaskReadinessNotice,useNewTaskChoice,SessionLocalMessages,SessionPendingClaim,toSubmittedAttachments,PendingAttachment,activeHostTurnChatMessageSurfacetaskReadinessNotice,onTaskReadinessActionpropsapp-shell-chat-actions.ts,app-shell-revision-actions.ts,app-shell-stop-action.ts,app-shell-turn-actions.ts(10 bridge references)features/conversation/controllerbehindComposerSubmissionServicesOther legacy files retired:
use-task-submission-readiness.tsand thetask-readiness-notice.tsre-export;skill-invocation-feedback.tsandfollow-up-submit-routing.ts, which move into the feature;session-copy-attempt.ts,attachment-preflight.ts,side-chat-command.tsanddesktop-slash-command.ts, which move toapplication/contractsbecause Workbar shares them. Workbar's three budgeted legacy imports become contract imports.What enforces it
controllerOwners.useTaskSubmissionReadinessruns only insideTaskReadinessProvider, anduseComposerSubmissiononly insideComposerSubmissionProvider. The public entry cannot re-export either.featurePrivateModules. The submission binding and reader context are private. The readiness context is module-local.window.maka. Source-scan tests pin one mount for each owner and one composition reference for each adapter, and they require that no renderer file outsideplatform/desktopcalls the moved bridge paths.ComposerStagingProvider; the Conversation README documents it. The owner tests cover it at the feature level only (a reader unmount/remount for readiness, Session switches for the draft).Behavior preserved
There are no product, copy, IPC, storage or visual changes.
workspace_pickerrouting.The send-time admission check (
checkTaskSubmissionReadiness) moves into the owner unchanged; it never read the readiness snapshot.Review focus
shellport has 15 named commands. Each is something the shell owns: navigation, the catalog, or another feature's command. None returns Composer state. Moving any of them further down depends on the PR2 reader work and on slices C and D.Inventory (
c7fa6bb6a→ this branch)Rows marked "#4582 measure" are from the issue's completion table.
c7fa6bb6ais the issue's baseline column;mainat1a66e4d5ehas the same values.legacyAppShell.filesbridgePathsactionFactoriescreateAppShellE2eFixtureActions,createAppShellProjectActionsfor slice C)useAppShellSessionUiReads; Copy/Save message read)check:app-shell-hooksuseTaskSubmissionReadiness,useNewTaskChoice;useState8 → 4;useStableActions4 → 1nonTriviaTokenscontrollerOwnersfeaturePrivateModulesscripts/check-app-shell-hooks.mjsis edited by hand.renderer-architecture.jsonadds twocontrollerOwners, two private modules and removes two retiredownershipentries by hand; the rest is regenerated with--write.Left for the M3 reader PR
These depend on slices C and D:
useAppShellSessionUiReads. Its values are read on about 20 lines of app-shell.tsx (turnActivealone on 11), which also feed root chrome (pet state, slash-command availability, permission and mode disabled reasons, the home surface); that needs D's retained-root table.app-shell-command-actions.ts.useShellResume,useShellChatModel,useTurnActionRegistry,useAppShellTurnPresentationanduseActiveExecutionBoundary.The external PRs #5058, #5274, #5581, #5513, #4138 and #5405 rebase onto these owners.
Verification
At
b4a07eed8(main8ad836ce1merged in), Node 24.19.0, freshnpm installplus the dependency patches and Electron install:npm --workspace @maka/desktop run clean:main && build:test && test:dist: 3,165 / 3,165 pass, 0 cancelled. fix(desktop): offer composer Resume after a manual Stop, drop the turn-footer resume button #5927 removed some resume tests onmain.task-readiness-owner.test.ts(12 tests) andcomposer-submission-owner.test.ts(13 tests), plus new-task tests insession-settings-provider-scope.test.ts(2 tests). They exercise the real Conversation, staging and submission owners in the fake DOM.window.makadoubles:app-shell-*-actions, first-send cleanup, busy race, revision resend and composer staging.main'sapp-shell.tsxandchat-message-surface.tsxrestored, the readiness ownership tests fail.npm run check:app-shell-hooks: ok, 26 hooks / 33 call sites.npm run check:renderer-architecture -- --base 8ad836ce1 --strict-base: passed.npm run typecheck(includes Storybook);npm run lint,npm run format:check;npx knip --workspace apps/desktop,npx knip --workspace packages/ui;npm run check:asf-headers;npm run windows:inventory(current, 119 declarations);npm run check:e2e-budget(34 tests in 20 files, unchanged).npm run astryx:surface-inventory: regenerated; +2 aligned files (the two providers).npm run buildthennpx playwright test --config e2e/playwright.config.ts, all 34 Electron tests onb4a07eed8(after merging fix(desktop): offer composer Resume after a manual Stop, drop the turn-footer resume button #5927, which changed the Stop path this PR moves): 34 passed (3.5 min), 0 failed, no retries. An earlier run before the rebase and merges also passed 34/34. No Electron test was added; pere2e/AGENTS.mdthe new coverage is in the fake-DOM owner tests. The existing specs already drive first send, local recovery across restart, side chat,/compactand streaming remount through the real app.AI use
Select exactly one:
Tool(s) and scope: Claude Opus 5.5 in Claude Code wrote the implementation, the tests, the ledger and inventory regeneration, and this description. The author reviewed them. Each commit carries
Generated-by: Claude Opus 5.5.Checklist
Does this PR entail a change in behavior?