Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/changes/unreleased/1426-home-task-working.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
kind: fixed
title: Home conversations keep their working mark while tasks run
pr: 1426
surface: [chat, docs]
invalidates:
- Home read an owned conversation's answering flag alone, so tasks could keep running after its working mark disappeared; Home now shares the tab's live working state, including tasks and background commands, until the last work settles. A task waiting for a decision no longer hides its conversation's other running work on Home, and a conversation whose first act is a `/task`, before it has a name, is marked too.
- A stale Home task count could keep an owned conversation marked working after its tasks finished; the conversation's live status now takes precedence over the saved count.
---
25 changes: 16 additions & 9 deletions internal/manual/chat/home.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Home has no `needs you` heading or empty attention panel. An amber `?` beside a
conversation or task means it has an unanswered question. The question stays on
that item's existing row instead of creating a duplicate. A task decision names
the task when its question identifies it; other questions stay on the conversation.
The question mark takes priority over the conversation's answering or unread mark.
The question mark takes priority over the conversation's working or unread mark.

The selected row's description shows the question and its available answers. The
same digit keys answer it. Once the question is resolved and Home refreshes, the
Expand Down Expand Up @@ -297,9 +297,10 @@ The choice survives closing the app. Folder actions use the task's conversation

## Why does only one row spin — the one spinner on home

**The first answering conversation wears the one turning cell.** However much is happening, exactly one cell on the frame turns: the other
running rows wear no mark at all, and the page stays a still page redrawn every three
seconds. An `ask here` errand's row takes the spinner while its answer is coming, and so does
**Only one working conversation animates.** The other running conversations keep a
still working mark. Home's latest activity reading chooses the spinner; if it has none,
the first conversation with live work takes it, passing over a row that shows `?`. The page refreshes its saved facts every
three seconds. An `ask here` errand's row takes the spinner while its answer is coming, and so does
a row on its way here from another window.

Two reasons, and they are the same reason:
Expand Down Expand Up @@ -350,7 +351,7 @@ nothing.

Home has **one bulleted conversation list under sessions**. It combines this window's
open tabs and saved history, deduplicates them, and shows the fifteen most recent
conversations, newest first. Bullets show answering and unread replies. The current
conversations, newest first. Bullets show running work and unread replies. The current
conversation is bold; its description can say `here` and show the last thing you wrote.
Closed conversations keep their place in this same chronological list, dimmed.

Expand Down Expand Up @@ -1521,7 +1522,7 @@ show up without you doing anything. There is no file watcher: home reads, and cl
screen stops the reading. (Standing items — reminders, watches, rules — are a different
mechanism and do keep going; see the keeping-an-eye page.)

The resting screen does not animate, except the one spinner on the first answering conversation row
The resting screen does not animate, except the one spinner on the first working conversation row
(*Why does only one row spin*); the ages simply change on the next reading. In screen-reader
(linear) mode nothing ever animates.

Expand Down Expand Up @@ -2211,11 +2212,17 @@ which column each stands in is read off what it holds — a panel with rows is i
the left, a quiet one in the rail at the right — so nothing about them is yours to set; only
their heights and their sides move with the terminal and the day.

## Conversation bullets — answering and unread replies
## Conversation bullets — running tasks, answering and unread replies

Every conversation in Home has a bullet. A dim bullet means there is no unread
reply known to this window. A working mark means the conversation is answering;
the first answering row animates when no other Home row owns the spinner. An amber `?` means there is an unanswered question and takes priority over those marks.
reply known to this window. A working mark means the conversation is answering or has
queued or running tasks or background commands, even after its answer ends. This applies
to the conversation in front, including one with no name yet, and those held in other tabs. The mark clears when the last
work finishes; one completed task does not hide another that is still running, and a
task waiting for your decision carries its `?` on the task's own row without hiding the
conversation's other work.
The first working row animates when no other Home row owns the spinner. An amber `?`
means there is an unanswered question and takes priority over those marks.
A bright filled bullet means a reply finished while you were away. Opening the conversation
clears that unread state. Recently closed rows keep dim bullets. These indicators
use this window’s live conversations; they do not infer unread history from other
Expand Down
2 changes: 1 addition & 1 deletion internal/manual/chat/places.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ queued, waiting or unanswered work. Once that work settles, its whole tree moves
completed; new work moves it back. Each task retains its own state word.

Both sections default to newest activity first; click the age heading to reverse. Conversation titles match Home, and the same
bullets mark answering, unread and unanswered states. Every conversation and nested task
bullets mark running work, unread replies and unanswered questions. Every conversation and nested task
starts expanded, with connecting tree lines and fold arrows immediately after titles in the left column. Projects
have their own column. You can fold a branch yourself. `/history`, `ctrl+.` and `alt+2` open it.

Expand Down
2 changes: 1 addition & 1 deletion internal/manual/chat/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2591,7 +2591,7 @@ conversations, retaining older parents and children instead of splitting their t
Filtering never moves a conversation into a different section.

