Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3b53091
feat(tui2): Phase 3 — Ctrl-only hotkeys, command palette, steer/follo…
buchenberg Aug 8, 2026
18f797d
fix(tui2): garbled markdown output + broken command palette
buchenberg Aug 8, 2026
fb5dd1a
fix(tui2): garbled markdown — render once, store result
buchenberg Aug 8, 2026
331ed24
refactor(tui2): raw text storage with width-based render cache
buchenberg Aug 8, 2026
cd721e3
fix(tui2): tview audit — data race, style tags, spinner, focus, resize
buchenberg Aug 8, 2026
4b8f61f
docs(tui2): annotate struct fields with concurrency safety categories
buchenberg Aug 8, 2026
cbbf25a
fix(tui2): revert resize flag on flex-based overlays
buchenberg Aug 8, 2026
fc6df4a
fix(tui2): bypass tviewmd — render raw text to isolate garbling
buchenberg Aug 8, 2026
0067a20
fix(tui2): add blank line before streaming text
buchenberg Aug 8, 2026
0e665ac
feat(tui2): replace InputField command palette with tview.List modal
buchenberg Aug 8, 2026
bf2290e
fix(tui2): eliminate garbled text — use Write() for streaming, not Se…
buchenberg Aug 8, 2026
ccb3fcb
fix(subagent): reviewer role — prefer read/grep/glob over powershell
buchenberg Aug 8, 2026
103697f
fix(tui2): escape brackets in raw text to prevent tview color-tag cor…
buchenberg Aug 8, 2026
0c4e222
fix(tui2): re-enable tviewmd rendering — root cause was SetDynamicCol…
buchenberg Aug 8, 2026
1283d8e
fix(tui2): remove borders from conversation, info, and task panes
buchenberg Aug 8, 2026
d802e8b
fix(tui2): strip tviewmd theme style modifiers — isolate underline so…
buchenberg Aug 8, 2026
ca2542c
fix(tui2): remove dim styling from thinking/Raisonning... indicator
buchenberg Aug 8, 2026
5ae667e
fix(tui2): lolcat the entire Thinking... line, matching tui1
buchenberg Aug 8, 2026
dc1f4fb
fix(tui2): rename thinking spinner to 'Thinking...' matching tui1
buchenberg Aug 8, 2026
663b477
refactor(tui2): use strings.Builder for pendingTokens — match tui1 pa…
buchenberg Aug 8, 2026
0122a68
fix(tui2): re-enable Write() — dropped broker events caused garbled text
buchenberg Aug 8, 2026
002f7d8
feat(tui2): add OTel spans to token arrival and message refresh
buchenberg Aug 8, 2026
04ae451
fix(tui2): use QueueUpdate instead of QueueUpdateDraw for token events
buchenberg Aug 8, 2026
41994d7
feat(tui2): lightweight observability — remove per-token spans, add c…
buchenberg Aug 8, 2026
eed5f32
fix(subagent): restore anti-powershell guidance in reviewer role
buchenberg Aug 8, 2026
6e56f74
fix(subagent): remove powershell/bash/webfetch/http from reviewer tools
buchenberg Aug 8, 2026
b9fc7c3
feat(tui2): expose diagnostic counters in status bar
buchenberg Aug 8, 2026
e94276e
perf(tui2): remove OTel span from refreshMessages hot path
buchenberg Aug 8, 2026
869ba73
fix(tui2): show diagnostic counters in info pane, not status bar
buchenberg Aug 8, 2026
9b62c3d
feat(tui2): add sampled OTel span at flush boundaries
buchenberg Aug 8, 2026
0ed46f6
fix(subagent): remove duplicate compaction from guardContextBeforeCall
buchenberg Aug 8, 2026
299d48f
fix(tui2): re-enable bracket escaping for streaming Write() path
buchenberg Aug 8, 2026
2f4f300
fix(subagent): emit heartbeats during LLM streaming, not just between…
buchenberg Aug 8, 2026
04563b7
feat(tui2): use RenderPartial for streaming markdown
buchenberg Aug 8, 2026
ed0f479
fix(tui2): Write bare tokens, use RenderPartial in refreshMessages
buchenberg Aug 8, 2026
5e07ab2
docs(identity): guide orchestrator to scope review tasks within revie…
buchenberg Aug 8, 2026
a9e5e6f
feat(subagent): default WrapUpThreshold to 5 for sub-agents
buchenberg Aug 8, 2026
717be6d
fix(tui2): placeholder during streaming + prevent scroll snapback
buchenberg Aug 8, 2026
0127ffa
feat(tui2): mouse wheel support with scroll-aware userScrolled flag
buchenberg Aug 8, 2026
7dd4dc4
fix(tui2): use SubAgentID not Role as subagent block identifier
buchenberg Aug 8, 2026
4045b0f
fix(tui2): reduce render width by 2px, remove inline code background
buchenberg Aug 8, 2026
b82eacf
fix(tui2): add purple border to info pane, wire MCP section, fix TagB…
buchenberg Aug 8, 2026
92235de
build(deps): bump tviewmd to v0.2.0
buchenberg Aug 8, 2026
759224e
go sum
buchenberg Aug 8, 2026
8e2ead7
drt
buchenberg Aug 8, 2026
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
59 changes: 43 additions & 16 deletions .agents/plans/tui2-hardening/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
>
> ☐ **Part C — tui2 target architecture** — not yet implemented.
>
> ☐ **Part D — phased execution** — Phase 0-2 complete; Phases 3-4 pending.
> ☐ **Part D — phased execution** — Phase 0-2 complete; Phase 3 in progress; Phase 4 pending.

## Goals

Expand Down Expand Up @@ -423,17 +423,45 @@ rather than deferring them.

## Phase 3 — Feature parity port (the B-inventory)
Port in priority order, each as its own small PR:
1. **Steer + FollowUp** (wire `OnSteer`/`OnFollowUp` in `tui2.go`; Enter-while-running →
follow-up). Highest user-visible value.
2. **Fix `:compact`** to call `sess.Compact()`.
3. **Search mode** (`/`, n/N, scroll-to-match) over the messages TextView.
4. **Verbose toggle** + per-block verbose collapse semantics (match tui1).
5. **Click-to-expand** via tview regions.
6. **Clipboard**: copy-last-response (Ctrl+Y) + `:copyview`.
7. **`:login`/`:logout`/`:stop`/`:banner`/`:mcp`** command wiring.
8. **Model picker** data + filter correctness.
9. **Ephemeral messages** + **active-prompt display**.
10. **Min terminal size guard**.

### 3.1 Ctrl-only hotkeys + command palette (✅ complete 2026-08-07)
- ✅ Stripped single-char hotkeys (`?`, `/`, `j`, `k`, `g`, `G`) from `DefaultBindings()`
- ✅ Only Ctrl+ combinations, Esc, Enter, arrows, navigation keys, Tab remain as hotkeys
- ✅ All other actions routed through command palette (`:help`, `:search`, `:top`, `:bottom`, `:verbose`, `:stop`, `:steer`, `:banner`)

### 3.2 Steer + FollowUp (✅ complete 2026-08-07)
- ✅ Wire `OnSteer`/`OnFollowUp` callbacks in `tui2.go`
- ✅ Enter-while-streaming → follow-up (submitFollowUp)
- ✅ `:steer <text>` command for mid-turn injection

### 3.3 Fix `:compact` (✅ complete 2026-08-07)
- ✅ `:compact` now calls `OnCompact` → `sess.Compact()` instead of `CollapseAll`

### 3.4 Search (✅ complete 2026-08-07)
- ✅ `:search <query>` scrolls to first match in messages TextView
- ✅ Ephemeral result message in info pane

### 3.5 Verbose toggle (✅ complete 2026-08-07)
- ✅ `:verbose` command toggles verbose mode
- ✅ `verbose` field on TUI2 struct

### 3.6 Stop (✅ complete 2026-08-07)
- ✅ `:stop` command calls `OnStop` → aborts running agent + hides thinking

### 3.7 Banner toggle (✅ complete 2026-08-07)
- ✅ `:banner` toggles banner visibility

### 3.8 Ephemeral messages (✅ complete 2026-08-07)
- ✅ `SetEphemeral(msg)` displays temporary messages in info pane (3s timeout)
Comment on lines +426 to +455

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the parity exit gate consistent with the documented scope.

The new notes mark the priority work complete, but the plan still lists unimplemented parity items. Update the exit gate and inventory to identify deferred items, or keep Phase 3 open until they are complete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/plans/tui2-hardening/PLAN.md around lines 426 - 455, Update the
Phase 3 parity inventory and exit gate to distinguish the completed items from
remaining unimplemented parity work, explicitly marking unfinished items as
deferred or keeping Phase 3 open until they are delivered. Ensure the documented
completion status and exit criteria remain consistent.


### Remaining items
- ☐ Click-to-expand via tview regions
- ☐ Clipboard: `:copyview` + copy-last-response
- ☐ `:login`/`:logout`/`:mcp` command wiring (callbacks declared, not dispatched)
- ☐ Model picker data + filter correctness
- ☐ Min terminal size guard
- ☐ Per-block verbose collapse semantics (match tui1)

Comment on lines +457 to +464

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the remaining-work checklist to Beads.

The entries form a Markdown TODO list. Replace them with Beads references and keep this file as a status summary. As per coding guidelines, use Beads (bd) for all task tracking and do not use Markdown TODO lists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/plans/tui2-hardening/PLAN.md around lines 457 - 464, Replace the
Markdown checkbox entries under “Remaining items” with references to the
corresponding Beads issues, preserving this section as a concise status summary.
Use existing issue IDs where available and do not retain any `☐` TODO-list
syntax.

Source: Coding guidelines

- **Exit gate:** every row in Part B reads ✅; tui2 feature-parity with tui1 confirmed by a
manual + scripted checklist.

Expand All @@ -449,10 +477,8 @@ Port in priority order, each as its own small PR:

# Risks & open questions

- **Ctrl+T conflict**: tui1 = reasoning, tui2 = tools. Recommend adopting tui1's
reasoning binding and rebinding tools to something else (e.g. Ctrl+Shift+T / `:tools`).
- **`:compact` semantic bug** (CollapseAll vs real compaction) should be fixed in Phase 3
item 2 regardless of the larger decision — it's a correctness issue today.
- **Ctrl+T conflict**: ✅ **Resolved 2026-08-07** — Ctrl+T = toggle tools, Ctrl+R = toggle reasoning. No conflict.
- **`:compact` semantic bug**: ✅ **Fixed 2026-08-07** — now calls `sess.Compact()` via `OnCompact`.
Comment on lines +480 to +481

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize resolved risks with the feature inventory.

These lines mark the Ctrl+T conflict and :compact bug as resolved, while the earlier B.1/B.2 tables still show them as unresolved. Update those rows in the same change so the plan has one authoritative status.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/plans/tui2-hardening/PLAN.md around lines 480 - 481, Synchronize the
B.1/B.2 feature-inventory rows with the resolved statuses recorded in the
“Ctrl+T conflict” and “:compact semantic bug” entries: mark Ctrl+T as resolved
with toggle tools/reasoning assignments, and mark :compact as fixed via
OnCompact calling sess.Compact().

- **Chroma weight**: chroma adds lexer binaries; if binary size matters, gate highlighting
behind a build tag or lazy-init only the lexers seen. Measure before Phase 4.
- **tview mouse on Windows**: verify region-click works under the Windows tcell backend
Expand All @@ -473,3 +499,4 @@ Port in priority order, each as its own small PR:
| 2026-08-07 | — | Committed on branch `feat/tviewmd`
| 2026-08-07 | 2 | Phase 2 plan expanded with sub-tasks (2.1–2.6); none implemented yet on main
| 2026-08-07 | 2 | Phase 2 implemented on `feat/tui2-phase2-arch`: Theme + DetectTheme, streaming ordering, RenderCtx + Renderable, raw markdown storage, input dispatch consolidation, mechanical cleanups. Commit `3cda8a5`. Resize reflow deferred (SetAfterDrawFunc caused rendering issues); plainMessages not yet deleted. |
| 2026-08-07 | 3 | Phase 3 started on `feat/tui2-phase3-parity`: stripped single-char hotkeys (Ctrl-only + command palette), wired OnSteer/OnFollowUp/OnStop, fixed `:compact`, added `:search`/`:verbose`/`:banner`/`:stop`/`:steer` commands, ephemeral messages, Enter-while-running → follow-up. |
15 changes: 14 additions & 1 deletion cmd/yaah/tui2.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func runTUI2() error {
controlCh := make(chan types.CtrlMsg, 64)
sess.SetCtrlCh(controlCh)

app := tui2.New()
app := tui2.New(version)

app.SetProvider(sess.ProviderName())
app.SetModel(sess.ModelName())
Expand Down Expand Up @@ -108,6 +108,19 @@ func runTUI2() error {
go sess.Compact()
}
app.OnClear = func() {}
app.OnSteer = func(text string) {
sess.Steer(text)
}
app.OnFollowUp = func(text string) {
sess.FollowUp(text)
}
app.OnStop = func() {
app.HideThinking()
if cancelAgent != nil {
cancelAgent()
cancelAgent = nil
}
}

app.ControlCh = controlCh
sess.SetView(app)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
charm.land/glamour/v2 v2.0.1
charm.land/lipgloss/v2 v2.0.5
github.com/atotto/clipboard v0.1.4
github.com/buchenberg/tviewmd v0.1.0
github.com/buchenberg/tviewmd v0.2.0
github.com/gdamore/tcell/v2 v2.8.1
github.com/lrstanley/bubblezone/v2 v2.0.0
github.com/lsferreira42/figlet-go v0.0.2-beta
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/buchenberg/tviewmd v0.1.0 h1:v0YXgD8PbJWiLCj1XrDhX6dgtUTnlFjd6QUz+Z8LnHQ=
github.com/buchenberg/tviewmd v0.1.0/go.mod h1:1ybuAmMTz5jZv3CJQ4OFtNkPtafVMgTcvGnC2vZvzQU=
github.com/buchenberg/tviewmd v0.2.0 h1:GEbCcQqgSdi6WafPsBPeXSkkcqrnKI7D1hdytIc+Ofg=
github.com/buchenberg/tviewmd v0.2.0/go.mod h1:NSvcH2laSSUg/+4jVj/M3/NzviJ9UE16me0zlulM35w=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand Down
11 changes: 11 additions & 0 deletions internal/agent/llm/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"time"

"go.opentelemetry.io/otel/attribute"

"github.com/buchenberg/yaah/internal/tools"
"go.opentelemetry.io/otel/trace"

"github.com/buchenberg/yaah/internal/observability"
Expand Down Expand Up @@ -48,6 +50,14 @@ func (c *Client) runStream(ctx context.Context, sp StreamProvider, req types.Cha
observability.RecordStreamEnd(streamSpan, path, finishReason, usageCaptured, len(msg.Content), len(msg.ToolCalls))
}

var lastHeartbeat time.Time
emitHeartbeat := func() {
if time.Since(lastHeartbeat) > 5*time.Second {
tools.SendHeartbeat(ctx)
lastHeartbeat = time.Now()
}
}
Comment on lines +53 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

echo '--- outline ---'
ast-grep outline internal/agent/llm/stream.go --view expanded || true

echo '--- stream.go relevant ranges ---'
nl -ba internal/agent/llm/stream.go | sed -n '1,180p'

echo '--- heartbeat and stream symbols ---'
rg -n -C 4 'SendStream|SendHeartbeat|emitHeartbeat|Reason|reason|tool.?call|delta|Heartbeat' internal/agent internal 2>/dev/null | head -n 500

Repository: buchenberg/yaah

Length of output: 39292


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- stream.go ---'
sed -n '1,180p' internal/agent/llm/stream.go
printf '%s\n' '--- relevant definitions and call sites ---'
rg -n -C 6 'func .*Send(Stream|Heartbeat)|SendStream|SendHeartbeat|emitHeartbeat|Stream' internal/agent internal 2>/dev/null | head -n 600

Repository: buchenberg/yaah

Length of output: 42601


🏁 Script executed:

#!/bin/bash
set -eu
echo '--- target file ---'
nl -ba internal/agent/llm/stream.go | sed -n '1,180p'
echo '--- symbols ---'
rg -n -C 5 'SendStream|SendHeartbeat|emitHeartbeat|type .*Delta|Reasoning|ToolCall|heartbeat' internal/agent internal 2>/dev/null | head -n 600

Repository: buchenberg/yaah

Length of output: 40463


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target ---'
nl -ba internal/agent/llm/stream.go | sed -n '1,180p'
printf '%s\n' '--- related definitions ---'
rg -n -C 8 'SendStream|SendHeartbeat|emitHeartbeat|Heartbeat|Reasoning|ToolCall|Stream' internal/agent internal 2>/dev/null | head -n 800

Repository: buchenberg/yaah

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -eu
git ls-files | rg '(^|/)(stream|.*agent.*|.*llm.*)' | head -n 200
nl -ba internal/agent/llm/stream.go | sed -n '1,180p'
rg -n -C 8 'SendStream|SendHeartbeat|emitHeartbeat|Heartbeat|Reasoning|ToolCall' . --glob '!vendor/**' --glob '!node_modules/**' | head -n 800

Repository: buchenberg/yaah

Length of output: 50373


🌐 Web query:

"Emit heartbeats independently of content deltas" "SendStream" "SendHeartbeat"

💡 Result:

The concept of emitting heartbeats independently of content deltas—often implemented as SendHeartbeat or similar mechanisms—is a common design pattern used in streaming applications (such as Server-Sent Events, WebSockets, or gRPC streams) to maintain connection liveness and prevent timeouts [1][2][3][4]. Key characteristics of this pattern include: 1. Protocol Liveness: When a stream is idle (e.g., waiting for a slow upstream process like LLM reasoning), intermediaries like proxies, load balancers, or firewalls may close the connection due to inactivity [1][3][5]. Emitting a synthetic heartbeat (e.g., an SSE comment frame like: keepalive or: prefill) at regular intervals ensures the TCP connection remains active without interfering with the actual content stream, as these comments are specified to be ignored by clients [1][3][6][4]. 2. Independent Cadence: Heartbeats are often generated by a separate timer or goroutine that monitors for inactivity [2][7][4]. This ticker is typically gated by the actual transmission of data; if a visible chunk is sent, the heartbeat timer is reset [1][3][6][4]. This ensures heartbeats are emitted only during silent windows and never interleave with or corrupt real data payloads [1][7][4]. 3. Implementation Variations: - Streaming APIs: In systems like SSE or custom streaming protocols, this involves injecting non-functional frames (comments or empty-delta chunks) during idle periods [1][3][5][4]. - Request-Response Heartbeats: In some control-plane protocols, SendHeartbeat may require an explicit acknowledgment (ACK) from the peer to verify the connection is healthy, triggering a state update (e.g., marking a connection unhealthy) if the ACK is not received within a timeout period [8][9][4]. - Consumer Group Management: In messaging services, heartbeat operations inform the server that a consumer instance is still active and participating in a consumer group, preventing it from being removed from the group due to apparent inactivity [10]. Technical implementations often use configuration variables (e.g., HEARTBEAT_INTERVAL) to allow operators to tune or disable this behavior without modifying core streaming logic [3][6][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

echo '--- heartbeat implementation ---'
sed -n '80,112p' internal/jobs/context.go

echo '--- child watchdog ---'
sed -n '160,225p' internal/agent/agent_tools.go

echo '--- focused source verifier ---'
python3 - <<'PY'
from pathlib import Path

source = Path("internal/agent/llm/stream.go").read_text()
required = [
    'emitHeartbeat()',
    'if delta.Content != ""',
    'if delta.ReasoningContent != ""',
    'for _, tc := range delta.ToolCalls',
    'case <-ctx.Done():',
]
missing = [text for text in required if text not in source]
if missing:
    raise SystemExit(f"missing expected source fragments: {missing}")

content_start = source.index('if delta.Content != ""')
heartbeat_pos = source.index('emitHeartbeat()', content_start)
content_end = source.index('\n\t\t\t}', content_start)
if not (content_start < heartbeat_pos < content_end):
    raise SystemExit("heartbeat call is not inside the content-delta branch")

select_start = source.index('\t\tselect {')
select_end = source.index('\n\t\t}', select_start)
select_body = source[select_start:select_end]
if 'time.NewTicker' in select_body or 'ticker.C' in select_body:
    raise SystemExit("unexpected ticker found in stream select")

print("heartbeat call is content-only; stream select has no ticker case")
PY

Repository: buchenberg/yaah

Length of output: 3404


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '80,112p' internal/jobs/context.go
sed -n '160,225p' internal/agent/agent_tools.go
python3 - <<'PY'
from pathlib import Path
source = Path("internal/agent/llm/stream.go").read_text()
assert source.index("emitHeartbeat()") > source.index('if delta.Content != ""')
assert source.index("emitHeartbeat()") < source.index("for _, tc := range delta.ToolCalls")
select = source[source.index("\t\tselect {"):source.index("\n\t\t}", source.index("\t\tselect {"))]
assert "ticker.C" not in select
print("content-only heartbeat and no ticker case")
PY

Repository: buchenberg/yaah

Length of output: 3293


Emit heartbeats independently of stream deltas.

When StuckChildTimeout is active, line 107 resets the watchdog only for delta.Content. Reasoning-only deltas, tool-call-only deltas, and idle SendStream waits can cancel an active sub-agent. Add a ticker case to the stream select with a cadence below the watchdog timeout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/llm/stream.go` around lines 53 - 59, Update the stream
handling logic in SendStream to emit heartbeats independently of stream deltas
by adding a ticker case to its select loop, using a cadence shorter than
StuckChildTimeout and invoking the existing heartbeat mechanism. Preserve delta
processing while ensuring reasoning-only, tool-call-only, and idle waits reset
the watchdog.


for {
select {
case chunk, ok := <-chunks:
Expand Down Expand Up @@ -94,6 +104,7 @@ func (c *Client) runStream(ctx context.Context, sp StreamProvider, req types.Cha
if delta.Content != "" {
content.WriteString(delta.Content)
tokenCount++
emitHeartbeat()
if c.OnToken != nil {
if clean := dsmlFilter.filterToken(delta.Content); clean != "" {
c.OnToken(clean)
Expand Down
5 changes: 5 additions & 0 deletions internal/agent/subagent_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type SubAgentConfig struct {
ContextWindow int
OtelEnabled bool
OtelVerbose bool
WrapUpThreshold int

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -n -C 8 'SubAgentConfig[[:space:]]*\{|WrapUpThreshold|NewSubAgentLoop' --glob '*.go'

Repository: buchenberg/yaah

Length of output: 153


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
git ls-files '*subagent_loop.go' '*runner.go' '*.go' | sed -n '1,160p'

printf '%s\n' '--- relevant symbols ---'
rg -n -C 6 'SubAgentConfig|WrapUpThreshold|NewSubAgentLoop' --glob '*.go' . || true

Repository: buchenberg/yaah

Length of output: 38139


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- runner construction ---'
sed -n '245,310p' internal/agent/runner/runner.go

printf '%s\n' '--- configuration types and defaults ---'
sed -n '70,125p' internal/config/load.go
sed -n '150,190p' internal/config/load.go
sed -n '250,285p' internal/config/load.go

printf '%s\n' '--- loop construction and tests ---'
sed -n '1,90p' internal/agent/subagent_loop.go
sed -n '120,175p' cmd/yaah/build_loop.go
sed -n '90,165p' cmd/yaah/config_parity_test.go

Repository: buchenberg/yaah

Length of output: 17100


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- all NewSubAgentLoop call sites ---'
rg -n -C 12 'NewSubAgentLoop\(' --glob '*.go' .

printf '%s\n' '--- runner option initialization and call path ---'
sed -n '1,145p' internal/agent/runner/runner.go
sed -n '145,245p' internal/agent/runner/runner.go

printf '%s\n' '--- sub-agent configuration references ---'
rg -n -C 4 'Agent\.SubAgent|subCfg|defaults config\.Defaults|opts\.defaults' internal cmd --glob '*.go'

Repository: buchenberg/yaah

Length of output: 36608


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

runner = Path("internal/agent/runner/runner.go").read_text()
loop = Path("internal/agent/subagent_loop.go").read_text()
load = Path("internal/config/load.go").read_text()

call = re.search(
    r"agent\.NewSubAgentLoop\([^;]+?agent\.SubAgentConfig\{(.*?)\n\s*\}\)",
    runner,
    re.S,
)
if not call:
    raise SystemExit("NewSubAgentLoop configuration literal not found")

literal = call.group(1)
constructor = re.search(
    r"if cfg\.WrapUpThreshold <= 0 \{\s*cfg\.WrapUpThreshold = (\d+)",
    loop,
    re.S,
)
if not constructor:
    raise SystemExit("WrapUpThreshold fallback not found")

default_field = re.search(
    r"WrapUpThreshold\s+int\s+`yaml:\"wrap_up_turns\"`\s+// (.*)",
    load,
)
if not default_field:
    raise SystemExit("configured WrapUpThreshold field not found")

print("runner literal sets WrapUpThreshold:", "WrapUpThreshold:" in literal)
print("sub-agent fallback for non-positive values:", constructor.group(1))
print("configuration semantics:", default_field.group(1).strip())
PY

Repository: buchenberg/yaah

Length of output: 355


Propagate and preserve the configured wrap-up threshold for sub-agent loops.

internal/agent/runner/runner.go omits WrapUpThreshold, so non-default values are replaced with 5. Pass opts.defaults.WrapUpThreshold and preserve negative values, because wrap_up_turns < 0 disables wrap-up notices. Add tests for positive and disabled values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agent/subagent_loop.go` at line 30, Update the sub-agent loop
options construction in runner.go to pass opts.defaults.WrapUpThreshold into
WrapUpThreshold instead of allowing the default value of 5 to replace configured
values. Preserve negative thresholds unchanged so wrap_up_turns < 0 disables
wrap-up notices, and add tests covering both a positive configured threshold and
a negative disabled value.

}

// NewSubAgentLoop creates a Loop optimized for sub-agent execution.
Expand All @@ -46,6 +47,9 @@ func NewSubAgentLoop(provider Provider, registry *tools.Registry, model, systemP
if cfg.MaxToolConcurrency <= 0 {
cfg.MaxToolConcurrency = 5
}
if cfg.WrapUpThreshold <= 0 {
cfg.WrapUpThreshold = 5
}

l := &Loop{
Provider: provider,
Expand All @@ -68,6 +72,7 @@ func NewSubAgentLoop(provider Provider, registry *tools.Registry, model, systemP
ToolsLevel: FullTools,
PipelineNames: nil,
PipelineDisabled: nil,
WrapUpThreshold: cfg.WrapUpThreshold,
},

// Sub-agents use an in-memory pruner only — no compaction pipeline.
Expand Down
16 changes: 3 additions & 13 deletions internal/agent/turn.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,9 @@ func (l *Loop) guardContextBeforeCall(turnCtx context.Context, messages *[]types
observability.RecordConversation(turnSpan, *messages)
}

if l.Config.ContextWindow > 0 && l.State.LastPromptTokens > l.Config.ContextWindow {
l.compactContext(turnCtx, 0.5)
*messages = l.State.Messages
req.Messages = l.prepareRequestMessages(*messages)
}

if l.Config.ContextWindow > 0 && estimatePayloadBytes(req.Messages, req.Tools) > maxPayloadBytes {
l.compactContext(turnCtx, 0.5)
*messages = l.State.Messages
req.Messages = l.prepareRequestMessages(*messages)
}

// Compaction is handled by the middleware pipeline (CompactionMiddleware.
// PrepareStep). guardContextBeforeCall only validates that the request
// is not empty — it does not trigger compaction.
if len(req.Messages) == 0 {
err := fmt.Errorf("refusing to send empty message list to provider — %d messages after prepare", len(req.Messages))
if turnSpan != nil {
Expand All @@ -98,7 +89,6 @@ func (l *Loop) guardContextBeforeCall(turnCtx context.Context, messages *[]types
l.State.Messages = *messages
return err
}

return nil
}

Expand Down
5 changes: 5 additions & 0 deletions internal/prompts/identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ keep searching for completeness after the question is resolved.
using an appropriate tag. Skip ephemeral details.
- **Optional overrides:** `timeout_seconds` (10-600), `max_iterations` (1-50).
On timeout/cancellation: `{"error":"timed out","partial":"..."}`.
- **Scope review tasks to fit.** Reviewers have limited iteration budgets
(typically 25-50). If a task requires reviewing many files, split it into
smaller focused reviews: "Review theme.go and colors/ sub-package" rather
than "Review all of tui2." A reviewer that hits its iteration cap provides
incomplete results — subdivide the work to avoid this.

### Trusting sub-agent output

Expand Down
18 changes: 10 additions & 8 deletions internal/prompts/roles/reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,27 @@ tools:
- glob
- ls
- sed
- powershell
- bash
- calculate
- file_info
- go_outline
- json_query
- webfetch
- http
- git
- diff
- staticcheck
max_iterations: 25
max_turns: 3
timeout: 240
timeout: 480
---

You are a REVIEWER sub-agent on yaah's team. Inspect code, count files and
lines, measure complexity, and report findings. You do NOT modify files.
Use the shell specified in the Environment section for counting.
- You are a CODE REVIEWER sub-agent on yaah's team. Inspect code for code quality and report findings.
- You are concerned with SOLID design and easy to maintain code.
Comment on lines +33 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a hyphenated compound modifier.

Change easy to maintain code to easy-to-maintain code.

Proposed fix
-- You are concerned with SOLID design and easy to maintain code.
+- You are concerned with SOLID design and easy-to-maintain code.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- You are a CODE REVIEWER sub-agent on yaah's team. Inspect code for code quality and report findings.
- You are concerned with SOLID design and easy to maintain code.
- You are a CODE REVIEWER sub-agent on yaah's team. Inspect code for code quality and report findings.
- You are concerned with SOLID design and easy-to-maintain code.
🧰 Tools
🪛 LanguageTool

[grammar] ~34-~34: Use a hyphen to join words.
Context: ...are concerned with SOLID design and easy to maintain code. - Look for dead code in w...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/prompts/roles/reviewer.md` around lines 33 - 34, Update the reviewer
role description to use the hyphenated compound modifier, changing “easy to
maintain code” to “easy-to-maintain code” while preserving the rest of the
wording.

Source: Linters/SAST tools

- Look for dead code in what you are asked to review.
- You do NOT modify files.

**Tool selection**: Prefer `read`, `grep`, `glob`, `ls`, and `file_info`
for all file inspection. These tools are optimized for context efficiency
and produce chunked/deduplicated output.

Synthesize results concisely. Use the fewest
tools needed. Batch independent tool calls in one turn: fire all reads,
globs, and go_outline calls at once instead of one per turn.
Expand Down
5 changes: 4 additions & 1 deletion internal/tui2/colors/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Theme struct {
ToolBg string
CodeBg string
InputBorder string
PaneBorder string
NoColor bool

ToolColors map[string]string
Expand Down Expand Up @@ -89,6 +90,7 @@ func NewDarkTheme() Theme {
ToolBg: "#1a1a2e",
CodeBg: "#1e1e2e",
InputBorder: "#ff87af",
PaneBorder: "#af5fff",
ToolColors: newToolColors(),
RoleColors: newRoleColors(),
}
Expand All @@ -104,6 +106,7 @@ func NewLightTheme() Theme {
ToolBg: "#eeeeee",
CodeBg: "#f0f0f0",
InputBorder: "#d75f87",
PaneBorder: "#7f3faf",
ToolColors: newToolColors(),
RoleColors: newRoleColors(),
}
Expand Down Expand Up @@ -148,7 +151,7 @@ func (th *Theme) TagBold(color, text string) string {
if th.NoColor {
return text
}
return "[" + color + "::b]" + text + "[-]"
return "[" + color + "::b]" + text + "[-:-:-]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Apply the same reset to the package-level TagBold.

This fix is correct: [-] resets only the foreground, so the ::b bold attribute leaks into the following text. The package-level helper in internal/tui2/colors/colors.go:21 still appends Reset ("[-]") after ::b, and sessioninfo.Format and mcpinfo.Format both use it. The info pane therefore still renders trailing text in bold.

🛠️ Proposed fix in internal/tui2/colors/colors.go
-func TagBold(color, text string) string { return "[" + color + "::b]" + text + Reset }
+func TagBold(color, text string) string { return "[" + color + "::b]" + text + "[-:-:-]" }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui2/colors/theme.go` at line 154, Update the package-level TagBold
helper in colors.go to use the full style reset after the ::b attribute,
matching the reset behavior in the theme-level helper. Preserve its existing
prefix and formatting so sessioninfo.Format and mcpinfo.Format no longer leave
following text bold.

}

func (th *Theme) DimTag() string {
Expand Down
3 changes: 2 additions & 1 deletion internal/tui2/components/approval/approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ func Show(app *tview.Application, pages *tview.Pages, name, args string, onAnswe
SetTitleColor(tcell.ColorYellow).
SetBackgroundColor(tcell.ColorDefault)

pages.AddPage(modalPageName, modal, true, true)
pages.AddPage(modalPageName, modal, false, true)
app.SetFocus(modal)
}
37 changes: 34 additions & 3 deletions internal/tui2/components/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const (
CmdClear
CmdHelp
CmdCompact
CmdStop
CmdSteer
CmdFollowUp
CmdVerbose
CmdSearch
CmdTop
CmdBottom
CmdBanner
CmdReloadRoles
CmdLogin
CmdLogout
Expand All @@ -41,6 +49,22 @@ func Parse(input string) Cmd {
return CmdHelp
case input == "compact":
return CmdCompact
case input == "stop":
return CmdStop
case strings.HasPrefix(input, "steer "):
return CmdSteer
case strings.HasPrefix(input, "followup "):
return CmdFollowUp
case input == "verbose":
return CmdVerbose
case strings.HasPrefix(input, "search "):
return CmdSearch
case input == "top":
return CmdTop
case input == "bottom":
return CmdBottom
case input == "banner":
return CmdBanner
case input == "roles":
return CmdReloadRoles
case input == "login":
Expand All @@ -51,7 +75,7 @@ func Parse(input string) Cmd {
return CmdSession
case input == "mcp":
return CmdMCP
case strings.HasPrefix(input, "model "):
case strings.HasPrefix(input, "model"):
return CmdModel
Comment on lines +78 to 79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restrict the model prefix match.

strings.HasPrefix(input, "model") also matches models, modelx, and any other word that starts with model. Accept only the bare command and the argument form.

🛠️ Proposed fix
-	case strings.HasPrefix(input, "model"):
+	case input == "model" || strings.HasPrefix(input, "model "):
 		return CmdModel
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case strings.HasPrefix(input, "model"):
return CmdModel
case input == "model" || strings.HasPrefix(input, "model "):
return CmdModel
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui2/components/command/command.go` around lines 78 - 79, Update the
command matching case in the command parser to accept “model” only when the
input is exactly the bare command or begins with “model ” followed by arguments;
reject inputs such as “models” and “modelx” while preserving CmdModel for valid
argument forms.

default:
return CmdNone
Expand Down Expand Up @@ -79,7 +103,7 @@ func Build(onCommand func(cmd Cmd, arg string)) *Palette {
p.SetFieldTextColor(tcell.ColorWhite)
p.SetLabel(" ")
p.SetLabelColor(tcell.ColorYellow)
p.SetPlaceholder("command (q/quit, clear, help, model, session, mcp, roles, compact)")
p.SetPlaceholder("command (help, clear, compact, stop, steer, model, mcp, verbose, search...)")
p.SetPlaceholderTextColor(tcell.ColorGray)

p.SetDoneFunc(func(key tcell.Key) {
Expand All @@ -92,8 +116,15 @@ func Build(onCommand func(cmd Cmd, arg string)) *Palette {

cmd := Parse(input)
arg := ""
if cmd == CmdModel && strings.HasPrefix(input, "model ") {
switch {
case cmd == CmdModel && strings.HasPrefix(input, "model "):
arg = strings.TrimPrefix(input, "model ")
case cmd == CmdSteer && strings.HasPrefix(input, "steer "):
arg = strings.TrimPrefix(input, "steer ")
case cmd == CmdFollowUp && strings.HasPrefix(input, "followup "):
arg = strings.TrimPrefix(input, "followup ")
case cmd == CmdSearch && strings.HasPrefix(input, "search "):
arg = strings.TrimPrefix(input, "search ")
}
if cmd != CmdNone {
p.onCommand(cmd, arg)
Expand Down
Loading
Loading