+ {t("task:composerStartAgentHint")} +
+ ); +} diff --git a/apps/web/components/task/chat/message-list-footer.test.tsx b/apps/web/components/task/chat/message-list-footer.test.tsx index a48389711b..ea6d1c1781 100644 --- a/apps/web/components/task/chat/message-list-footer.test.tsx +++ b/apps/web/components/task/chat/message-list-footer.test.tsx @@ -12,23 +12,10 @@ vi.mock("@/components/task/chat/message-renderer", () => ({ ), })); -let mockResumeSkippedSessionIds: Record` with + `t("task:composerStartAgentHint")`. No click action. +- i18n: add `composerStartAgentHint` to + `apps/web/src/locales/{en,pseudo,pt-pt,zh-cn}/task.json`. Copy: "Sending a + message will auto-start the agent." (plain punctuation, no em dash). +- Component test: extend `chat-input-area.test.tsx` (or a focused test) to + assert the hint renders when `showAgentStartHint` is true and is absent + when false. + +## E2E Changes + +- `apps/web/e2e/tests/settings/prevent-auto-start-on-open.spec.ts`, + "post-restart session does not auto-resume when the preference is on": + - assert `composer-agent-start-hint` visible and + `session-resume-start-button` count 0 (no "Resumed agent" on its own); + - send `/e2e:simple-message` via the composer (`session.sendMessage`) and + assert the mock response appears and "Resumed agent" becomes visible + (sending a message is now the explicit start). + - The two final-step (CREATED) tests keep asserting + `task-description-start-button`; unchanged. +- Mobile (`mobile-chrome` project): new + `apps/web/e2e/tests/settings/mobile-agent-start-hint.spec.ts` using the + same restart pattern (task + first turn → `backend.restart()` → + `testPage.reload()`) on a phone viewport: hint visible, no resume button, + send message → mock response. The existing + `mobile-prevent-auto-start-on-open.spec.ts` (CREATED button) stays green. + +## Risks + +- The desktop restart test's send-message path must prove auto-start-on-send + (same mock flow as today's post-click send; the mock agent answers + `/e2e:simple-message` after a resumed launch). +- Mobile restart flow is slower (desktop restart test already needs + `test.setTimeout(180_000)`); reuse the same timeouts. +- i18n ratchet: the hint copy must go through `t()` in the component, and the + new key must exist in all four locale files (`i18n:check` fails otherwise). + +## Verification + +```bash +(cd apps/web && pnpm run typecheck) +``` + +```bash +(cd apps/web && pnpm vitest run lib/session-state.test.ts components/task/chat/message-list-footer.test.tsx components/task/chat/message-renderer.test.ts components/task/chat/chat-input-area.test.tsx) +``` + +```bash +(cd apps/web && pnpm run i18n:check && pnpm run i18n:ratchet) +``` + +```bash +(cd apps/backend && go test ./internal/orchestrator/ -run TestQueueAndInterruptForPeerMessage_StoppedSessionResumesAgent -race) +``` + +```bash +(cd apps/web && KANDEV_E2E_MOCK=true pnpm e2e:raw --project=chromium settings/prevent-auto-start-on-open.spec.ts) +(cd apps/web && KANDEV_E2E_MOCK=true pnpm e2e:raw --project=mobile-chrome settings/mobile-agent-start-hint.spec.ts settings/mobile-prevent-auto-start-on-open.spec.ts) +``` + +## Implementation Waves And Parallel Candidates + +``` +Wave 1 (sequential): +- [x] [task-01-shared-predicate-and-removal](task-01-shared-predicate-and-removal.md) +- [x] [task-02-composer-hint-ui](task-02-composer-hint-ui.md) +- [x] [task-03-e2e](task-03-e2e.md) +- [x] [task-04-peer-message-resumes-stopped-session](task-04-peer-message-resumes-stopped-session.md) +``` + +Tasks 01→03 are sequential (02 depends on 01's predicate, 03 depends on 02's +rendered hint). Task 04 is backend-only and independent of the frontend +tasks; it can run at any point in the wave. + +## Verification Results + +- Frontend: `make typecheck` and `make fmt` clean; `make lint` clean (0 + warnings). Targeted vitest (session-state, message-list-footer, + message-renderer, use-composer-agent-start-hint, composer-agent-start-hint) + 38/38 passing. `pnpm run i18n:check` + `pnpm run i18n:ratchet` clean. +- Backend: `go test ./internal/orchestrator/ -count=1` clean (75s, includes + the new `TestQueueAndInterruptForPeerMessage_StoppedSessionResumesAgent` + with `-race`). +- E2E (mock agent): `settings/prevent-auto-start-on-open.spec.ts` on + chromium 3/3; `settings/mobile-agent-start-hint.spec.ts` + + `settings/mobile-prevent-auto-start-on-open.spec.ts` on mobile-chrome 2/2. +- Full `make test` was NOT clean on this VM, but every failure is unrelated + to this change: `internal/agentctl/server/{api,config,process}`, + `internal/launcher`, `internal/system/updates` reproduce on a clean tree + (host init-system/process-output environment tests), and the web suite's + 18 failures are load-flaky 5-10s timeouts (git-server, azure-devops, + sentry, file-browser, settings, git-base) that all pass in isolation on + both the clean tree and this branch. `lib/http-git-server.test.ts` fails + on the clean tree too. diff --git a/docs/plans/start-agent-composer-hint/task-01-shared-predicate-and-removal.md b/docs/plans/start-agent-composer-hint/task-01-shared-predicate-and-removal.md new file mode 100644 index 0000000000..0a74b0c682 --- /dev/null +++ b/docs/plans/start-agent-composer-hint/task-01-shared-predicate-and-removal.md @@ -0,0 +1,76 @@ +--- +id: "01-shared-predicate-and-removal" +title: "Shared hint predicate and footer resume-button removal" +status: done +wave: 1 +parallelism: sequential +depends_on: [] +plan: "plan.md" +spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +--- + +# Task 01: Shared hint predicate and footer resume-button removal + +## Acceptance + +- `shouldShowComposerAgentStartHint({ resumeSkipped, sessionState, + hasRecoveryActions, sessionId })` is exported from + `apps/web/lib/session-state.ts` and returns true only when ALL of: + `resumeSkipped === true`, `hasRecoveryActions === false`, `sessionState` is + not `FAILED` / `RUNNING` / `STARTING`, and `sessionId` is non-null. This is + the exact condition set the footer button used. +- Unit tests in `apps/web/lib/session-state.test.ts` cover: visible for a + resume-skipped `WAITING_FOR_INPUT` session; hidden when not resume-skipped; + hidden for `FAILED`; hidden for `RUNNING` and `STARTING`; hidden when + recovery actions are present; hidden when `sessionId` is null. +- `apps/web/components/task/chat/session-resume-start-button.tsx` is deleted + and no file imports `session-resume-start-button` anymore. +- `apps/web/components/task/chat/message-list-footer.tsx` no longer renders + `SessionResumeStartButton` and no longer reads + `resumeSkippedSessionIds` / computes `hasRecoveryActions` / + `showResumeStartButton` (those existed only for the button). +- `apps/web/components/task/chat/message-list-footer.test.tsx` has no + `SessionResumeStartButton` mock and no resume-button describe blocks; the + remaining footer tests (AgentStatus, failure presentation) still pass. +- `apps/web/components/task/chat/message-renderer.tsx`: + `TaskDescriptionStartButton`'s `handleStart` drops the now-unreachable + `resumeSkipped ? buildResumeRequest : buildStartCreatedRequest` branch and + always dispatches `buildStartCreatedRequest`; `shouldShowDescriptionStartButton` + stays CREATED-only. The test comment "the footer owns that surface" in + `message-renderer.test.ts` is updated to name the composer hint. + +## Verification + +```bash +(cd apps/web && pnpm run typecheck) +``` + +```bash +(cd apps/web && pnpm vitest run lib/session-state.test.ts components/task/chat/message-list-footer.test.tsx components/task/chat/message-renderer.test.ts) +``` + +## Files Likely Touched + +- `apps/web/lib/session-state.ts` (+ `session-state.test.ts`) +- `apps/web/components/task/chat/session-resume-start-button.tsx` (deleted) +- `apps/web/components/task/chat/message-list-footer.tsx` (+ test) +- `apps/web/components/task/chat/message-renderer.tsx` (+ test) + +## Dependencies + +None. The predicate is new and the removal is independent of the hint UI. + +## Inputs + +- Spec: "What" bullets 1 and 3, scenarios 1 and 5. +- Current footer predicate at `message-list-footer.tsx:62-88` + (`resumeSkipped`, `hasRecoveryActions`, `showResumeStartButton`). +- Current `TaskDescriptionStartButton` branch at `message-renderer.tsx:90-95` + and `shouldShowDescriptionStartButton` at `message-renderer.tsx:67-72`. + +## Output Contract + +No UI change yet (the hint renders in task 02): the resume-skipped session +simply has no Start agent button anywhere, and the shared predicate pins the +exact condition set the hint will reuse. The CREATED description button is +behaviorally unchanged. diff --git a/docs/plans/start-agent-composer-hint/task-02-composer-hint-ui.md b/docs/plans/start-agent-composer-hint/task-02-composer-hint-ui.md new file mode 100644 index 0000000000..87f688212e --- /dev/null +++ b/docs/plans/start-agent-composer-hint/task-02-composer-hint-ui.md @@ -0,0 +1,85 @@ +--- +id: "02-composer-hint-ui" +title: "Composer hint UI and i18n" +status: done +wave: 1 +parallelism: sequential +depends_on: ["01-shared-predicate-and-removal"] +plan: "plan.md" +spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +--- + +# Task 02: Composer hint UI and i18n + +## Acceptance + +- `useComposerAgentStartHint(sessionId, sessionState, messages, + footerActionMessages)` (new hook in `apps/web/components/task/chat/`) + returns `shouldShowComposerAgentStartHint(...)` with `resumeSkipped` read + from `state.tasks.resumeSkippedSessionIds[sessionId] === true` and + `hasRecoveryActions` derived from `messages`/`footerActionMessages` using + the same rule the footer used (`metadata.recovery_actions === true` on any + message). `sessionId === null` short-circuits to false. Hook unit tests + cover: resume-skipped + `WAITING_FOR_INPUT` + no recovery → true; FAILED / + STARTING / RUNNING → false; recovery action present in `messages` or + `footerActionMessages` → false; no resume-skipped flag → false. +- `apps/web/components/task/task-chat-panel.tsx` computes + `showAgentStartHint` from the hook (`session?.state`, `allMessages`, + `footerActionMessages`) and passes it through `ChatFooter` to + `ChatInputArea` (`apps/web/components/task/chat/chat-input-area.tsx`) as a + new optional `showAgentStartHint` prop. +- `chat-input-area.tsx` renders, above `QueueAffordance`, a single muted + one-line hint when `showAgentStartHint` is true: + `
{t("task:composerStartAgentHint")}
`. + It is absent when the prop is false/undefined. No click handler. +- Component test (extend `chat-input-area.test.tsx` or a focused test) + asserts the hint element renders for `showAgentStartHint` true and is + absent for false. +- i18n: `composerStartAgentHint` added to + `apps/web/src/locales/{en,pseudo,pt-pt,zh-cn}/task.json` with copy "Sending + a message will auto-start the agent." (plain punctuation; pseudo and + translations follow the existing per-locale style). `pnpm run i18n:check` + and `pnpm run i18n:ratchet` pass. + +## Verification + +```bash +(cd apps/web && pnpm run typecheck) +``` + +```bash +(cd apps/web && pnpm vitest run components/task/chat/chat-input-area.test.tsx components/task/chat/use-composer-agent-start-hint.test.ts) +``` + +```bash +(cd apps/web && pnpm run i18n:check && pnpm run i18n:ratchet) +``` + +## Files Likely Touched + +- `apps/web/components/task/chat/use-composer-agent-start-hint.ts` (new, + test) +- `apps/web/components/task/task-chat-panel.tsx` (`ChatFooter` props) +- `apps/web/components/task/chat/chat-input-area.tsx` (+ test) +- `apps/web/src/locales/en/task.json`, `pseudo/task.json`, + `pt-pt/task.json`, `zh-cn/task.json` + +## Dependencies + +Task 01 (the `shouldShowComposerAgentStartHint` predicate and the removal of +the footer button, so no second affordance competes with the hint). + +## Inputs + +- Spec: "What" bullet 3 (hint visibility rules), scenarios 6 and 7. +- `ChatInputArea` render at `chat-input-area.tsx:629-659` (the + `data-testid="chat-input-area"` wrapper above `QueueAffordance`). +- `ChatFooter` prop threading at `task-chat-panel.tsx:400-418` and + `484-528`. + +## Output Contract + +A recovered-idle session shows exactly one affordance: the composer hint +("Sending a message will auto-start the agent."), always visible because the +composer is outside the transcript scroll container. The hint disappears when +the session starts/runs, fails, or recovery actions are visible. The CREATED +description button remains the never-started affordance. diff --git a/docs/plans/start-agent-composer-hint/task-03-e2e.md b/docs/plans/start-agent-composer-hint/task-03-e2e.md new file mode 100644 index 0000000000..9cc152e74b --- /dev/null +++ b/docs/plans/start-agent-composer-hint/task-03-e2e.md @@ -0,0 +1,71 @@ +--- +id: "03-e2e" +title: "E2E: hint replaces resume button, desktop and mobile" +status: done +wave: 1 +parallelism: sequential +depends_on: ["02-composer-hint-ui"] +plan: "plan.md" +spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +--- + +# Task 03: E2E — hint replaces resume button, desktop and mobile + +## Acceptance + +- `apps/web/e2e/tests/settings/prevent-auto-start-on-open.spec.ts`, + "post-restart session does not auto-resume when the preference is on": + - after `backend.restart()` + `testPage.reload()`, asserts + `composer-agent-start-hint` is visible and + `session-resume-start-button` has count 0 (and "Resumed agent" never + appears on its own); + - sends `/e2e:simple-message` via the composer (`session.sendMessage`) and + asserts the mock response ("simple mock response") becomes visible and + "Resumed agent" appears — sending a message is the explicit start. + - `test.setTimeout(180_000)` retained; settings/task teardown unchanged. +- The two final-step tests in the same file still assert + `task-description-start-button` visibility/absence exactly as today + (CREATED button unchanged, setting-off control unchanged). +- `apps/web/e2e/tests/settings/mobile-prevent-auto-start-on-open.spec.ts` + (CREATED button on mobile) stays green unchanged. +- New `apps/web/e2e/tests/settings/mobile-agent-start-hint.spec.ts` + (`mobile-chrome` project, phone viewport from the project device, no + per-test `setViewportSize`): same restart pattern — task with + `/e2e:simple-message` description, first turn completes, `backend.restart()` + + `testPage.reload()`, then asserts `composer-agent-start-hint` visible, + `session-resume-start-button` count 0, sends a message, and asserts the + mock response appears. Timeouts mirror the desktop restart test + (`test.setTimeout(180_000)`). + +## Verification + +```bash +(cd apps/web && KANDEV_E2E_MOCK=true pnpm e2e:raw --project=chromium settings/prevent-auto-start-on-open.spec.ts) +(cd apps/web && KANDEV_E2E_MOCK=true pnpm e2e:raw --project=mobile-chrome settings/mobile-agent-start-hint.spec.ts settings/mobile-prevent-auto-start-on-open.spec.ts) +``` + +## Files Likely Touched + +- `apps/web/e2e/tests/settings/prevent-auto-start-on-open.spec.ts` +- `apps/web/e2e/tests/settings/mobile-agent-start-hint.spec.ts` (new) + +## Dependencies + +Task 02 (the hint must be rendered before the specs can assert it). + +## Inputs + +- Current restart test body at + `prevent-auto-start-on-open.spec.ts:117-166` (uses `backend.restart()` + + `testPage.reload()`, `SessionPage.waitForLoad`, + `session.sendMessage("/e2e:simple-message")`, + `session.expectChatResponseVisible("simple mock response", 0, …)`). +- Mobile restart flow mirrors the desktop one; the mobile-chrome project + auto-selects `mobile-*.spec.ts` files and supplies the device. + +## Output Contract + +Desktop and mobile E2E prove: a recovered-idle session shows the composer +hint and no Start agent button; sending a message starts the agent and the +mock response appears; the CREATED final-step button flows (desktop and +mobile) are regression-green. diff --git a/docs/plans/start-agent-composer-hint/task-04-peer-message-resumes-stopped-session.md b/docs/plans/start-agent-composer-hint/task-04-peer-message-resumes-stopped-session.md new file mode 100644 index 0000000000..cf39c19dc6 --- /dev/null +++ b/docs/plans/start-agent-composer-hint/task-04-peer-message-resumes-stopped-session.md @@ -0,0 +1,81 @@ +--- +id: "04-peer-message-resumes-stopped-session" +title: "Backend pin test: peer message resumes a stopped session" +status: done +wave: 1 +parallelism: sequential +depends_on: [] +plan: "plan.md" +spec: "../../specs/prevent-agent-autostart-on-open/spec.md" +--- + +# Task 04: Backend pin test — peer message resumes a stopped session + +## Acceptance + +- A new Go test in `apps/backend/internal/orchestrator/` + (`task_operations_test.go`, next to the other + `TestQueueAndInterruptForPeerMessage_*` tests) pins the requirement that a + message received from another task (sub-task via `message_task_kandev`, + delivered as `QueueAndInterruptForPeerMessage` with `QueuedByAgent`) + auto-starts the agent when the receiving session is stopped-but-resumable + (the post-restart / recovered-idle shape the prevent-auto-start preference + leaves the session in). +- Test shape: seed a task + session in `WAITING_FOR_INPUT` with a resume + token (an `executors_running` row carrying the token, like + `TestResumeTaskSession_*` / `TestEnsureSessionRunning_*` fixtures), an + agent manager reporting `isAgentRunning: false` (so `ensureSessionRunning` + takes the resume path) with a recording `LaunchAgent`, and the executor + wired like the existing resume-path tests. Call + `QueueAndInterruptForPeerMessage(ctx, taskID, sessionID, prompt, metadata)` + and assert the agent was launched (resume) and the queued message was + dispatched to the agent (PromptAgent reached), mirroring the assertions of + `TestQueueAndInterruptForPeerMessage_DeliversQueuedMessageWithoutUserCancelSideEffects` + but with the stopped-agent resume path instead of + `isAgentRunning: true`. +- The test must PASS against the current backend (the behavior already + exists: `takeIfPromptableLocked` → `executeQueuedMessage` → `PromptTask` → + `ensureSessionRunning` resumes). It is a regression pin, not a fix; if it + fails, stop and report — that would mean the requirement is actually + broken and needs a fix design. + +## Verification + +```bash +(cd apps/backend && go test ./internal/orchestrator/ -run TestQueueAndInterruptForPeerMessage_StoppedSessionResumesAgent -race) +``` + +## Files Likely Touched + +- `apps/backend/internal/orchestrator/task_operations_test.go` (new test) +- Possibly `apps/backend/internal/orchestrator/task_operations.go` ONLY if the + test proves the behavior is broken (then stop and report; do not fix + silently) + +## Dependencies + +None. Backend-only; independent of the frontend hint tasks. + +## Inputs + +- Spec: "What" bullet "A message auto-starts the agent covers every message…" + and the scenario "GIVEN a recovered-idle session whose composer hint is + visible, WHEN a message is received from another task…". +- `QueueAndInterruptForPeerMessage` at + `task_operations.go:5363-5423` (queues with `QueuedByAgent`, dispatches via + `takeIfPromptableLocked` when promptable). +- Resume-path fixture examples: `TestResumeTaskSession_*` at + `task_operations_test.go:5161-5204` (sets `AgentProfileID`, wires the + executor with the mock agent manager, asserts the resumed state and prompt + dispatch); `TestQueueAndInterruptForPeerMessage_*` at + `task_operations_test.go:1905-2920` (queue + dispatch assertions). +- `ensureSessionRunning` at `task_operations.go:1891` (resumes + WAITING_FOR_INPUT sessions with a resume token). + +## Output Contract + +The backend test suite proves: a peer (sub-task) message delivered to a +stopped-but-resumable session starts the agent and the message reaches the +agent. Combined with the frontend E2E (task-03), the hint's promise — +"a message auto-starts the agent" — is verified for both the user's composer +sends and received sub-task messages. diff --git a/docs/specs/prevent-agent-autostart-on-open/spec.md b/docs/specs/prevent-agent-autostart-on-open/spec.md index 88d8ca5808..bae1ab306a 100644 --- a/docs/specs/prevent-agent-autostart-on-open/spec.md +++ b/docs/specs/prevent-agent-autostart-on-open/spec.md @@ -1,9 +1,17 @@ --- status: draft created: 2026-08-11 +updated: 2026-08-13 owner: platform --- +> Follow-up (2026-08-13): for recovered-idle sessions the in-chat Start agent +> button was replaced by a hint in the chat composer ("sending a message will +> auto-start the agent"), because the button rendered at the bottom of the +> transcript and could be clipped when the transcript was not scrolled all the +> way down. The never-started (CREATED) task-description Start agent button is +> unchanged. The sections below describe the current behavior. + # Prevent Agent Auto-Start On Open ## Why @@ -22,16 +30,36 @@ open and start it explicitly instead. surfaced in Settings → Task Actions as a switch. - When the setting is ON, opening a task whose session exists but whose agent is not running (the post-restart / recovered-idle shape) MUST NOT - automatically resume the agent. The task opens with the agent stopped and a - Start agent button is shown in the chat; clicking it resumes the agent. - (The existing session-menu resume and chat composer remain available too.) - For FAILED-but-resumable sessions the existing recovery actions (Resume - session / Start fresh session buttons) remain the manual affordance; the - new Start agent button is not rendered for FAILED sessions. + automatically resume the agent. The task opens with the agent stopped and + the chat composer shows a hint that sending a message will auto-start the + agent; sending a message starts it (the explicit-user-action rule below). + The Start agent button is NOT shown for recovered-idle sessions: the + composer hint replaces it (a footer button could be clipped when the + transcript is not scrolled to the bottom). For FAILED-but-resumable + sessions the existing recovery actions (Resume session / Start fresh + session buttons) remain the manual affordance; the hint is not rendered for + FAILED sessions. - When the setting is ON, opening a task whose current workflow step is the final step of its workflow MUST NOT auto-start the agent when the task has no session. The session is created in the never-started (prepared) state - and the Start agent button is shown. + and the Start agent button is shown under the task description. This button + is unchanged: a task that was explicitly never started keeps its explicit + Start agent affordance (no hint replaces it, because the CREATED transcript + is short and the button cannot be clipped). +- The composer hint is a single informational line above the chat input, + shown exactly when the recovered-idle session is stopped and no other + affordance owns the surface: it is hidden while the session is FAILED + (recovery actions own the surface), while it is STARTING or RUNNING, and + when recovery actions are already visible in the transcript. The hint has + no click action; it only communicates that a message auto-starts the + agent. +- "A message auto-starts the agent" covers every message that reaches the + session's message queue, not only the user's composer sends: messages + received from other tasks (`message_task_kandev`, e.g. a sub-task + reporting back) are queued the same way and the queue dispatch resumes a + stopped-but-resumable agent through the same + `ensureSessionRunning` path. The preference never gates message-driven + starts — it only gates starts triggered by *opening* the task. - When the setting is OFF (default), opening a task behaves exactly as today. - The setting gates only automatic starts triggered by *opening* a task. It MUST NOT affect: workflow step transitions whose on-enter actions include @@ -84,10 +112,11 @@ auto-start or auto-resume is *triggered by opening the task page*: | Trigger | Setting OFF (default) | Setting ON | |---|---|---| | Open task, no session, step allows auto-start | agent starts (`created_start`) | non-final step: unchanged; final step: `created_prepare`, Start agent button shown | -| Open task, session exists, agent not running, session resumable | auto-resume (`session.launch` intent=resume) | no auto-resume; session stays stopped and a Start agent button (resume action) is shown in the chat | +| Open task, session exists, agent not running, session resumable | auto-resume (`session.launch` intent=resume) | no auto-resume; session stays stopped; the chat composer shows a hint that sending a message will auto-start the agent | | Open task, agent already running | no-op | no-op (unchanged) | | Workflow step transition with `auto_start_agent` on-enter | agent starts | agent starts (unchanged) | | User clicks Start agent / Resume / sends a message | agent starts | agent starts (unchanged) | +| Message received from another task (sub-task via `message_task`), agent stopped | agent resumes (queue dispatch runs `ensureSessionRunning`) | agent resumes (unchanged; message-driven starts are never gated by the preference) | ## Failure modes @@ -113,13 +142,14 @@ per-task variant in this feature. - **GIVEN** the setting is ON and a task's session is in an active state with a resume token but no running agent (post-restart shape), **WHEN** the user opens the task page, **THEN** no `session.launch` resume request is sent on - open, the session stays stopped, and the task page shows a Start agent - button that resumes the agent when clicked. + open, the session stays stopped, and the chat composer shows a hint that + sending a message will auto-start the agent; sending a message starts the + agent. - **GIVEN** the setting is ON and a task's session is FAILED but resumable, **WHEN** the user opens the task page, **THEN** no auto-resume is sent on open and the session keeps the existing recovery actions (Resume session and - Start fresh session buttons). The new Start agent button is not rendered for - FAILED sessions. + Start fresh session buttons). The composer hint is not rendered for FAILED + sessions. - **GIVEN** the setting is ON and the task's current workflow step is the final step, **WHEN** the user opens the task and it has no session, **THEN** `session.ensure` is called with `auto_start: false`, the session is created @@ -133,7 +163,18 @@ per-task variant in this feature. **THEN** today's auto-start and auto-resume behavior is unchanged. - **GIVEN** the setting is ON and a task in the final step already has a never-started (`CREATED`) session, **WHEN** the task is opened, **THEN** the - Start agent button is shown and no agent starts. + Start agent button is shown under the task description and no agent starts. +- **GIVEN** a recovered-idle session whose composer hint is visible, **WHEN** + a message is received from another task (e.g. a sub-task via + `message_task_kandev`), **THEN** the agent resumes automatically (the + message is queued and the queue dispatch runs `ensureSessionRunning`) and + the hint disappears once the session reports STARTING/RUNNING. +- **GIVEN** a recovered-idle session whose composer hint is visible, **WHEN** + the session state becomes STARTING or RUNNING (e.g. the user sent a + message), **THEN** the hint disappears. +- **GIVEN** a recovered-idle session, **WHEN** recovery actions are already + visible in the transcript, **THEN** the composer hint is hidden (recovery + owns the surface). - **GIVEN** the setting is ON, **WHEN** the user explicitly starts or resumes the agent (Start agent button, session menu Resume, or sending a message), **THEN** the agent starts normally.