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.goisProcesses,Agent Tree, andScratchpadsonly. - 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.goandinternal/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.
Recommended Scope
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 taskactions for focused tasks and task-bound children. - Thread
TaskIDthrough children spawned from task context. - Expose task context and
task_register_worktreeonly 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.tmpthen 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)toStart(argv, env, workDir, cols, rows). - Set
cmd.Dir = workDirwhen non-empty.
Update call sites:
internal/app/child.go: startPTYpassesc.WorkDir.internal/harness/session.goandinternal/harness/restart_persist_test.gopass an empty workdir or project dir as appropriate.cmd/spike/main.gopasses 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 stringtoChild. - Add
taskIDtonewChildEntry.
Modify internal/app/session.go:
- Add
TaskID stringtoSpawnSpec. - 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, andLaunchTerminalto accept this context or an equivalent minimal option. - Generic palette spawns pass empty
TaskIDeven when a task is focused. - Explicit task actions pass
TaskID. - MCP spawns inherit
TaskIDfrom 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)duringRun, near scratchpads/trust/persist stores. - Add
tasks *task.StoretouiState. - Add
focusedTaskID string, mutually exclusive withfocusedIDandfocusedPad. - Add
tasksCacheMu/tasksCache, mirroringpadsList(). - Add
tasksList(),invalidateTasksCache(), andtasksChanged(). - Pass the task store to
newToolHost.
Modify internal/app/host.go:
- Add
tasks *task.StoretotoolHost. - Extend
newToolHostto accept it. - Add a small
taskSinkinterface liketasksChanged()so MCP worktree registration can refresh the sidebar/detail view.
5. Make Tasks First In The Sidebar
Modify internal/app/sidebar.go:drawSidebar:
- Render
TasksbeforeProcesses. - 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 worktreesor2w. - Keep current
Processes,Agent Tree, andScratchpadssections after Tasks. - Do not add mouse or click behavior in this pass.
Modify internal/app/tree.go:
- Extend
navEntrywithtaskID stringandisTask(). - Change
empty()to include task entries. - Change
sidebarNavandnextNavEntryto accept tasks and order entries astasks -> processes -> active agent tree -> scratchpads. - Keep
nextChildIDcompatibility tests working for process-only behavior.
Modify internal/app/app.go:
- Add
focusTask(taskID string). - Add
renderTaskView(taskID string)andrepaintFocusedTask(). - Update
focusProcessandfocusScratchpadto clearfocusedTaskID. - Update
repaintFocused,repaintFocusedWithChrome,restoreViewinsideclosePalette, and Ctrl-W/S pending nav handling to route task entries correctly. - Update
drawStatusLineto showtask: <title>when a task is focused, and optionallytask: <title>when a focused child is task-bound. - Update
renderEmptyStatecopy to mention tasks, for examplePress 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 stringtopaletteAction. - Add task action kinds:
task-create-formtask-create-submittask-switchtask-rename-formtask-rename-submittask-start-agent
- Add a task macro if useful, but do not make it required for the first patch.
- Reuse
renameFormfor create/rename by allowing subjecttaskand different submit kinds.
Palette item behavior:
- Global/Open group includes
Create task...andOpen task: <title>. - When a task is focused, show
Rename taskandStart agent for task: <preset>rows. - When a task-bound child is focused, show
Open task: <title>andStart another agent for task: <preset>rows. - Normal
Spawn agent: <preset>remains unscoped and passes emptyTaskID.
Modify internal/app/app.go:openPaletteLocked:
- Pass
tasksList(),focusedTaskID, and current focused child task information intonewPalette.
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, andTaskRegisterWorktreeArgs. - Add optional
Task *TaskInfojson:"task,omitempty"`` toWhoAmIandProjectStatus. - Do not add task fields to
ProcessInfoin this pass, so unbound agents cannot discover task assignments throughlist_processes. - Extend
ToolHostwith:
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
serverInstructionspath with a helper that can append task-bound instructions only whenhost.CallerTask(callerID)is present. - Change
toolCatalog(role)totoolCatalog(role, taskBound bool). - Advertise
task_register_worktreeonly whentaskBoundis true. - Keep
spawn_agenthidden 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) stringbased onChild.TaskID. - Implement
CallerTaskby resolving the caller child and task store entry. WhoAmIincludesTaskonly when the caller child has a valid task ID.GetProjectStatusincludes the same optional task only for task-bound callers.SpawnAgentandSpawnProcessinheritTaskIDfrom the caller.- Replace
wrapSubAgentPromptwith 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 calleragent_instructionsis empty. RegisterTaskWorktreeresolves relative paths against the caller childWorkDir, falling back to the project dir, cleans/absolutizes the path, stores it, and triggerstasksChanged().
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.goor focused launch tests:TaskIDlands on spawned child.- MCP spawn from task-bound caller propagates
TaskIDto child. - Generic palette spawn while
focusedTaskIDis set keepsTaskID == "". - Explicit
task-start-agentsetsTaskIDand 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 omitstask_register_worktree; task-bound tools/list includes it.internal/mcp/tools.gotests or app host tests: unbound registration is rejected.internal/app/host_test.go: task-boundwhoamiincludes task; unboundwhoamiomits 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
Tasksand the task title. - Start a fake agent through
Start agent for task. - Use MCP
whoamifrom 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 agentwhile the task is selected and confirm it is not task-bound. - From a task-bound fake/test agent, call
whoamiand confirm task context appears. - From an unbound agent, call
whoamiand confirm no task context appears. - From a task-bound agent, call
task_register_worktreeand 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.