feat(desktop): start a coding agent from New task's prompt-first composer, with Plan mode for a call with Luke

Merged
DA
12d ago
+1450-527

PR 2 of the "New task, prompt first" redesign, stacked on #3697. New task's name field is now the shared agent composer card. Its words start a coding agent on the new task. A Plan toggle brings back the old flow: plan template plus a call with Luke.

Summary

  • Composer card (tasks/new-task-composer.tsx, mounted by the dialog so PR 8 can put it on Home): this is the agent box itself (AgentComposer), not a fork of it. It has the words field, "+"/paste/drop for files, dictation, the model chip and the round Start button. The repository chip and #3697's LinearIssueChip now sit in its foot. The workspace chip stays in the title. To make this possible, AgentComposer takes a few optional slots: wording, above, tools, actions, sendable, onKey, onLeave, focusedByOwner. Its servers prop is now optional. The pickers' Escape fallback can belong to the owner.
  • Mode (@luke/panel/tasks/new-task-mode replaces @luke/panel/tasks/new-task-planning): AGENT | PLAN. You switch it with the Plan toggle chip or with Shift+Tab from the field. It is kept under a new key, luke.new-task-mode, and defaults to Agent. The old Plan switch's key is ignored.
  • Agent mode: Start is enabled once there are words, files, or a picked issue, plus a repository. Without a repository, Start is disabled and the card says "An agent needs a repository to work in. Choose one, or turn on Plan." The model chip starts on the account default. A pick applies only to the agent this card starts. Since #3718 on main, only Settings writes the default, so this PR follows that rather than the original spec's "a pick becomes the default". Start sends TASKS_START with planning: false, plus the repository, the workspace, linearIssueId, and the issue's title as the name. startTask then gives the new task a draft agent through useCodingAgents.onHandDraft. That draft holds the prompt, the attachments and the chosen model, uses a fresh idempotency key, and is marked sendsOnOpen. The task opens on that draft's tab (mainTabs.onShow). DraftAgentTab sends it once start.available is true. If the agent start is refused, you stay on the draft tab with your words, the reason and Retry, which is the draft tab's existing behaviour.
  • Linear issue: choosing an issue adds a chip above the words (key, title, state, ×) and starts nothing. In Agent mode the agent's prompt is agentPromptOf(issue, words) in @luke/ui/tasks-model: [@KEY Title](url), a blank line, then the words, or the mention alone if there are no words. This comes with a unit test. Switching workspace drops a picked issue.
  • Plan mode: same start as before (planning: true, the call opens), using the issue if one was picked. The model chip is hidden and Start is always enabled. Until PR 5, the words become the task name: first line, trimmed to TASK_BOUNDS.MAX_NAME_CHARS (taskNameOfWords).
  • Keys: Return starts, Shift+Return adds a line. ⌘Return on an Agent-mode box with no words and no files makes a bare task (planning: false, no agent). Escape closes an open menu or dictation first, then the card. ⌘L opens Linear, and is ignored while an input method is composing, as in #3697.
  • Task name refresh: when an agent of the open task gets its first title (going from no title to a title) and the task is still Untitled, the window sends TASKS_REFRESH once, so the name PR 3 gives it on the service shows up.
  • Works with #3693's Start agent… (merged from main through the base): the issue's own preset (agent: true) keeps the card in Agent mode with no Plan toggle and titles it "New agent from …". In Agent mode, a card opened on a workspace issue with nothing written starts the agent on the task's plan through NewTaskStart.agent, as #3693 does. Once words or files are written, they go to the agent as NewTaskStart.handed. newTask.start keeps its five parameters.
  • Cancel and Create task are gone. The round Start button and Escape replace them.

Screenshots

These come from the real NewTaskDialog, drawn in Chromium with fake data in the dark theme, not from the packaged app.

Agent mode, words typed:

Agent mode with words typed

A Linear issue attached above the words:

A Linear issue attached

Plan mode (Shift+Tab): the model chip is hidden:

Plan mode

No repository: Start is disabled and the reason shows under the card:

No repository