Conversation names use the full title shown on Home and truncate only to fit. Their
bullets also match Home: dim at rest, working while answering, bright for unread replies,
bullets also match Home: dim at rest, working while answering or running tasks, bright for unread replies,
and a question mark when an answer is needed. A missing transcript still leaves its tasks
under an identified conversation row; nothing is promoted into a top-level task.

Expand Down
2 changes: 1 addition & 1 deletion internal/tui3/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func (a *app) homeBeat(gen int) tea.Cmd {
// twenty things out wakes it exactly as often — and costs the wire exactly as
// much — as a machine with one.
func (a *app) homeAnimating() bool {
return a.at(pageHome) && !a.linear && (a.homeSpins(a.home.spin) || a.homeAnsweringLine() >= 0)
return a.at(pageHome) && !a.linear && (a.homeSpins(a.home.spin) || a.homeWorkingLine() >= 0)
}

// homeGutter is the empty space between the two columns, and it is the ONLY
Expand Down
54 changes: 38 additions & 16 deletions internal/tui3/homebullets.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,52 @@ package tui3

import "github.com/Agent-Field/codeaf/internal/tui2/tokens"

// homeChatState reads only the foreground state and the keeper's cached events.
// A task running in the conversation does not mean its answer is still streaming.
func (a *app) homeChatState(cell *homeCell) (answering, unread bool) {
if cell == nil || cell.closed || cell.chatKey == "" {
// homeChatState shares the tab's live reading of work so tasks that outlast an
// answer keep their conversation working. Owned conversations use cached events;
// another window's tasks use Home's latest presence reading.
//
// IT READS THE WORK, NOT THE TAB'S RANKED SIGNAL. On a tab a question outranks
// work; on Home the question has its own mark, [cellMarkNeeds], which already
// outranks working in [conversationBullet], and a landed task's question sits on
// the task's row. A conversation waiting on one task's decision while another
// runs is still working.
//
// THE CONVERSATION IN FRONT IS KNOWN BY ITS ROW AS WELL AS BY ITS KEY. One that
// has never been named has no tab ([app.tabList]), so Home draws it from the
// saved rows, which carry `here` and no key — and a `/task` can be the first
// thing it does.
func (a *app) homeChatState(cell *homeCell) (working, unread bool) {
if cell == nil || cell.closed {
return false, false
}
key := cell.chatKey
if key == a.frontTabKey() {
return a.state == stateWorking, a.unreadChats[key]
if (cell.row != nil && cell.row.here) || (key != "" && key == a.frontTabKey()) {
front := a.frontTabKey()
return a.frontWorking(), a.unreadChats[front]
}
if key == "" {
// Another window's conversation, or one this window holds without a name:
// the saved count is the only reading there is, and nothing here is unread.
return cell.row != nil && cell.row.session.Tasks.Running > 0, false
}
if held := a.behind[key]; held != nil && held.watch != nil {
return held.watch.turning.Load(), a.unreadChats[key] || held.watch.landedSince() > 0
return held.watch.working(), a.unreadChats[key] || held.watch.landedSince() > 0
}
if cell.row != nil {
working = cell.row.session.Tasks.Running > 0
}
return false, a.unreadChats[key]
return working, a.unreadChats[key]
}

// The first answering conversation takes the spinner only if another panel has none.
func (a *app) homeAnsweringLine() int {
// The first working conversation takes the spinner only if another panel has none.
//
// A ROW WEARING A QUESTION IS PASSED OVER, because its `?` outranks the working
// mark and would hide the spinner: Home's frame clock would then run for a
// frame that never changes.
func (a *app) homeWorkingLine() int {
for i, line := range a.home.lines {
if line.kind == homeSession && line.cell != nil && (line.cell.panel == panelRecent || line.cell.panel == panelSessions) {
if line.kind == homeSession && line.cell != nil && line.cell.mark != cellMarkNeeds &&
(line.cell.panel == panelRecent || line.cell.panel == panelSessions) {
if working, _ := a.homeChatState(line.cell); working {
return i
}
Expand All @@ -32,12 +58,8 @@ func (a *app) homeAnsweringLine() int {

func (a *app) homeConversationBullet(cell *homeCell, pal palette) string {
working, unread := a.homeChatState(cell)
if cell != nil && !cell.closed && cell.row != nil {
_, moving := homeMovingAt(homeLine{kind: homeSession, row: cell.row.session})
working = working || moving
}
mark := pal.glyph(tokens.GWorking)
first := a.homeAnsweringLine()
first := a.homeWorkingLine()
if working && !a.linear && ((a.home.spin >= 0 && a.home.lines[a.home.spin].cell == cell) ||
(a.home.spin < 0 && first >= 0 && a.home.lines[first].cell == cell)) {
mark = a.homeSpinGlyph()
Expand Down
Loading
Loading