Files
patterm/.opencode/plans/1782293186639-eager-falcon.md
2026-06-24 12:28:56 +01:00

15 KiB

Task Sidebar Simplification Plan

User Decisions

  • First pass is manual project-local tasks only. Future GitHub/Linear linking should not be built now.
  • The right sidebar should put Tasks first, while keeping existing process, agent tree, and scratchpad navigation secondary.
  • Generic palette-spawned top-level agents must not inherit a selected task. Only explicit task-scoped actions, such as Start agent for task, create task-bound agents.
  • Agents should receive task context only when they are task-bound, and task-bound agents should be told to register task worktrees through MCP.

UX Review: Missing Pieces Today

  • There is no task model. The current right rail in internal/app/sidebar.go is Processes, Agent Tree, and Scratchpads only.
  • There is no task list, task detail view, task-focused navigation entry, or visible way to discover existing tasks.
  • Task creation is absent. Existing creation flows are process/agent palette actions in internal/app/palette.go and internal/app/app.go.
  • There is no explicit task-scoped launch action, so a user cannot intentionally start an agent for a task.
  • MCP has no task context and no worktree registration tool, so agents cannot tell patterm which worktrees belong to a task.
  • Current process rows are not grouped by task and do not surface task membership.
  • Sidebar empty states are generic process/scratchpad placeholders and do not advertise task creation.
  • The sidebar has no click or row activation model. First pass should stay keyboard/palette driven rather than inventing mouse interactions.
  • Cleanup/status/archive and external issue links are real task-list UX needs, but they are out of scope for this first simplification pass unless requested later.

Implement a minimal first-class task feature:

  • Persist manual tasks per project.
  • Render Tasks as the first sidebar section.
  • Let users create, open, and rename tasks through the palette.
  • Render a focused task detail view in the main viewport.
  • Add explicit Start agent for task actions for focused tasks and task-bound children.
  • Thread TaskID through children spawned from task context.
  • Expose task context and task_register_worktree only to task-bound MCP callers.
  • Fix PTY working-directory propagation, because task worktree paths are otherwise misleading.

Do not add GitHub/Linear linking, task import, agent-side task creation, task list MCP tools, click handling, or broad task/project management UI in this pass.

Implementation Steps

1. Add Project Task Store

Add internal/task/task.go and internal/task/task_test.go.

Persist to $XDG_DATA_HOME/patterm/projects/<projectKey>/tasks.json, matching the existing project-local persistence pattern in internal/persist/persist.go.

Minimal structs:

type Task struct {
    ID        string     `json:"id"`
    Title     string     `json:"title"`
    CreatedAt string     `json:"created_at"`
    UpdatedAt string     `json:"updated_at"`
    Worktrees []Worktree `json:"worktrees,omitempty"`
}

type Worktree struct {
    Path               string `json:"path"`
    Branch             string `json:"branch,omitempty"`
    CreatedByProcessID string `json:"created_by_process_id,omitempty"`
    RegisteredAt        string `json:"registered_at"`
}

Store API:

Open(projectKey string) (*Store, error)
(*Store).List() []Task
(*Store).Get(id string) (Task, bool)
(*Store).Create(title string) (Task, error)
(*Store).Rename(id, title string) (Task, error)
(*Store).RegisterWorktree(taskID string, wt Worktree) (Task, error)

Keep it deliberately small:

  • Generate opaque IDs like task_<hex>.
  • Validate non-empty titles.
  • Use stable list ordering.
  • De-duplicate worktrees by cleaned absolute path.
  • Atomic write via tasks.json.tmp then rename.
  • No status, assignee, priority, external refs, labels, or issue links yet.

2. Fix PTY Working Directory

Change internal/pty/pty.go:

  • Update Start(argv, env, cols, rows) to Start(argv, env, workDir, cols, rows).
  • Set cmd.Dir = workDir when non-empty.

Update call sites:

  • internal/app/child.go: startPTY passes c.WorkDir.
  • internal/harness/session.go and internal/harness/restart_persist_test.go pass an empty workdir or project dir as appropriate.
  • cmd/spike/main.go passes an empty workdir.

Add a focused PTY/app test that spawns sh -lc pwd with a temp workdir and verifies the child runs there.

3. Thread Task Context Through Runtime State

Modify internal/app/child.go:

  • Add TaskID string to Child.
  • Add taskID to newChildEntry.

Modify internal/app/session.go:

  • Add TaskID string to SpawnSpec.
  • Pass it into newChildEntry.
  • Keep persistence limited to top-level command entries. Do not persist agent/task process trees in processes.json.

Modify internal/app/launch.go:

  • Avoid adding more positional string parameters. Introduce a small launch context struct, for example:
