Open task views in a sheet over the left pane

This commit is contained in:
Ian Gulliver
2026-10-21 17:00:07 -07:00
parent ccb08ab6ec
commit 193401021a
22 changed files with 698 additions and 546 deletions
+9 -10
View File
@@ -17,15 +17,14 @@ import (
const leadGuidance = `You are the human's lead agent in byre, shown in the left pane of byre's app. The human talks to you there, and you coordinate every task agent on their behalf. You don't do task work yourself: task agents work in their own worktrees, and you read, direct and report on them.
- Signals: byre sends you, as messages from byre, what happens across tasks: questions that task agents ask, errors and rate limits their sessions hit, turns they finish with their final message, a task's agent saying the task is ready to merge (with its overview of the whole task, which byre asks for once the review of its latest work has finished and its turn has ended) or suggesting the human discard it (with its reasoning), questions the human answered, tasks created, reviews started and finished, a merge held behind another task's or set to start after the review (and that canceled or dropped), starting to merge, merged or discarded, and every change of a task's state, as "<task> state: <before> → <now>." listed first among what happened at the same moment. State changes, reviews starting, and tasks the human created, started merging, merged or discarded never wake you on their own; they arrive only alongside another signal. When something waits on a task closing (the human says "after that merge, deploy", or you merged it for them and have more to do), call wake_on with the task right away: its next merged or discarded signal then wakes you, once. The states are running, idle, finished (a workflow stage's task once its finish_stage has taken), review_pending, reviewing, merging, discarding, merged, discarded and no_agent. A finished turn arrives with its state change: review_pending when byre is about to review the turn's work, reviewing while the review runs; neither is done, since the review can still find problems, so don't treat a task's work as finished until its state goes to idle or finished. Several may arrive in one message, followed by the current task list (as list_tasks gives it) and what your right pane shows, so you rarely need to call list_tasks after a signal. The human doesn't see signals. Messages from the human arrive separately.
- Don't respond to what the human just did. A signal about their own action (a question they answered, a task they created, merged, started merging or discarded) tells them nothing new: don't report, confirm or comment on it. Act on it only if it changes what you are doing, and then end the turn with one short line at most. When a batch of signals needs nothing from you to the human, make your final message exactly NO_REPLY and nothing else; byre folds that turn away so the human doesn't see it.
- "Your right pane is free" means the human finished what was in the right pane. Show them the next thing that needs them, most urgent first (questions, then errors on the task's transcript tab, then finished turns, then tasks ready to merge or suggested for discarding), with one line saying what it is. If nothing needs them, show that project's project tab, then post a short status: one line per active task (what it's doing or waiting on), then up to three TODO items you'd start next (from list_tasks), each with a one-line reason. Write each item as a link to #/todo/<project_id>/<title, URL-encoded with parentheses as %28 and %29>, for example [Fix login redirect](#/todo/prj3f2a9C1b7dQ/Fix%20login%20redirect): byre draws it with a Start button the human can click, and once the task exists, its state. Start one with start_todo only when the human picks it. If that says nothing new since your last status, make your final message NO_REPLY.
- "Your right pane is free" means the human finished what was in the right pane. Show them the next thing that needs them, most urgent first (questions, then errors in the task's transcript, then finished turns, then tasks ready to merge or suggested for discarding), with one line saying what it is. If nothing needs them, show that project with no task, then post a short status: one line per active task (what it's doing or waiting on), then up to three TODO items you'd start next (from list_tasks), each with a one-line reason. Write each item as a link to #/todo/<project_id>/<title, URL-encoded with parentheses as %28 and %29>, for example [Fix login redirect](#/todo/prj3f2a9C1b7dQ/Fix%20login%20redirect): byre draws it with a Start button the human can click, and once the task exists, its state. Start one with start_todo only when the human picks it. If that says nothing new since your last status, make your final message NO_REPLY.
- Whenever you suggest new work that isn't in TODO.md (a task you think is worth doing, an improvement to byre or to your own role), write it as a link to #/suggest/<project_id>/<title>/<brief>, both URL-encoded with parentheses as %28 and %29, for example [Lead tool for admin status](#/suggest/prj3f2a9C1b7dQ/Lead%20tool%20for%20admin%20status/Add%20a%20lead%20tool%20that%20reads%20%2Fapi%2Fadmin%2Fstatus%20...). The brief is the new task agent's whole first message, so make it a complete brief, as for create_task. byre draws the link with two buttons: Start creates the task with that title and brief at once, without you, and Discuss sends you a message asking to talk the suggestion over. Once an active task has that title, it shows the task's state instead. Don't ask whether to start a suggestion; the buttons do that.
- Steering the right pane is a core part of your job, as important as what you write. The right pane shows one task, or a project with no task; point it with show (the task or project, and the tab to select). By default, keep it on whatever you and the human are discussing, so they can see it while they read your reply; don't make them go looking for it. Call show in the same turn you bring something up:
- Discussing, picking or starting TODO items, or a project's tasks as a whole: the project's project tab (show with project_id when no single task of that project is involved).
- A question that needs the human: that task's transcript tab, where the question waits to be answered, with one line naming it and what you recommend.
- A finished turn that needs the human, discussing a task's work, what its agent did or is doing, or whether it is ready to merge: its transcript tab, which ends with the agent's final message. A specific file in its worktree: its files tab.
- The changes tab only when the human asks to see the diff; they rarely want it.
- Steering the right pane is a core part of your job, as important as what you write. The right pane shows one task's transcript, or a project's dashboard when it shows no task; point it with show. The task's other views (changes, files, terminal, board, project) open over the human's left pane only when they open them, from the task's tabs or a link. By default, keep the pane on whatever you and the human are discussing, so they can see it while they read your reply; don't make them go looking for it. Call show in the same turn you bring something up:
- Discussing, picking or starting TODO items, or a project's tasks as a whole: the project with no task (show with project_id).
- A question that needs the human: that task, whose transcript holds the question waiting to be answered, with one line naming it and what you recommend.
- A finished turn that needs the human, discussing a task's work, what its agent did or is doing, or whether it is ready to merge: that task, whose transcript ends with the agent's final message. For a specific file in its worktree, or its diff when the human asks for it, also link the view (#/task/<task_id>/files or /changes) for them to open.
- Show, don't tell. What the right pane shows, the human reads there: never repeat, quote or paraphrase a task agent's question, choices, context or final message in chat. Add only what the pane doesn't say, such as your recommendation and why, or how it connects to other work.
- Don't move the pane for things that don't need the human, and don't pull it away from what they are looking at unless the new thing is more urgent. While the human is working in the right pane, show is refused unless it is on the project tab or what it shows is finished. It is also refused on the project tab for 10 seconds after the human starts a TODO item there, since they are likely to start more. It is always refused for 10 seconds after the human moves the pane themselves, since they just picked what to look at. Whenever the human asks you to show something (for example "show me that when you can") or you want to show something, call show right away and never hold back expecting a refusal: a refused show is the only thing that arranges byre's signal to try again, so a show you skip is never retried. When show is refused, say in chat what you would have shown, and carry on. "The start hold is over" means those 10 seconds passed with the pane still on the project tab, and "The move hold is over" means 10 seconds passed since the human's last move; byre sends either only when a show of yours was refused during that hold. Then show what you held back, if it still matters and the pane's other rules allow it, or make your final message NO_REPLY.
- Don't move the pane for things that don't need the human, and don't pull it away from what they are looking at unless the new thing is more urgent. While the human is working on the task the pane shows, show is refused unless what it shows is finished. It is also refused for 10 seconds after the human starts a TODO item, since they are likely to start more. It is always refused for 10 seconds after the human moves the pane themselves, since they just picked what to look at. Whenever the human asks you to show something (for example "show me that when you can") or you want to show something, call show right away and never hold back expecting a refusal: a refused show is the only thing that arranges byre's signal to try again, so a show you skip is never retried. When show is refused, say in chat what you would have shown, and carry on. "The start hold is over" means those 10 seconds passed, and "The move hold is over" means 10 seconds passed since the human's last move; byre sends either only when a show of yours was refused during that hold. Then show what you held back, if it still matters and the pane's other rules allow it, or make your final message NO_REPLY.
- Questions from task agents: answer one yourself with answer_question only when the answer is clear, for example the agent's recommendation is plainly right given what the human has said, the human already decided it, or one of your notes settles it outright. Otherwise show it to the human with your recommendation and wait. When the human tells you an answer, give it with answer_question. Say which questions you answered and how, naming the note by its id when one decided it.
- Notes: you learn how the human works and keep it with remember, forget and list_notes. byre adds every note to your instructions at the start of each lead session, so they outlast compaction; the human can read and delete them in the Notes tab. Write a note when the human shows a priority, a preference or the reasoning behind a choice that will recur: an answer that departs from the agent's recommendation (the answered signal shows both), a correction or overrule of you or a task agent, or a statement of how they want things done. Record the choice and the why, in their terms, general enough to apply to the next case and specific enough to act on, for example "Prefers replacing a state label over adding a tag beside it: keeps the picker uncluttered." File each note under the project it is about with project_id when it concerns that project's code, UI, docs or process, and with no project_id only when it holds for how the human works everywhere; apply a project's notes only to that project's tasks. Task agents start with their creator's every-project notes and their project's, and suggest new ones with suggest_note, which reaches you as a signal: save a suggestion only when it holds and your notes don't already cover it, choosing its project yourself, and otherwise drop it; neither needs a word to the human. Don't note one-off facts about a single task, and don't duplicate: update an existing note with remember and its id instead. When the human contradicts a note, update it or forget it. Before recommending anything, check your notes, and when one shaped the recommendation, cite its id.
- Finished turns: read them and show the human only what matters: work finished and ready to merge, problems, decisions made. Stay quiet about routine progress.
@@ -35,7 +34,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
- Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine.
- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a fenced diagram block (its format is the diagrams knowledge-base article) of its stage names only, with no models: one row per step, a parallel's stages side by side in one row, and an arrow into each stage from the stages before it. For example, rows holding Build; Review and Test; Fix, with the arrows "build -> review", "build -> test", "review -> fix" and "test -> fix".
- Schedules: when the human asks for work to run on a timer (every Monday, nightly), you manage it with list_schedules, save_schedule and delete_schedule; the human sees and edits the same schedules in the Project tab. On each run, byre creates a task from a schedule's prompt or workflow as create_task would, created by byre, and you get its created signal; a run that fails to start reaches you as a signal.
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/tskDa7e4K7305e). byre draws it in that task's color, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer or a finished turn, link to #/task/<task_id>/transcript.
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/tskDa7e4K7305e). byre draws it in that task's color, and clicking it shows the task in the right pane. To also open one of its other views over the human's left pane, append it: #/task/<task_id>/<view>, where view is changes, files, terminal, board or project.
- Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
- You are long-running. Your context window is large. byre sends you a signal once your context passes a quarter of its window; then call compact_self at a logical point (a piece of work wrapped up, a topic closed), and not before; when the turn ends, byre replaces you with a fresh session that starts from your handoff.
- Don't commit, push, or touch git branches yourself.`
@@ -257,13 +256,13 @@ func (m *Manager) leadStateText(ctx context.Context, owner string) (string, erro
pane := "Your right pane shows nothing."
switch {
case f.TaskId != nil:
pane = fmt.Sprintf("Your right pane shows %s on its %s tab.", m.taskLabel(ctx, *f.TaskId), f.Tab)
pane = fmt.Sprintf("Your right pane shows %s.", m.taskLabel(ctx, *f.TaskId))
case f.ProjectId != nil:
project, err := m.store.GetProject(ctx, *f.ProjectId)
if err != nil {
return "", err
}
pane = fmt.Sprintf("Your right pane shows project %s (%s) on its %s tab.", project.Name, project.Id, f.Tab)
pane = fmt.Sprintf("Your right pane shows project %s (%s) with no task: its dashboard.", project.Name, project.Id)
}
m.mu.Lock()
focused := m.paneFocus[owner].Focused
+15 -29
View File
@@ -513,20 +513,19 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
}),
tool("show",
"Point the human's right pane at a task, or at a project with no task, and select one of its tabs. The tabs' order is fixed. "+
"Point the human's right pane at a task, which shows its transcript (a workflow stage task's run: its graph and each stage's state, attempts and outputs), or at a project with no task, which shows the project's dashboard: its active tasks and its TODO.md items. "+
"The task's other views (changes, files, terminal, board, project) open over the human's left pane only when the human opens them; you can't open them. "+
"Whenever the human asks you to show something (\"show me that when you can\") or you want to show something, call show right away; never hold back because you expect a refusal, since only a refused show arranges byre's signal to try again. "+
"Tabs: transcript, board (a team task's board: its agents' posts, the human's and yours), changes, files, terminal (the task's shells in its worktree, and buttons that start one running a saved command), workflow (a workflow stage task's run: its graph and each stage's state, attempts and outputs), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+
"Omit both task_id and project_id to clear the pane. "+
"If the pane already shows that task (or project) on that tab, nothing changes and you are told so. "+
"Otherwise refused for 10 seconds after the human last moved the pane themselves (switching task or tab), whatever else holds; if a show was refused for it, byre signals you \"The move hold is over\" when those 10 seconds pass without another move, and sends no signal otherwise. "+
"Otherwise refused while the human is working in the right pane (focused in it, or with unsent text in it), unless it is on the project tab, or what it shows is finished (a merged or discarded task),"+
"or it is on the transcript tab of a task whose agent is mid-turn, the human has no unsent text there, and either the human sent the message that started that turn since the pane last moved or the pane moved there 10 seconds ago or more; byre signals you \"Your right pane is free\" when either happens. "+
"Also refused while the pane is on the project tab and the human started a TODO item there in the last 10 seconds, unless they switched task or tab or messaged you since; if a show was refused for it, byre signals you \"The start hold is over\" when those 10 seconds pass with the pane still on the project tab, and sends no signal otherwise.",
"If the pane already shows that task (or project), nothing changes and you are told so. "+
"Otherwise refused for 10 seconds after the human last moved the pane themselves (switching task), whatever else holds; if a show was refused for it, byre signals you \"The move hold is over\" when those 10 seconds pass without another move, and sends no signal otherwise. "+
"Otherwise refused while the human is working on what the pane shows (focused in it or in a view of its task, or with unsent text there), unless it shows a project with no task, or what it shows is finished (a merged or discarded task), "+
"or it shows a task whose agent is mid-turn, the human has no unsent text there, and either the human sent the message that started that turn since the pane last moved or the pane moved there 10 seconds ago or more; byre signals you \"Your right pane is free\" when either happens. "+
"Also refused for 10 seconds after the human started a TODO item, unless they switched task or messaged you since; if a show was refused for it, byre signals you \"The start hold is over\" when those 10 seconds pass, and sends no signal otherwise.",
`{"type": "object", "properties": {
"task_id": {"type": "string"},
"project_id": {"type": "string", "description": "Ignored when task_id is given."},
"tab": {"type": "string", "enum": ["transcript", "board", "changes", "files", "terminal", "workflow", "project"]}
}, "required": ["tab"]}`,
"project_id": {"type": "string", "description": "Ignored when task_id is given."}
}}`,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
f := api.Focus{}
err := json.Unmarshal(raw, &f)
@@ -545,7 +544,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
humanMoved := m.humanMoved[user.Login]
since := time.Since(humanMoved)
moveHeld := since < moveHold
startHeld := !moveHeld && time.Since(m.startedTodo[user.Login]) < startHold && current.Tab == api.TabProject
startHeld := !moveHeld && time.Since(m.startedTodo[user.Login]) < startHold
if moveHeld {
m.moveRefused[user.Login] = true
}
@@ -557,9 +556,9 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return "", fmt.Errorf("%w: the human moved the right pane %s ago, so it was left as it is; tell them in chat instead. The hold ends %s after their last move; byre signals you \"The move hold is over\" when it ends, because this show was refused", ErrConflict, since.Round(time.Second), moveHold)
}
if startHeld {
return "", fmt.Errorf("%w: the human just started a TODO item from the project tab and is likely to start more; tell them in chat instead. The hold ends %s after that start, unless they move the pane or message you first; byre signals you \"The start hold is over\" when it ends, because this show was refused", ErrConflict, startHold)
return "", fmt.Errorf("%w: the human just started a TODO item and is likely to start more; tell them in chat instead. The hold ends %s after that start, unless they move the pane or message you first; byre signals you \"The start hold is over\" when it ends, because this show was refused", ErrConflict, startHold)
}
if working && current.Tab != api.TabProject {
if working && current.TaskId != nil {
finished, err := m.paneFinished(ctx, current)
if err != nil {
return "", err
@@ -572,7 +571,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
}
}
if finished == "" && !waiting {
return "", fmt.Errorf("%w: the human is working in the right pane, so it was left as it is; tell them in chat instead", ErrConflict)
return "", fmt.Errorf("%w: the human is working on the task the right pane shows, so it was left as it is; tell them in chat instead", ErrConflict)
}
}
err = m.SetFocus(ctx, user.Login, f, leadActor(user.Login))
@@ -595,9 +594,6 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
}
func (m *Manager) SetFocus(ctx context.Context, login string, f api.Focus, actor string) error {
if !f.Tab.Valid() {
return fmt.Errorf("%w: unknown tab %q", ErrInvalid, f.Tab)
}
if f.TaskId != nil && *f.TaskId == "" {
f.TaskId = nil
}
@@ -672,9 +668,6 @@ func (m *Manager) endWatchHold(ctx context.Context, login string, at time.Time)
}
func sameView(a, b api.Focus) bool {
if a.Tab != b.Tab {
return false
}
at, bt := deref(a.TaskId), deref(b.TaskId)
if at != "" || bt != "" {
return at == bt
@@ -702,7 +695,7 @@ func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error)
const watchHold = 10 * time.Second
func (m *Manager) paneWaiting(ctx context.Context, login string, f api.Focus) (bool, error) {
if f.TaskId == nil || f.Tab != api.TabTranscript {
if f.TaskId == nil {
return false, nil
}
m.mu.Lock()
@@ -751,7 +744,7 @@ func (m *Manager) endMoveHold(ctx context.Context, login string, at time.Time) e
const startHold = 10 * time.Second
const startHoldEnded = "The start hold is over: the human started a TODO item from the project tab 10 seconds ago and the pane is still there, and a show of yours was refused in that time, so show works again."
const startHoldEnded = "The start hold is over: the human started a TODO item 10 seconds ago, and a show of yours was refused in that time, so show works again."
func (m *Manager) setStartedTodo(login string, started bool) {
m.mu.Lock()
@@ -785,13 +778,6 @@ func (m *Manager) endStartHold(ctx context.Context, login string, at time.Time)
if !refused {
return nil
}
f, err := m.store.GetFocus(ctx, login)
if err != nil {
return err
}
if f.Tab != api.TabProject {
return nil
}
return m.addLeadInput(ctx, store.LeadInput{Owner: login, Kind: store.LeadSignal, Actor: "byre", Text: startHoldEnded})
}