stay and start at first attention item
This commit is contained in:
+1
-1
@@ -97,7 +97,7 @@ Connections belong to `Servers`, not to the window: every saved server's store a
|
|||||||
|
|
||||||
## Views
|
## Views
|
||||||
|
|
||||||
- **Transcript and Lead.** `TranscriptBuilder` is a pure function from events to a list of rows: user and agent messages, consecutive tool calls folded into one group with its summary ("Read file ×3"), subagents nested with their status, pending tool calls and running subagents (background ones included after the turn ends) outlined in the accent colour with a 0.6s fade, thinking, the byre prompt collapsed, held turns, `NO_REPLY` turns folded away, handoffs, snapshot rows with changes, fork and review (and stop while the review runs), question bubbles that follow their item's `inbox.updated` events, a finished review as its summary row followed by one row per finding, the agent's `respond_to_finding` calls as their own rows, and notes for the other event kinds. Rows that hold a review (`Row.review`) are kept per review id and updated by each later `review.*` event. It is incremental: a new batch extends the existing rows. At the transcript's top right, the attention buttons (`AttentionBar`, from `ChatModel.attention`) count what needs the user: open questions, findings of the latest review that neither the user nor the agent has answered, and the current ready-for-merge overview; each click scrolls to the next one, wrapping. Jumps from elsewhere (`Store.jumpTo` with a `JumpTarget`: a summary, a question or a snapshot's review) select the task's transcript and scroll there once the row exists. The view is a `ScrollView` with a plain `VStack`, stuck to the bottom unless the user has scrolled more than 80 points up. The stack isn't lazy because a `LazyVStack` estimates the height of rows it hasn't shown yet and corrects it when they scroll into view, without moving the scroll offset, so scrolling up through rows not yet shown jumped by each correction; SwiftUI can't move the offset mid-scroll without stopping its momentum. While the user is scrolling (`onScrollPhaseChange`), content height changes only update whether it is stuck and never pull it back to the bottom. The lead pages back when the view comes within one screen of the top: the fetched page is held while the scroll phase is scrolling and inserted once it is idle (`ChatModel.older`, `prependOlder`), and the next geometry change moves the offset down by the height added, so the rows on screen stay put; a page that adds no rows (all folded `NO_REPLY` turns) fetches the next one at once. Each Markdown text run's `NSTextView` measures its height with one TextKit stack kept in its coordinator, and a repeat measurement at the same width and text returns the stored size. Pending sends show at the end until the server echoes them. A turn summary counts as read when its row is on screen and its server's window is the key window of the active app (`PaneFocus.windowKey`, `markSummaryRead`), checked again whenever the app activates or a window becomes key. The head carries Compact (with its context-usage fill; a long press opens a menu of models to continue on), Sync from main, Merge and Discard.
|
- **Transcript and Lead.** `TranscriptBuilder` is a pure function from events to a list of rows: user and agent messages, consecutive tool calls folded into one group with its summary ("Read file ×3"), subagents nested with their status, pending tool calls and running subagents (background ones included after the turn ends) outlined in the accent colour with a 0.6s fade, thinking, the byre prompt collapsed, held turns, `NO_REPLY` turns folded away, handoffs, snapshot rows with changes, fork and review (and stop while the review runs), question bubbles that follow their item's `inbox.updated` events, a finished review as its summary row followed by one row per finding, the agent's `respond_to_finding` calls as their own rows, and notes for the other event kinds. Rows that hold a review (`Row.review`) are kept per review id and updated by each later `review.*` event. It is incremental: a new batch extends the existing rows. At the transcript's top right, the attention buttons (`AttentionBar`, from `ChatModel.attention`) count what needs the user: open questions, findings of the latest review that neither the user nor the agent has answered, and the current ready-for-merge overview; each click scrolls to the next one, wrapping. Jumps from elsewhere (`Store.jumpTo` with a `JumpTarget`: a summary, a question or a snapshot's review) select the task's transcript and scroll there once the row exists. The view is a `ScrollView` with a plain `VStack`, stuck to the bottom unless the user has scrolled more than 80 points up; while an attention row is held (`ChatModel.held`), being stuck instead keeps that row top-anchored, which the scroll view clamps to the bottom until the row reaches the top. The stack isn't lazy because a `LazyVStack` estimates the height of rows it hasn't shown yet and corrects it when they scroll into view, without moving the scroll offset, so scrolling up through rows not yet shown jumped by each correction; SwiftUI can't move the offset mid-scroll without stopping its momentum. While the user is scrolling (`onScrollPhaseChange`), content height changes only update whether it is stuck and never pull it back to the bottom. The lead pages back when the view comes within one screen of the top: the fetched page is held while the scroll phase is scrolling and inserted once it is idle (`ChatModel.older`, `prependOlder`), and the next geometry change moves the offset down by the height added, so the rows on screen stay put; a page that adds no rows (all folded `NO_REPLY` turns) fetches the next one at once. Each Markdown text run's `NSTextView` measures its height with one TextKit stack kept in its coordinator, and a repeat measurement at the same width and text returns the stored size. Pending sends show at the end until the server echoes them. A turn summary counts as read when its row is on screen and its server's window is the key window of the active app (`PaneFocus.windowKey`, `markSummaryRead`), checked again whenever the app activates or a window becomes key. The head carries Compact (with its context-usage fill; a long press opens a menu of models to continue on), Sync from main, Merge and Discard.
|
||||||
- **Composer.** A `TextEditor`, with paste and drop handled as in "Gaps in SwiftUI". Text, the model menu (filtered to the live session's vendor), Interrupt and Send. Enter sends, Shift-Enter adds a newline, and Enter during IME composition does neither. Pasted or dropped PNG, JPEG, GIF and WebP images become thumbnails with ×, uploaded to `POST /api/blobs` on send; any other image `NSBitmapImageRep` decodes, such as TIFF, is converted to PNG first, its size checked after conversion. A failed send puts the text and images back and shows the error.
|
- **Composer.** A `TextEditor`, with paste and drop handled as in "Gaps in SwiftUI". Text, the model menu (filtered to the live session's vendor), Interrupt and Send. Enter sends, Shift-Enter adds a newline, and Enter during IME composition does neither. Pasted or dropped PNG, JPEG, GIF and WebP images become thumbnails with ×, uploaded to `POST /api/blobs` on send; any other image `NSBitmapImageRep` decodes, such as TIFF, is converted to PNG first, its size checked after conversion. A failed send puts the text and images back and shows the error.
|
||||||
- **Questions.** An open question's bubble in the transcript holds its card, rendered from its Markdown context with each choice's marker replaced by a clickable block (pick-one, sends at once) or checkbox (pick-any, with Submit), the "recommended" tag, image-choice grids, attachments, Let the agent decide and Other…. Arrow keys move between choices. A 409 shows its error. Once answered or dismissed, the bubble shrinks to the question and who answered what.
|
- **Questions.** An open question's bubble in the transcript holds its card, rendered from its Markdown context with each choice's marker replaced by a clickable block (pick-one, sends at once) or checkbox (pick-any, with Submit), the "recommended" tag, image-choice grids, attachments, Let the agent decide and Other…. Arrow keys move between choices. A 409 shows its error. Once answered or dismissed, the bubble shrinks to the question and who answered what.
|
||||||
- **Changes, Files, Workflow, Project, Status, Settings.** Including the pinned-snapshot stepper, open-diff memory, per-task place in Files, nested subtasks, TODO Start and Discuss, rate-limit pace bars and lead notes. The Workflow tab draws its stage graph natively from the `WorkflowRun` (stages as boxes coloured by state, `after` as edges, laid out top to bottom by depth) with the layout and drawing code from "Diagrams", fed from the run's data rather than Mermaid text.
|
- **Changes, Files, Workflow, Project, Status, Settings.** Including the pinned-snapshot stepper, open-diff memory, per-task place in Files, nested subtasks, TODO Start and Discuss, rate-limit pace bars and lead notes. The Workflow tab draws its stage graph natively from the `WorkflowRun` (stages as boxes coloured by state, `after` as edges, laid out top to bottom by depth) with the layout and drawing code from "Diagrams", fed from the run's data rather than Mermaid text.
|
||||||
|
|||||||
+1
-1
@@ -297,7 +297,7 @@ Agents ask questions with the `ask_human` MCP tool. Each question carries a `con
|
|||||||
|
|
||||||
`GET /api/inbox` is one query over open items, oldest first; `project_id` and `task_id` narrow it to a project or task view, and `state` picks another state. The right pane shows a task's questions in its transcript, where they were asked, and they are answered there (below).
|
`GET /api/inbox` is one query over open items, oldest first; `project_id` and `task_id` narrow it to a project or task view, and `state` picks another state. The right pane shows a task's questions in its transcript, where they were asked, and they are answered there (below).
|
||||||
|
|
||||||
**Attention.** At the top right of a task's transcript, over its content, a column of buttons (`AttentionBar` in `mac/Sources/Byre/Chat.swift`, counted by `ChatModel.attention`) shows what in the transcript needs the user, one button per kind that has any, in the warn colours: "N questions" for its open questions, "N findings" for the findings of the task's latest review (the highest-numbered snapshot with a `review.*` event, once it is `done`) that have neither a state from the user nor a response from the agent, and "ready to merge" while the latest ready-for-merge overview is current (see "Ready for merge"). Each click scrolls to the next one of that kind, top-anchored, and after the last back to the first. A button goes away once nothing of its kind is left. Elsewhere, the switcher and the Merge confirmation's question warning jump to the task's oldest open question (`Store.showQuestion`), and the review warning to that snapshot's review (`Store.jumpTo` with `.review`): each selects the task's Transcript tab and scrolls there once the row is in the transcript.
|
**Attention.** At the top right of a task's transcript, over its content, a column of buttons (`AttentionBar` in `mac/Sources/Byre/Chat.swift`, counted by `ChatModel.attention`) shows what in the transcript needs the user, one button per kind that has any, in the warn colours: "N questions" for its open questions, "N findings" for the findings of the task's latest review (the highest-numbered snapshot with a `review.*` event, once it is `done`) that have neither a state from the user nor a response from the agent, and "ready to merge" while the latest ready-for-merge overview is current (see "Ready for merge"). Each click scrolls to the next one of that kind, top-anchored, and after the last back to the first. Where the transcript would follow the bottom, it follows only until the first of these rows in transcript order (`ChatModel.held`) reaches the top, and stays there as rows arrive; so a transcript opens at that row, and one that a new question arrives in stops with the question at the top. Scrolling by hand or sending a message lets that row go (`ChatModel.release`), and the transcript follows the bottom again until a different row becomes the first. A button goes away once nothing of its kind is left. Elsewhere, the switcher and the Merge confirmation's question warning jump to the task's oldest open question (`Store.showQuestion`), and the review warning to that snapshot's review (`Store.jumpTo` with `.review`): each selects the task's Transcript tab and scrolls there once the row is in the transcript.
|
||||||
|
|
||||||
`POST /api/inbox/responses` answers any number of questions in one submission, each becoming `answered`. Response types:
|
`POST /api/inbox/responses` answers any number of questions in one submission, each becoming `answered`. Response types:
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ final class ChatModel {
|
|||||||
private(set) var hasUnmerged = false
|
private(set) var hasUnmerged = false
|
||||||
@ObservationIgnored var stuck = true
|
@ObservationIgnored var stuck = true
|
||||||
@ObservationIgnored var prepended = false
|
@ObservationIgnored var prepended = false
|
||||||
|
@ObservationIgnored var released: Int?
|
||||||
@ObservationIgnored private var unmergedTimer: Task<Void, Never>?
|
@ObservationIgnored private var unmergedTimer: Task<Void, Never>?
|
||||||
@ObservationIgnored private var offs: [() -> Void] = []
|
@ObservationIgnored private var offs: [() -> Void] = []
|
||||||
@ObservationIgnored private var focusStart = true
|
@ObservationIgnored private var focusStart = true
|
||||||
@@ -335,6 +336,18 @@ final class ChatModel {
|
|||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var held: Int? {
|
||||||
|
let ids = Set(attention().flatMap(\.rows))
|
||||||
|
guard let first = builder.root.rows.first(where: { ids.contains($0.id) })?.id, first != released else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
|
func release() {
|
||||||
|
released = held ?? released
|
||||||
|
}
|
||||||
|
|
||||||
func cycle(_ g: AttentionGroup) {
|
func cycle(_ g: AttentionGroup) {
|
||||||
let next = (cursors[g.id] ?? -1) + 1
|
let next = (cursors[g.id] ?? -1) + 1
|
||||||
let i = next < g.rows.count ? next : 0
|
let i = next < g.rows.count ? next : 0
|
||||||
@@ -770,16 +783,34 @@ private struct TranscriptScroll: View {
|
|||||||
model.stuck = new.distance < 80
|
model.stuck = new.distance < 80
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if model.stuck && new.distance > 1 && (new.content != old.content || new.visible != old.visible) {
|
if !model.stuck || (new.content == old.content && new.visible == old.visible) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let id = model.held {
|
||||||
|
position.scrollTo(id: id, anchor: .top)
|
||||||
|
} else if new.distance > 1 {
|
||||||
position.scrollTo(edge: .bottom)
|
position.scrollTo(edge: .bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onScrollPhaseChange { _, phase in
|
.onScrollPhaseChange { _, phase in
|
||||||
scrolling = phase.isScrolling
|
scrolling = phase.isScrolling
|
||||||
|
if phase == .interacting {
|
||||||
|
model.release()
|
||||||
|
}
|
||||||
if !scrolling {
|
if !scrolling {
|
||||||
model.prependOlder()
|
model.prependOlder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: model.held) { _, id in
|
||||||
|
if !model.stuck || scrolling {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let id {
|
||||||
|
position.scrollTo(id: id, anchor: .top)
|
||||||
|
} else {
|
||||||
|
position.scrollTo(edge: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
.onChange(of: model.older == nil) {
|
.onChange(of: model.older == nil) {
|
||||||
if !scrolling {
|
if !scrolling {
|
||||||
model.prependOlder()
|
model.prependOlder()
|
||||||
@@ -789,6 +820,7 @@ private struct TranscriptScroll: View {
|
|||||||
guard let new, new > old ?? 0 else {
|
guard let new, new > old ?? 0 else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
model.release()
|
||||||
model.stuck = true
|
model.stuck = true
|
||||||
position.scrollTo(edge: .bottom)
|
position.scrollTo(edge: .bottom)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user