Skip to content

Proposal: surface plan as a first-class entity with live tracking and a dedicated view #615

@yuruiz

Description

@yuruiz

Summary

Paseo currently treats "plan" as transient chat content. There is no dedicated surface for the plan an agent is executing, no way to watch it evolve over time, and no diff view when the agent revises it. This proposal makes plan a first-class entity alongside subagents (#532) and todos.

Why now

Two converging signals make this the right moment:

  1. Claude Code Plan Mode shifted to file-based storage in v2.0.51+. ExitPlanMode now fires with an empty input field — the actual plan body is written to a markdown file in a plans/ folder. Listening to the SDK stream alone is no longer enough to recover plan content. Refs:
  2. File-based planning skills are now the dominant pattern. planning-with-files (Manus-style task_plan.md + findings.md + progress.md), spec-kit (.specify/spec.md, plan.md, tasks.md), and similar workflows write plans to disk during execution. None of the major Claude Code clients (Happy, Claudia, Cline, Aider) render these as a dedicated, live-updating view — they are buried inline in the chat stream.

This is a clean market gap and a natural extension of the same surface area #532 is opening up for subagents.

Today in Paseo

The only plan-related issues/PRs to date are bug-fixes against existing in-stream plan rendering:

There is no model of "plan" as an entity, no persistence, no diffs, no separate view.

Proposed model

Add a Plan entity to the agent state, with the following minimal shape:

type Plan = {
  id: string;
  agentId: string;
  source: "claude-plan-mode" | "file" | "tool-output";
  filePath?: string;       // when source is file or claude-plan-mode (v2.0.51+)
  body: string;            // current rendered markdown
  status: "draft" | "approved" | "executing" | "completed" | "abandoned";
  createdAt: number;
  updatedAt: number;
  revisions: Array<{ at: number; body: string }>; // bounded ring buffer
};

A plan is created when any of the following triggers fire, and updated thereafter:

Trigger Provider Source
tool_use.name === "ExitPlanMode" with non-empty input.plan Claude (≤2.0.34) tool-output
tool_use.name === "ExitPlanMode" with empty input Claude (2.0.51+) resolve via file watcher on agent worktree plans/*.md
File created/changed at conventional paths any file (paths listed below)
Codex / OpenCode plan tool equivalents Codex / OpenCode tool-output

File watcher conventions (configurable, with sensible defaults):

  • plans/*.md (Claude Code 2.0.51+)
  • task_plan.md, findings.md, progress.md (planning-with-files)
  • .specify/spec.md, .specify/plan.md, .specify/tasks.md (spec-kit)
  • PLAN.md, TASK_PLAN.md (legacy / common ad-hoc)

The watcher lives in packages/server and pushes plan deltas to clients over the existing WebSocket protocol as a new plan.updated event (additive, optional fields, backward compatible per the WS schema rules).

UI

Where these surfaces live in the existing layout

Paseo's agent screen (packages/app/src/panels/agent-panel.tsx) is structured as:

┌──────────────────────────────────────────────────────────────┐
│  Workspace tab bar                                          │ ← workspace-screen.tsx
├──────────────────────────────────────────────────────────────┤
│                                                              │
│                                                              │
│   AgentStreamView                                            │ ← scrollable message
│   (messages, tool calls, file diffs, etc.)                  │   stream — fills the
│                                                              │   remaining vertical
│                                                              │   space
│                                                              │
│                                                              │
├──────────────────────────────────────────────────────────────┤
│   ┌──[ Subagent strip from #532 ]────────────────────────┐   │
│   │  ▸  ●  research-agent · 2 active                     │   │ ← inserted between
│   └──────────────────────────────────────────────────────┘   │   stream and composer
│                                                              │
├──────────────────────────────────────────────────────────────┤
│   AgentComposerSection (input, send button, attachments)    │ ← composer.tsx
└──────────────────────────────────────────────────────────────┘

Surface 1 (the plan strip) is a new sibling to the subagent strip from #532 — both live in a vertical stack between AgentStreamView and AgentComposerSection. Concretely:

  AgentStreamView                  ← scrollable, flex: 1
  ─────────────────────────
  PlanStrip          (new)         ← only present if a plan exists
  SubagentStrip      (#532)        ← only present if subagents exist
  ─────────────────────────
  AgentComposerSection             ← fixed height, anchored bottom

Order matters: plan above subagents because plan is the "what we are doing" context, subagents are the "who is doing it" — plan reads like a header for the subagent list. Both strips collapse to zero height when empty, so the composer stays visually anchored to the stream when there is nothing to show.

Surface 2 (the plan detail view) opens differently per form factor:

Form factor How it opens Why
Compact (phone, narrow web) Full-screen modal pushed onto the navigation stack Composer + stream already saturate the viewport; a side panel would crush both
Wide (tablet, desktop, Electron) New workspace tab registered in panel-registry, opened as a sibling pane Reuses the existing pane infrastructure (workspace-pane-content.tsx / panel-registry). User can then split-pane it next to the agent or swap it in the same pane. No new "side drawer" primitive needed.

This means on wide form factor, opening a plan does not cover the agent panel — the user can have the agent stream + plan detail side by side via the existing pane split. On compact, it's a modal because that's how every other detail surface in Paseo behaves.

A new panel kind plan-detail is registered in register-panels.ts, with target { kind: "plan-detail", agentId, planId }. Tapping the strip calls onOpenTab({ kind: "plan-detail", ... }), which the workspace already knows how to route.

Surface 1 — Plan strip

A persistent strip rendered between AgentStreamView and AgentComposerSection, sibling to the subagent strip from #532. When no plan exists for the current agent, the strip is absent (zero chrome cost).

Layout (single row, ~36–40px tall):

┌─────────────────────────────────────────────────────────────────┐
│ ▸  ●  Migrate auth to OAuth2          3/7 steps · 2m ago    ⋯  │
└─────────────────────────────────────────────────────────────────┘
   ↑   ↑   ↑                            ↑           ↑          ↑
   │   │   plan title (first H1 /       │           │          overflow
   │   │   first line, truncated)       │           timestamp  menu
   │   │                                │           of last
   │   status dot (color-coded)         step counter update
   expand/collapse chevron              (parsed from
                                        `- [ ]` / `- [x]`)
  • Status dot colors: gray = draft, blue = executing, green = completed, amber = abandoned. Pulses while a plan.updated event is in-flight (<500ms after a file change).
  • Tap title or chevron → opens Surface 2 (detail view).
  • Overflow menu (⋯): Open file in editor, Copy plan, Mark abandoned (read-only safe ops; no plan editing).
  • Multiple plans (e.g. spec-kit's three files): the strip becomes a horizontal pager with a 1/3 indicator; swipe (mobile) or arrow keys (web) cycle. We do not stack multiple strips — vertical real estate near the composer is precious.

Surface 2 — Plan detail view

Opens as a side pane on wide form factor (≥768px, via the existing pane split) and a full-screen modal on compact. Reuses useIsCompactFormFactor() per CLAUDE.md gating rules.

Header bar:

┌─────────────────────────────────────────────────────────────────┐
│ ←  Migrate auth to OAuth2                              ⋯   ✕   │
│    plans/migrate-auth.md · executing · updated 2m ago           │
├─────────────────────────────────────────────────────────────────┤
│  [ Current ]  [ Diff ]  [ History ]                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  (tab content)                                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Three tabs:

  1. Current — full markdown render of the latest plan body.

    • Checkbox items (- [ ] / - [x]) rendered as visual checkboxes (read-only — checking is a no-op with a tooltip "Plan is read-only in Paseo").
    • Headings get anchor links so deep-linking to a section works.
    • Live-updates in place when plan.updated fires; the changed region briefly highlights (1.5s yellow fade) so the user can see what just moved.
  2. Diff — previous revision vs current.

    • Reuses the existing diff gutter component.
    • Default comparison: revisions[N-1] vs current. A dropdown lets the user pick any earlier revision as the base.
    • Empty state when only one revision exists: "No prior revision yet — the plan will be diffed against its previous version after the next update."
  3. History — vertical timeline of revisions.

    • Each entry: timestamp, first-line summary, +N/−M line counts, tap → loads that revision into the Diff tab as the "right side" with current as the base.
    • Bounded to the last 20 revisions in-memory (per the entity definition); older ones collapse into "… N earlier revisions discarded".

Empty / loading / error states

State Strip Detail view
No plan detected Strip hidden N/A
File watcher just started, no event yet Hidden until first event N/A
Plan file deleted while open Strip dims to 50% opacity, shows "Plan file removed" Banner: "The underlying plan file no longer exists. Showing last known content."
Watcher error (permission, FS event flood) Strip shows red dot + "Plan tracking paused" Banner with retry button

Cross-platform notes

  • Native (iOS/Android): detail view uses native modal presentation; swipe-down to dismiss.
  • Web/desktop: detail view opens as a new pane in the existing workspace pane system; ESC closes; remembers last open pane width in user settings.
  • Hover affordances (overflow menu, revision summary preview): isHovered || isNative || isCompact per CLAUDE.md, so they're always visible on touch.
  • Diff/Current tab content uses existing markdown + diff renderers — no new rendering pipeline.

Out of scope for v1

  • Editing plan content (read-only is a hard constraint for v1).
  • Side-by-side diff (top/bottom split only initially; side-by-side is a follow-up).
  • Cross-agent plan comparison ("how does this plan differ from the one in agent X?").
  • Plan templates / library.

Phasing

  1. Phase 1 — entity + file watcher. Wire up the Plan entity, the daemon-side file watcher on conventional paths, and the WS event. No UI yet; verify via CLI (paseo plan show).
  2. Phase 2 — Claude ExitPlanMode resolution. Handle both the legacy input.plan path and the v2.0.51+ file-resolved path. Add a deterministic test for both.
  3. Phase 3 — UI. Strip + detail view + diff tab.
  4. Phase 4 — provider parity. Codex / OpenCode plan tool equivalents.

Each phase ships independently behind a flag and is reverted-without-regret if it doesn't pan out.

Non-goals

  • Editing plans from Paseo. Read-only first; editing is a separate, much bigger conversation about who owns the plan.
  • Replacing inline chat rendering. The plan still appears in the stream; this is an additional surface.
  • Plan execution orchestration (scheduling tasks, checking off items). That overlaps with Surface paseo subagents in a collapsible section above the composer #532 (subagents) and the todo work — keep them separate concerns.

Open questions

  1. Should plan revisions be persisted across daemon restarts, or only kept in-memory? (Lean: in-memory + on-disk file is the source of truth.)
  2. How do we handle two plans in the same agent worktree (e.g. spec-kit's three files)? Treat as one logical plan with sub-documents, or separate Plan entities? (Lean: separate, with optional grouping.)
  3. Backward compat for older clients: plan.updated is additive, but should the plan body also continue to appear inline so 6-month-old clients still see it? (Lean: yes, no regression on the inline path.)
  4. Is there a Codex equivalent of ExitPlanMode we can latch onto, or is it purely file-based there?

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions