merge: integrate feat/backlog-burndown into master
master had advanced 16 commits past feat/backlog-burndown's base, and the two branches independently built four of the same features. Resolved 26 conflicts. Overlap features — kept master's implementation (more complete / production-wired / more robust), dropped the feature branch's parallel constellation: - llama-server health probe: kept master's event-store-backed tps probe; dropped the branch's LlamaLivenessClient (liveness-only, throughput unwired). - event-store probe: kept master's EventStoreHealthProbe; dropped EventStoreLatencyProbe. - brief echo-back gate: kept master's BriefEchoDiff (Jaccard, tolerates rewording); dropped the branch's exact-set-diff BriefEchoComparator/Extractor. - static-first reviewer: kept master's command/exit-code gate (ProcessStaticAnalysisRunner, wired); dropped the branch's structured-finding static_check stage (no-op seam). Its structured-findings model is filed as a follow-up in BACKLOG. Feature-branch net-new work brought in and kept (master had none): - native task tracking (aggregate, agent tools wired into analyst/implementer/reviewer, dependency graph + gates, decompose, REST/CLI, TUI task board) - critique-outcome producer (role-rel §6 — master had deferred it) - stage-level plan checkpointing (C-A2, folded into runPostStageGates) - CLAUDE.md/AGENTS.md L0 standing context - cross-session grants + TUI (grant scopes/revoke, @ picker, session resume browser) Verified: full Gradle compile (all modules + tests) green; tests pass for core:events, core:kernel, infrastructure:workflow, apps:server, apps:cli, testing:integration; tui-go go build + go test green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/correx/tui-go/internal/protocol"
|
||||
"github.com/correx/tui-go/internal/ws"
|
||||
)
|
||||
@@ -54,12 +57,20 @@ const (
|
||||
OverlayStats
|
||||
OverlayIdeas
|
||||
OverlayHealth
|
||||
OverlaySessions
|
||||
OverlayTasks
|
||||
OverlayFiles
|
||||
OverlayStatusbar
|
||||
OverlayGrants
|
||||
OverlayGrantScope
|
||||
OverlayHelp
|
||||
)
|
||||
|
||||
// RouterEntry is one line in a session's conversation transcript.
|
||||
type RouterEntry struct {
|
||||
Role string // user | router | tool | narration | narration_llm
|
||||
Role string // user | router | tool | narration | narration_llm | action
|
||||
Content string
|
||||
Icon string // action role only: the gutter glyph (✓ ✎ ✗ ⌘ ✕ ⊞ ⊟)
|
||||
Metrics *TurnMetrics
|
||||
}
|
||||
|
||||
@@ -111,6 +122,27 @@ type Approval struct {
|
||||
Rationale []string
|
||||
}
|
||||
|
||||
// tierNum parses the numeric part of a tier label ("T3" → 3). Unparseable or
|
||||
// empty tiers return -1, which is treated as low-risk (never requires confirm).
|
||||
func tierNum(tier string) int {
|
||||
t := strings.TrimSpace(strings.ToUpper(tier))
|
||||
t = strings.TrimPrefix(t, "T")
|
||||
if t == "" {
|
||||
return -1
|
||||
}
|
||||
n, err := strconv.Atoi(t)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// HighTier reports whether this gate is destructive/high-risk (T3+), so an approve
|
||||
// must be confirmed with a second keypress rather than acting on one keystroke.
|
||||
func (a *Approval) HighTier() bool {
|
||||
return tierNum(a.Tier) >= 3
|
||||
}
|
||||
|
||||
// ClarQuestion is one open question a stage raised (mirrors the wire DTO).
|
||||
type ClarQuestion struct {
|
||||
ID string
|
||||
@@ -159,10 +191,15 @@ type Session struct {
|
||||
Tools []ToolRecord
|
||||
ToolsByStage map[string][]ManifestTool
|
||||
Events []EventEntry
|
||||
Pending *Approval
|
||||
Clar *Clarification // open questions awaiting answers (clarification view)
|
||||
Propose *Proposal // router workflow suggestion awaiting a pick (propose view)
|
||||
Active bool // an inference/tool is in flight (drives the spinner)
|
||||
// Pending is the *current* approval gate shown in the band. It always mirrors
|
||||
// PendingQueue[PendingIdx] (kept in sync by syncPending) so the render code can
|
||||
// keep reading a single pointer while multiple gates queue up behind it.
|
||||
Pending *Approval
|
||||
PendingQueue []*Approval // all pending gates, oldest first; navigated with ↑/↓
|
||||
PendingIdx int // selected index into PendingQueue
|
||||
Clar *Clarification // open questions awaiting answers (clarification view)
|
||||
Propose *Proposal // router workflow suggestion awaiting a pick (propose view)
|
||||
Active bool // an inference/tool is in flight (drives the spinner)
|
||||
}
|
||||
|
||||
// Workflow is a launchable workflow advertised by the server.
|
||||
@@ -192,16 +229,21 @@ type Model struct {
|
||||
wfVisible bool
|
||||
wfPendingID string // workflow chosen, awaiting an intent line before StartSession
|
||||
wfPendingName string
|
||||
bgUpdates int
|
||||
// launcher (idle screen): the active "what to launch" selection — 0 = chat, 1..N =
|
||||
// workflows[i-1] — cycled with Tab and shown at the input's lower-right. railHidden
|
||||
// folds away the idle status/quick-keys rail.
|
||||
launcherWf int
|
||||
railHidden bool
|
||||
bgUpdates int
|
||||
|
||||
// input
|
||||
editMode EditMode
|
||||
inputMode InputMode
|
||||
inputBuffer string
|
||||
inputCursor int
|
||||
history map[string][]string
|
||||
historyIndex int
|
||||
savedBuffer string
|
||||
inputHistory []string // submitted lines across all contexts (chat, intent, in-session), newest last
|
||||
historyIndex int // -1 = editing the live buffer; else an index into inputHistory
|
||||
savedBuffer string // live buffer stashed while walking history
|
||||
|
||||
// flow flags
|
||||
sessionEntered bool
|
||||
@@ -212,6 +254,11 @@ type Model struct {
|
||||
routerMessages map[string][]RouterEntry
|
||||
routerConnected bool
|
||||
chatMode string
|
||||
// transcriptSel is the index (into the selected session's transcript) of the message
|
||||
// the operator has navigated to with ctrl+↑/↓; -1 = no selection (tail-follow). A
|
||||
// selected message is highlighted, scrolled into view, and is what `y` copies.
|
||||
transcriptSel int
|
||||
copiedFlash int // frame at which a copy happened, for a brief "copied" footer note
|
||||
|
||||
// provider
|
||||
currentModel string
|
||||
@@ -231,7 +278,11 @@ type Model struct {
|
||||
overlay OverlayKind
|
||||
overlayEventIdx int
|
||||
diffScrollOffset int
|
||||
modalScroll int // body scroll for tall fixed-content modals (help, stats)
|
||||
eventStripShown bool
|
||||
// in-session right panel: 0 = events, 1 = changes (token usage + changed files),
|
||||
// 2 = off (output full-width). Cycled with `d`.
|
||||
rightPanel int
|
||||
|
||||
// artifact viewer (OverlayArtifacts) — populated by the artifact.list response
|
||||
artifacts []protocol.ArtifactDto
|
||||
@@ -264,20 +315,85 @@ type Model struct {
|
||||
health *protocol.HealthDto
|
||||
healthLoading bool
|
||||
|
||||
// session browser (OverlaySessions) — recent sessions fetched over HTTP from
|
||||
// GET /sessions, so prior runs are reachable after a server restart + TUI reopen
|
||||
// (the WS stream only carries live sessions, not the historical roster).
|
||||
sessionList []SessionSummary
|
||||
sessionListIndex int
|
||||
sessionListLoading bool
|
||||
sessionListErr string
|
||||
|
||||
// task board (OverlayTasks) — all tasks across projects, fetched over HTTP from
|
||||
// GET /tasks. taskDetail toggles the per-task detail pane (built from the list payload).
|
||||
taskList []TaskSummary
|
||||
taskListIndex int
|
||||
taskListLoading bool
|
||||
taskListErr string
|
||||
taskDetail bool
|
||||
taskDetailScroll int
|
||||
taskFilter string // `/` substring narrow over id/title/goal
|
||||
taskFilterTyping bool
|
||||
|
||||
// command palette
|
||||
paletteFilter string
|
||||
paletteIndex int
|
||||
|
||||
// @ file picker (OverlayFiles) — workspace paths from the file.list reply
|
||||
files []string // all workspace-relative paths for filesFor
|
||||
filesFor string // sessionId the file list belongs to
|
||||
filesLoading bool
|
||||
fileFilter string // the query typed after @ (narrows the list)
|
||||
fileIndex int
|
||||
|
||||
// status-bar segment visibility (OverlayStatusbar) — persisted TUI-local in tui-prefs.json.
|
||||
// A segment id present in sbHidden is hidden; absent = shown. sbIndex is the toggle cursor.
|
||||
sbHidden map[string]bool
|
||||
sbIndex int
|
||||
|
||||
// actionsHidden mutes the inline action rows (tool calls / writes / approvals / grants) in
|
||||
// the OUTPUT transcript; they always stay in the EVENTS panel. Persisted in tui-prefs.json.
|
||||
actionsHidden bool
|
||||
|
||||
// outputScroll is how many rows up from the bottom the OUTPUT transcript is scrolled
|
||||
// (0 = tail-follow the newest output). PgUp/PgDn + ctrl+u/d move it; esc snaps back.
|
||||
outputScroll int
|
||||
|
||||
// event-inspector filter (OverlayEventInspector): narrows the event list by a substring
|
||||
// of type/detail. eventFilterTyping is true while the operator is editing the query after /.
|
||||
eventFilter string
|
||||
eventFilterTyping bool
|
||||
|
||||
// standing grants (OverlayGrants) — active PROJECT/GLOBAL grants from the grant.list reply.
|
||||
grants []protocol.GrantDto
|
||||
grantsLoading bool
|
||||
grantIndex int
|
||||
// grant scope picker (OverlayGrantScope) — chosen when the operator presses A on an approval.
|
||||
// grantScopeIndex selects session/project/global; grantFor holds the approval being widened.
|
||||
grantScopeIndex int
|
||||
grantFor *Approval
|
||||
|
||||
// animation
|
||||
frame int // tick counter; drives spinner + caret blink
|
||||
frame int // tick counter; drives spinner + caret blink
|
||||
ticking bool // true while a tick loop is scheduled. The loop is gated on animating()
|
||||
// so an idle screen stops redrawing — which lets native terminal selection survive
|
||||
// (the 120ms redraw used to wipe a mouse drag every frame).
|
||||
|
||||
// snapshot phase
|
||||
snapshotPhase bool
|
||||
pendingEvents []protocol.ServerMessage
|
||||
|
||||
// lastBase is the full-screen render behind the active modal, stashed by View each
|
||||
// frame so center() can composite a modal over a dimmed copy of it (transparent
|
||||
// backdrop) instead of an opaque scrim. Transient render scratch — not real state.
|
||||
lastBase string
|
||||
|
||||
// approval steering input buffer
|
||||
steerBuffer string
|
||||
steering bool
|
||||
// approvalArmed is set while a high-tier (T3+) approve is awaiting its second
|
||||
// confirming keypress — the destructive-action safety. Any non-confirm key
|
||||
// disarms it; a confirming `y`/`a`/enter sends the decision.
|
||||
approvalArmed bool
|
||||
|
||||
// clarification view (the interactive question form)
|
||||
clarFocus int // focused question index
|
||||
@@ -301,7 +417,6 @@ func NewModel(client *ws.Client) Model {
|
||||
theme: NewTheme(SoftBlue),
|
||||
wfIndex: -1,
|
||||
inputMode: ModeRouter,
|
||||
history: map[string][]string{},
|
||||
historyIndex: -1,
|
||||
routerMessages: map[string][]RouterEntry{},
|
||||
configStaged: map[string]string{},
|
||||
@@ -309,6 +424,9 @@ func NewModel(client *ws.Client) Model {
|
||||
providerType: "LOCAL",
|
||||
snapshotPhase: true,
|
||||
eventStripShown: true,
|
||||
transcriptSel: -1,
|
||||
sbHidden: loadStatusbarHidden(),
|
||||
actionsHidden: loadPrefs().InlineActionsHidden,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +466,79 @@ func (m Model) session(id string) *Session {
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncPending re-points Pending at the selected queue entry, clamping the index
|
||||
// so it stays in range as the queue grows and shrinks. Called after any queue
|
||||
// mutation; Pending is nil exactly when the queue is empty.
|
||||
func (s *Session) syncPending() {
|
||||
if len(s.PendingQueue) == 0 {
|
||||
s.PendingQueue = nil
|
||||
s.PendingIdx = 0
|
||||
s.Pending = nil
|
||||
return
|
||||
}
|
||||
if s.PendingIdx < 0 {
|
||||
s.PendingIdx = 0
|
||||
}
|
||||
if s.PendingIdx >= len(s.PendingQueue) {
|
||||
s.PendingIdx = len(s.PendingQueue) - 1
|
||||
}
|
||||
s.Pending = s.PendingQueue[s.PendingIdx]
|
||||
}
|
||||
|
||||
// enqueueApproval adds a gate to the queue (replacing one with the same request
|
||||
// id, so a re-sent gate doesn't duplicate). The freshly-added gate is left where
|
||||
// it is in the order; the current selection is preserved.
|
||||
func (s *Session) enqueueApproval(a *Approval) {
|
||||
for i, p := range s.PendingQueue {
|
||||
if p.RequestID == a.RequestID {
|
||||
s.PendingQueue[i] = a
|
||||
s.syncPending()
|
||||
return
|
||||
}
|
||||
}
|
||||
s.PendingQueue = append(s.PendingQueue, a)
|
||||
s.syncPending()
|
||||
}
|
||||
|
||||
// removeApproval drops the gate with requestID and advances the selection. If the
|
||||
// removed entry was before the cursor the index shifts down to stay on the same
|
||||
// gate; if it was the selected one the cursor stays put (now showing the next gate).
|
||||
func (s *Session) removeApproval(requestID string) {
|
||||
idx := -1
|
||||
for i, p := range s.PendingQueue {
|
||||
if p.RequestID == requestID {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return
|
||||
}
|
||||
s.PendingQueue = append(s.PendingQueue[:idx], s.PendingQueue[idx+1:]...)
|
||||
if idx < s.PendingIdx {
|
||||
s.PendingIdx--
|
||||
}
|
||||
s.syncPending()
|
||||
}
|
||||
|
||||
// clearApprovals empties the queue (used on resume/completion, where the server
|
||||
// invalidates every gate at once).
|
||||
func (s *Session) clearApprovals() {
|
||||
s.PendingQueue = nil
|
||||
s.PendingIdx = 0
|
||||
s.Pending = nil
|
||||
}
|
||||
|
||||
// navApproval moves the queue selection by dir (wrapping) and re-syncs Pending.
|
||||
func (s *Session) navApproval(dir int) {
|
||||
n := len(s.PendingQueue)
|
||||
if n <= 1 {
|
||||
return
|
||||
}
|
||||
s.PendingIdx = (s.PendingIdx + dir + n) % n
|
||||
s.syncPending()
|
||||
}
|
||||
|
||||
// ensureSession returns the session with id, creating an ACTIVE entry if absent.
|
||||
// Session existence is derived from the event stream (any event for an unknown
|
||||
// session vivifies it) rather than a dedicated control frame.
|
||||
|
||||
Reference in New Issue
Block a user