Evidence

  • pnpm check:affected: it ran out of memory in the sandbox (typecheck, discarded-effect and desktop-shell.test.ts were OOM-killed, and app-menu.test.ts needs an Electron binary the sandbox lacks). I re-ran each of those on its own:

    • tsc --noEmit for @luke/desktop and @luke/ui: pass.
    • pnpm discarded-effect (6 GB heap): "No discarded Effect, Stream, or Layer in 2358 files."
    • pnpm knip, pnpm lint (biome + oxlint), scripts/repository-checks.sh (task-names included): pass.
    • vitest run on new-task-dialog, new-task-workspace, draft-agent-tab, use-tasks-tab, desktop-shell, modals, the agent-composer* tests and packages/ui/src/tasks-model.test.ts: pass.
    • In the full desktop vitest run, the only failure apart from the OOM and the missing Electron binary was modals.test.ts. I fixed it: the card now holds focus until the editor lands.
  • Screenshots: drawn from the real NewTaskDialog in headless Chromium, not the packaged app (evidence.sh only runs on macOS). Drawing it showed two bugs, now fixed: the foot's chips were squeezed to ellipses in the 520px card (fixed with a new 620px DIALOG_SIZE.COMPOSER), and the issue chip's Linear mark was unsized.

  • Bundle budget: the renderer/panel.js baseline moves from 901,571 to 946,875 gzipped bytes (+45,304), so the ceiling moves from 946,649 to 994,218. Measured at this head, panel.js and the chunks it loads come to 946,875 bytes. Main alone measures about 946,165, already about 480 bytes under the old ceiling: most of the gap between the old baseline and today is main's own growth, which used up the 5% slack. This PR adds about 710 bytes, all in panel.js's own chunk (95,596 → 96,306), because New task now draws the agent composer card. That is what pushed it over. Only that one entry was re-recorded, at today's measured size.

Tests (new and rewritten)

  • new-task-dialog.test.ts:
    • Agent mode starts the task with planning off and an agent with the prompt and the chip's model.
    • A model pick applies to this agent only and the default is left alone.
    • Shift+Tab or the toggle switches to Plan mode, which starts the call and is named by the first line.
    • Plan mode starts empty or with no repository.
    • No repository blocks Agent mode and shows why.
    • The mode is remembered, and the old key is ignored.
    • ⌘Return on an empty box makes a bare task.
    • A picked issue becomes a chip and then a mention in the prompt; its × drops it.
    • Linear search, ⌘L and the issue-preset cases, adapted.
  • draft-agent-tab.test.ts: a handed draft sends itself once it can start, and a refusal leaves the words with Retry.
  • use-tasks-tab.test.ts:
    • Agent mode hands the new task a draft that sends itself and opens its tab.
    • An agent's first title on an Untitled task asks TASKS_REFRESH, and a named task asks nothing.
  • tasks-model.test.ts: agentPromptOf and taskNameOfWords.
  • new-task-workspace.test.ts and desktop-shell.test.ts: Create task and Cancel changed to Plan mode's Start and Escape.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.runPromise/runSync/runFork outside the edges.
  • No new raw async primitives.
  • No discarded Effect, Stream, or Layer.
  • Test count: +18 new or rewritten cases across the files above; old Create-task/Cancel/name-field cases removed or rewritten.
  • new-task-planning.ts deleted, replaced by new-task-mode.ts.
  • No CLAUDE.md/AGENTS.md sentence names a changed part.
  • PRIVACY.md unchanged.
  • Package deps unchanged.
  • Barrel/door rule unchanged.

Notes

  • The bare task (⌘Return) still uses a picked issue. If an issue is picked, it names the task and links to it. ⌘Return with words or files in the box acts like Return.
  • A task opened from a workspace issue gets the issue through the task (its link and the plan the agent starts on), not as a mention in the prompt, because the renderer has no URL for workspace issues.
  • The repository and Linear chips keep their bordered field look in the foot. Their menus' placement is unchanged from #3697.
  • Out of scope: the brief (PR 5), the one-call start (PRs 6/7), Home (PR 8).

Made with Luke


Devin Review