type LaunchContext struct {
    ParentID string
    TaskID   string
    WorkDir  string
}
  • Update LaunchAgent, LaunchCommandPreset, LaunchCommandArgv, and LaunchTerminal to accept this context or an equivalent minimal option.
  • Generic palette spawns pass empty TaskID even when a task is focused.
  • Explicit task actions pass TaskID.
  • MCP spawns inherit TaskID from the caller only when the caller is task-bound.

4. Wire Task Store Into App State

Modify internal/app/app.go:

  • Open task.Open(opts.ProjectKey) during Run, near scratchpads/trust/persist stores.
  • Add tasks *task.Store to uiState.
  • Add focusedTaskID string, mutually exclusive with focusedID and focusedPad.
  • Add tasksCacheMu / tasksCache, mirroring padsList().
  • Add tasksList(), invalidateTasksCache(), and tasksChanged().
  • Pass the task store to newToolHost.

Modify internal/app/host.go:

  • Add tasks *task.Store to toolHost.
  • Extend newToolHost to accept it.
  • Add a small taskSink interface like tasksChanged() so MCP worktree registration can refresh the sidebar/detail view.

5. Make Tasks First In The Sidebar

Modify internal/app/sidebar.go:drawSidebar:

  • Render Tasks before Processes.
  • Show empty state as something actionable, for example (Ctrl-K create task).
  • Show task rows with focus marker, title, and a compact suffix such as 2 worktrees or 2w.
  • Keep current Processes, Agent Tree, and Scratchpads sections after Tasks.
  • Do not add mouse or click behavior in this pass.

Modify internal/app/tree.go:

  • Extend navEntry with taskID string and isTask().
  • Change empty() to include task entries.
  • Change sidebarNav and nextNavEntry to accept tasks and order entries as tasks -> processes -> active agent tree -> scratchpads.
  • Keep nextChildID compatibility tests working for process-only behavior.

Modify internal/app/app.go:

  • Add focusTask(taskID string).
  • Add renderTaskView(taskID string) and repaintFocusedTask().
  • Update focusProcess and focusScratchpad to clear focusedTaskID.
  • Update repaintFocused, repaintFocusedWithChrome, restoreView inside closePalette, and Ctrl-W/S pending nav handling to route task entries correctly.
  • Update drawStatusLine to show task: <title> when a task is focused, and optionally task: <title> when a focused child is task-bound.
  • Update renderEmptyState copy to mention tasks, for example Press Ctrl-K to create a task or spawn an agent/process.

Focused task detail view should be read-only and compact:

  • Title and task ID.
  • Registered worktrees.
  • Task-bound running children if cheap to derive from Session.Children().
  • Hints: Ctrl-K task actions, Ctrl-W/S navigate.

6. Add Palette Task Actions

Modify internal/app/palette.go:

  • Add task list and focused task fields to paletteState.
  • Add taskID string to paletteAction.
  • Add task action kinds:
    • task-create-form
    • task-create-submit
    • task-switch
    • task-rename-form
    • task-rename-submit
    • task-start-agent
  • Add a task macro if useful, but do not make it required for the first patch.
  • Reuse renameForm for create/rename by allowing subject task and different submit kinds.

Palette item behavior:

  • Global/Open group includes Create task... and Open task: <title>.
  • When a task is focused, show Rename task and Start agent for task: <preset> rows.
  • When a task-bound child is focused, show Open task: <title> and Start another agent for task: <preset> rows.
  • Normal Spawn agent: <preset> remains unscoped and passes empty TaskID.

Modify internal/app/app.go:openPaletteLocked:

  • Pass tasksList(), focusedTaskID, and current focused child task information into newPalette.

Modify internal/app/app.go:closePalette:

  • task-create-submit: create task, invalidate cache, focus the new task.
  • task-switch: focus task.
  • task-rename-submit: rename task, invalidate cache, redraw/focus task.
  • task-start-agent: launch selected agent preset with the task ID and a task initial prompt.

Task initial prompt should be injected only for task-bound agents. Keep it single-line because agent input submission is line-sensitive:

[system: you are working on patterm task "<title>" (<task_id>). If you create or use git worktrees for this task, call task_register_worktree with the path and branch.]

Include known registered worktrees in the prompt only if the list is short; otherwise rely on whoami.

7. Add Conditional MCP Task Context And Worktree Registration

Modify internal/mcp/tools.go:

  • Add MCP structs TaskInfo, TaskWorktree, and TaskRegisterWorktreeArgs.
  • Add optional Task *TaskInfo json:"task,omitempty"`` to WhoAmI and ProjectStatus.
  • Do not add task fields to ProcessInfo in this pass, so unbound agents cannot discover task assignments through list_processes.
  • Extend ToolHost with:
CallerTask(processID string) (TaskInfo, bool)
RegisterTaskWorktree(callerID string, args TaskRegisterWorktreeArgs) (TaskInfo, error)

TaskRegisterWorktreeArgs should contain only path and optional branch. Do not accept a task ID from the caller; the host infers the task from callerID.

Modify internal/mcp/protocol.go:

  • Replace the constant-only serverInstructions path with a helper that can append task-bound instructions only when host.CallerTask(callerID) is present.
  • Change toolCatalog(role) to toolCatalog(role, taskBound bool).
  • Advertise task_register_worktree only when taskBound is true.
  • Keep spawn_agent hidden from sub-agents as today.

Modify internal/mcp/tools.go:callTool:

  • Add case task_register_worktree.
  • Reject unbound callers with role_forbidden.
  • Call h.RegisterTaskWorktree(callerID, args) and return the updated task.

Modify internal/app/host.go:

  • Add helper callerTaskID(callerID string) string based on Child.TaskID.
  • Implement CallerTask by resolving the caller child and task store entry.
  • WhoAmI includes Task only when the caller child has a valid task ID.
  • GetProjectStatus includes the same optional task only for task-bound callers.
  • SpawnAgent and SpawnProcess inherit TaskID from the caller.
  • Replace wrapSubAgentPrompt with a small prompt builder that can include sub-agent instructions, task instructions, or both. Task instructions should be added whenever the spawned agent is task-bound, even when caller agent_instructions is empty.
  • RegisterTaskWorktree resolves relative paths against the caller child WorkDir, falling back to the project dir, cleans/absolutizes the path, stores it, and triggers tasksChanged().

Important security/UX rule:

  • Unbound MCP callers should not receive task context, task tool availability, task worktree data, or task IDs.

8. Tests

Add or update focused unit tests before broad harness work.

Task store:

  • internal/task/task_test.go: create/list persists across reopen.
  • Validate empty title errors.
  • Rename updates title and updated_at.
  • Register worktree de-duplicates cleaned absolute paths.

Runtime/spawn:

  • internal/app/session_test.go or focused launch tests: TaskID lands on spawned child.
  • MCP spawn from task-bound caller propagates TaskID to child.
  • Generic palette spawn while focusedTaskID is set keeps TaskID == "".
  • Explicit task-start-agent sets TaskID and injects task prompt.

Sidebar/navigation/palette:

  • internal/app/tree_test.go: task nav entries precede processes, agent tree, and scratchpads.
  • internal/app/palette_context_test.go: focused task shows task actions; generic spawn remains unscoped.
  • Add render-level tests only where existing helpers make that cheap; avoid brittle full-frame snapshots.

MCP:

  • internal/mcp/protocol_test.go: unbound tools/list omits task_register_worktree; task-bound tools/list includes it.
  • internal/mcp/tools.go tests or app host tests: unbound registration is rejected.
  • internal/app/host_test.go: task-bound whoami includes task; unbound whoami omits task.
  • internal/app/host_test.go: registration stores an absolute path and returns updated task info.

PTY working dir:

  • Add the smallest test proving a PTY child honors configured WorkDir.

Harness:

  • Add one lightweight scenario under internal/harness/scenarios/:
    • Open palette.
    • Create a task.
    • Assert sidebar shows Tasks and the task title.
    • Start a fake agent through Start agent for task.
    • Use MCP whoami from that fake agent if feasible, or assert the prompt/context path indirectly through captured output.

9. Changelog

Update CHANGELOG.md under [Unreleased].

Suggested bullets:

### Added
- Added project-local manual tasks in the right sidebar, with palette actions to create tasks, open tasks, rename tasks, and start task-scoped agents.
- Added task-bound MCP context and `task_register_worktree` so agents launched for a task can register git worktrees they create.

### Fixed
- Child PTYs now honor their configured working directory when launched.

Verification Commands

Run focused checks first:

go test ./internal/task/...
go test ./internal/pty/...
go test ./internal/app/...
go test ./internal/mcp/...

Then run broader checks:

go test ./internal/harness/...
go test ./...
go build -o ./bin/patterm ./cmd/patterm

Manual TUI smoke test:

./bin/patterm --project /home/harry/Dev/patterm

Manual flow to verify:

  • Press Ctrl-K, create a task, and confirm it appears first in the sidebar.
  • Focus the task from the sidebar navigation with Ctrl-W/S.
  • Press Ctrl-K, start an agent for that task, and confirm the task remains visible and the agent is task-bound.
  • Start a normal agent through Spawn agent while the task is selected and confirm it is not task-bound.
  • From a task-bound fake/test agent, call whoami and confirm task context appears.
  • From an unbound agent, call whoami and confirm no task context appears.
  • From a task-bound agent, call task_register_worktree and confirm the task detail view/sidebar suffix updates.

If harness tests fail with Unix socket or PTY permission errors, rerun them in an environment that permits sockets and PTYs, as documented in AGENTS.md.