mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-12 16:23:18 -10:00
* feat(tui): chat + context sidebar redesign, AGENTS panel, animations, plan-timer fix
Two-column managed-mode layout: the existing chat (scroll engine, reduced width)
on the left + a new right context sidebar — AGENTS (spawned subagents: status
glyph, name, and a live working detail for running ones, with a running/total
count), the live PLAN, and a token readout. The sidebar is suppressed on the
home screen and under overlays/subchat; alt-screen managed mode, wide terminals.
Also: freeze the plan clock while the agent is idle (an in_progress step left
mid-plan when the agent yields stops ticking instead of counting forever), and a
clean-room animation port (phase scroll indicator + a cosine ripple working
line; the spinner was already animated, frozen under ZERO_REDUCED_MOTION).
Reuses the scroll/viewport engine; inline mode unchanged. New unit tests for the
sidebar, plan-clock freeze, and the pure animation helpers; go build/vet/test green.
* feat(swarm): live AGENTS sidebar + un-defer coordination tools
Sidebar AGENTS section now reflects the swarm/team accurately:
- name each member by a short 1-2 word task (not subagent-N)
- a mild, slow cool pulse on live member names (no per-letter flicker)
- members disappear when swarm_status reports them [done]/[failed]
- hide bogus "specialist not found" tool-misroute attempts
- keep the animation tick alive while sidebar agents exist
Plus the deeper fix so a coordinating model stops misrouting swarm calls
to the specialist tool ("specialist swarm_send not found"): the swarm
COORDINATION tools (swarm_send/status/inbox/collect) un-defer once a
swarm is active, while spawn/schedule/handoff stay discoverable via
tool_search. A new DeferralEligible() interface keeps all swarm tools
counting toward DeferThreshold even when exposed, so un-deferring can
never deactivate deferral / force-expose MCP tools in any config.
* feat(tui): polish pass — hierarchy, status line, motion, sidebar toggle
A grounded polish pass across the TUI (17 items):
- color: spread the gray tiers (AA-safe), lime working-ripple (no semantic
green/amber), calm steady ask-label, dim completed plan-step bodies,
bold-muted sidebar headers + stateful PLAN count
- status line: run-state chip (permission mode + effort) replaces the
duplicated provider; composer rule is model-only; context-fill gauge down to
the narrow tier + a sidebar context-% chip
- footer: persistent idle key-hint (managed mode) + a jump-to-bottom cue
- motion: pulsing streaming caret; running specialist spins; swarm/specialist
agents linger with a fading ✓ before removal (no abrupt pop)
- layout: Ctrl+B toggles the sidebar; padded ' │ ' divider; sidebar gated to
the medium tier so it never starves the chat
- home screen: cwd · branch · model orientation + example prompts
- cleanup: drop dead autoTag, fix 'six swarm tools' miscount
go build/vet/test green; -race clean; no hex outside theme.go.
* feat(tui): reading column for the chat transcript
Cap transcript body rows at a readable ~90-col measure and indent them by a
small left gutter, so on a wide terminal the chat text no longer runs
edge-to-edge. Only true transcript rows use the reading column; the frame,
composer, status line, sidebar, title bar, empty state, and prompts keep the
full chatColumnWidth. Streaming text shares the column so it doesn't snap on
finalize. Plus extra vertical spacing before headings and between paragraphs.
Implementation keeps the height cache valid (rows render at contentWidth, which
the width-keyed cache already captures) and the gutter is a horizontal post-pad;
each selectable line's textStart is shifted by the gutter so click-to-select and
the selection highlight stay aligned. Mouse card/toggle hit-testing is Y-based,
so it's unaffected. go build/vet/test green; -race clean.
* fix(tui): plain cancellation marker + copyable card/toggle rows
- 'Run cancelled.' renders as a plain amber '⊘ Run cancelled.' line instead of a
heavy box (other system notices keep their box).
- Copy root cause: the app's text-selection skipped 'button' rows (collapsible
toggles, specialist cards) — they carried no selectable text, so a drag-select
omitted exactly that (dull) content. Fix: those rows now carry their plain text
+ start, so a selection dragged THROUGH them copies their content; a direct
click still toggles/drills (resolved on press, before selection). The selection
highlight no longer excludes them. Permission buttons stay excluded.
- Surface Ctrl+E in the idle hint — it releases the mouse for native terminal
selection, which copies everything (the zero-risk full-copy path).
go build/vet/test green; -race clean.
* fix(tui): render all system notices as plain lines, not boxes
Every system notice (mouse-mode changes, mode set, run cancelled, etc.) now
renders as a plain marked line instead of a bordered box: a faint '·' marker +
muted text for info, amber '⊘' for a run cancellation. Multi-line notices keep
the marker on the first line and indent the rest. Command cards and error rows
keep their boxes. go build/vet/test green.
* fix(tui): separate think→act tool groups with a blank line
Reasoning headers (Thought for X) interleave the tool cards (tool → thought →
tool …), which broke the existing tool-card→tool-card gap rule, so consecutive
list_directory/read_file/update_plan groups packed into a dense wall. Now a
reasoning header that follows a tool card gets a blank line above it, so each
think→act group reads as a distinct block. go build/vet/test green.
* feat(tui): colour markdown emphasis + headings for readability
Assistant prose now reads as three tiers instead of one flat ink colour:
- body text stays ink
- bold / important words render in the brand accent (lime) so they pop
- markdown headings render accent + bold + underline as a clear top tier
go build/vet/test green.
* fix(tui): calmer prose colours + a working-phase label
- Revert the accent colour on bold/important words: emphasis is weight-only now
(no colour), dark-mode-friendly.
- Headings drop the bright lime — distinguished by ink + bold + underline instead
of a saturated accent colour.
- Working status line gains a phase label ('writing' while the answer streams,
'thinking' otherwise) so a long, output-less step reads as live progress
('Working · thinking · 55s') rather than a frozen screen.
go build/vet/test green.
* fix(tui): paint selection highlight in the gutter-shifted coordinate
Mouse text-selection looked like it started in the wrong place: the highlight
landed a few cells right of where you clicked, so selecting/copying felt
impossible to aim.
Cause: the row renderers painted the selection highlight on the UNshifted,
unpadded line, then the body item applied the reading-column gutter (pad lines +
shift selectable textStart) afterward. But the stored selection points are in the
shifted/displayed coordinate (that's how the mouse maps them), so the highlight
was computed against unshifted textStart and landed gutter-cells off. (The copied
text was actually correct — extraction uses the shifted selectable — but the
visual selection was wrong, which is what made it un-aimable.)
Fix: stop highlighting inside the row renderers; add finalizeTranscriptBodyRow,
which pads + shifts THEN paints the highlight, so it's computed in the same
coordinate the mouse uses and lands exactly on the click. Also applies the
highlight uniformly to all rows + the interim block. Regression test added.
93 lines
3.6 KiB
Go
93 lines
3.6 KiB
Go
package swarm
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/Gitlawb/zero/internal/tools"
|
|
)
|
|
|
|
// With NO active swarm, all swarm tools must be deferred-eligible so their
|
|
// schemas stay out of the eager per-request tool prefix (loaded on demand via
|
|
// tool_search), while the core built-ins remain eager. (A zero-value &Swarm{}
|
|
// has a nil coordinator, so hasActiveSwarm reports inactive — see nil-safety.)
|
|
func TestSwarmToolsAreDeferred(t *testing.T) {
|
|
registry := tools.NewRegistry()
|
|
RegisterTools(registry, &Swarm{})
|
|
for _, tool := range registry.All() {
|
|
if !tools.IsDeferred(tool) {
|
|
t.Errorf("swarm tool %q is NOT deferred-eligible (would bloat the eager prefix)", tool.Name())
|
|
}
|
|
}
|
|
if len(registry.All()) != 7 {
|
|
t.Fatalf("expected 7 swarm tools registered, got %d", len(registry.All()))
|
|
}
|
|
// Every swarm tool counts toward the deferral threshold (deferral-eligible),
|
|
// cold start and always.
|
|
for _, tool := range registry.All() {
|
|
if !tools.IsDeferralEligible(tool) {
|
|
t.Errorf("swarm tool %q should be deferral-eligible", tool.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Once a swarm is active, the COORDINATION tools un-defer (IsDeferred==false) so a
|
|
// coordinating model sees them eagerly and doesn't misroute the calls to the
|
|
// specialist tool. The entry-point/handoff tools stay deferred for cold-start
|
|
// discovery. Critically, the un-deferred tools STAY deferral-eligible, so the
|
|
// global deferral count is unchanged and other deferred tools (MCP) are never
|
|
// force-exposed — in any threshold/filter config.
|
|
func TestSwarmCoordinationToolsUndeferWhenActive(t *testing.T) {
|
|
reg, sw := newToolSwarm(t, newLauncher(okFor))
|
|
|
|
// Cold start: every swarm tool is deferred.
|
|
for _, tool := range reg.All() {
|
|
if !tools.IsDeferred(tool) {
|
|
t.Errorf("with no active swarm, %q should be deferred", tool.Name())
|
|
}
|
|
}
|
|
|
|
// Activate the swarm by registering a task in the coordinator.
|
|
if _, err := sw.Coordinator().Register("t1", "teammate-1", "default", "do work"); err != nil {
|
|
t.Fatalf("register task: %v", err)
|
|
}
|
|
|
|
undeferred := map[string]bool{SendToolName: true, StatusToolName: true, InboxToolName: true, CollectToolName: true}
|
|
stillDeferred := map[string]bool{SpawnToolName: true, ScheduleToolName: true, HandoffToolName: true}
|
|
eligibleCount := 0
|
|
for _, tool := range reg.All() {
|
|
name := tool.Name()
|
|
got := tools.IsDeferred(tool)
|
|
// The active-gate counts deferral-ELIGIBLE tools; every swarm tool must
|
|
// stay eligible even when un-deferred, so the count never drops.
|
|
if !tools.IsDeferralEligible(tool) {
|
|
t.Errorf("swarm tool %q must stay deferral-eligible even when un-deferred", name)
|
|
} else {
|
|
eligibleCount++
|
|
}
|
|
switch {
|
|
case undeferred[name] && got:
|
|
t.Errorf("with an active swarm, coordination tool %q should NOT be deferred", name)
|
|
case stillDeferred[name] && !got:
|
|
t.Errorf("entry/handoff tool %q must stay deferred", name)
|
|
}
|
|
}
|
|
// All 7 stay deferral-eligible (the count partitionTools' active-gate uses),
|
|
// so un-deferring the 4 coordination tools cannot deactivate deferral.
|
|
if eligibleCount != 7 {
|
|
t.Fatalf("expected all 7 swarm tools deferral-eligible while active, got %d", eligibleCount)
|
|
}
|
|
}
|
|
|
|
// hasActiveSwarm must be nil-safe: a nil *Swarm or a zero-value Swarm (nil
|
|
// coordinator) reports inactive, so the coordination tools stay deferred rather
|
|
// than panicking — this is what keeps TestSwarmToolsAreDeferred (&Swarm{}) green.
|
|
func TestHasActiveSwarmNilSafe(t *testing.T) {
|
|
var nilSwarm *Swarm
|
|
if nilSwarm.hasActiveSwarm() {
|
|
t.Error("nil swarm should report inactive")
|
|
}
|
|
if (&Swarm{}).hasActiveSwarm() {
|
|
t.Error("zero-value swarm (nil coord) should report inactive")
|
|
}
|
|
}
|