Move-hold signal only after refused show

This commit is contained in:
Ian Gulliver
2026-10-17 09:24:13 -07:00
parent 248c0d5904
commit d1dda3f8ab
4 changed files with 38 additions and 13 deletions
+31 -10
View File
@@ -407,13 +407,14 @@ 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: tabs are the tabs to put first, in order (the rest follow), and tab is the one to select. "+
"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, changes, files, terminal (a shell in the task's worktree), inbox (the task's open items), reviews (the task's reviews by reviewer agents), 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, only the tab order changes and you are told so, even while the human is working in the pane. "+
"Otherwise refused for 10 seconds after the human last moved the pane themselves (switching task or tab), whatever else holds; byre signals you \"The move hold is over\" when those 10 seconds pass without another move. "+
"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, unless it is on the project tab, or what it shows is finished (a merged or discarded task, or an inbox tab with nothing left), "+
"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; byre signals you \"The start hold is over\" when those 10 seconds pass with the pane still on the project tab.",
"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.",
`{"type": "object", "properties": {
"task_id": {"type": "string"},
"project_id": {"type": "string", "description": "Ignored when task_id is given."},
@@ -443,13 +444,21 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
m.mu.Lock()
focused := m.paneFocus[user.Login].Focused
humanMoved := m.humanMoved[user.Login]
startedTodo := m.startedTodo[user.Login]
m.mu.Unlock()
if since := time.Since(humanMoved); since < moveHold {
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", ErrConflict, since.Round(time.Second), moveHold)
since := time.Since(humanMoved)
moveHeld := since < moveHold
startHeld := !moveHeld && time.Since(m.startedTodo[user.Login]) < startHold && current.Tab == api.TabProject
if moveHeld {
m.moveRefused[user.Login] = true
}
if time.Since(startedTodo) < startHold && current.Tab == api.TabProject {
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", ErrConflict, startHold)
if startHeld {
m.startRefused[user.Login] = true
}
m.mu.Unlock()
if moveHeld {
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)
}
if focused && current.Tab != api.TabProject {
finished, err := m.paneFinished(ctx, current)
@@ -518,6 +527,7 @@ func (m *Manager) SetFocus(ctx context.Context, login string, f api.Focus, actor
m.paneMoved[login] = at
if actor == login {
m.humanMoved[login] = at
delete(m.moveRefused, login)
}
m.mu.Unlock()
if actor == login {
@@ -640,7 +650,7 @@ func (m *Manager) SetPaneFocus(login string, p api.PaneFocus) {
const moveHold = 10 * time.Second
const moveHoldEnded = "The move hold is over: the human moved the right pane 10 seconds ago and hasn't moved it since, so show works again under its other rules."
const moveHoldEnded = "The move hold is over: the human moved the right pane 10 seconds ago and hasn't moved it since, and a show of yours was refused in that time, so show works again under its other rules."
func (m *Manager) endMoveHold(ctx context.Context, login string, at time.Time) error {
m.mu.Lock()
@@ -650,7 +660,12 @@ func (m *Manager) endMoveHold(ctx context.Context, login string, at time.Time) e
return nil
}
delete(m.humanMoved, login)
refused := m.moveRefused[login]
delete(m.moveRefused, login)
m.mu.Unlock()
if !refused {
return nil
}
err := m.store.AddLeadInput(ctx, store.LeadInput{Owner: login, Kind: store.LeadSignal, Actor: "byre", Text: moveHoldEnded})
if err != nil {
return err
@@ -661,11 +676,12 @@ 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, so show works again."
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."
func (m *Manager) setStartedTodo(login string, started bool) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.startRefused, login)
if !started {
delete(m.startedTodo, login)
return
@@ -688,7 +704,12 @@ func (m *Manager) endStartHold(ctx context.Context, login string, at time.Time)
return nil
}
delete(m.startedTodo, login)
refused := m.startRefused[login]
delete(m.startRefused, login)
m.mu.Unlock()
if !refused {
return nil
}
f, err := m.store.GetFocus(ctx, login)
if err != nil {
return err