mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
fix/opencode-session-header
39
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d73ec81e2d |
feat(tui): interactive terminal attach for tty exec sessions (#1055)
* feat(execution): add a stdin-only write for interactive processes * feat(tui): attach overlay for interactive terminal sessions * feat(tui): add /attach for interactive terminal sessions * feat(tools): point the model at tty sessions for interactive prompts * feat(tui): open the terminal automatically for tty exec sessions * feat(tools): tell the model to keep polling while the user types * feat(tui): give attached terminal sessions the full viewport * feat(tools): route sudo through an escalated tty session * fix(tui): route the auto-attach tick to its run * fix(tui): defer auto-attach while another view owns the screen * fix(tui): keep bracketed-paste framing for attached sessions * fix(tools): let tty sessions run interactive commands * fix(tui): capture the auto-attach baseline before the exec starts |
||
|
|
d7ac85cbf9 |
Polish Zero's TUI and harden patch, model, and background workflows (#916)
* Checkpoint UI polish * Fix structured apply patch handling * Restore tool previews when resuming sessions * Improve theme-aware code rendering * Replace persistent sidebar with run details * Remove inert transcript interactions * Cover resumed edit previews * Smooth active run status feedback * Preserve semantic colors behind overlays * Normalize ChatGPT model aliases * Render staged image attachments in terminal * Polish transcript and tool feedback * Resolve rebased diff renderer conflict * Address UI polish review findings * Fix smoke test regressions * Harden patch and transcript edge cases * Bind structured patches to workspace root * Use unique structured patch temp files * Retry structured patch temp allocation * Reuse rooted patch temp allocator * Fix Windows CI process checks * Fix structured patch safety boundaries Preserve read-before-write credit only when the model already knew the full source, reject ambiguous hunk matches, and retain CRLF line endings. Add regression coverage for unsafe overwrites, positive tracking cases, duplicate matches, and Windows-style files. * Resolve plan test seam after rebase Drop the obsolete sticky-plan height helper while retaining the PR status test seams brought forward by the UI changes. * Address final review correctness gaps Bound narrow waiting labels, reject malformed plan records, preserve missing final newlines, reject out-of-order EOF hunks, clear stale tracking after failed rollback, and keep deferred tool loading aligned with model visibility. * Harden patch transactions and review edges Publish new patch targets without overwriting raced destinations, journal only completed mutations, and create replacement temps beside their targets. Reject pre-plan diagnostics, cover tool-search transcript suppression, and leave sufficient cleanup time in the Windows process fixture. * Bind patch rollback to committed versions Capture file identity and content before publication, quarantine rollback targets atomically, and refuse destructive undo when a path was replaced or modified in place. Add deterministic coverage for publication and rollback races. * Align structured patch failure semantics Use prevalidated sequential application with explicit partial-success errors instead of unsafe multi-file rollback. Retain atomic per-file publication, no-overwrite creates, unsupported-hardlink fallback, and conservative tracker invalidation. * Preserve patch context bytes Track source offsets for structured-patch context lines and reuse the exact matched source text during replacement. This keeps tolerant matching from changing indentation or mixed line endings outside the requested edit. |
||
|
|
04fd3c01a7 |
Improve diff viewer rendering (#902)
* Improve diff viewer rendering * Address diff viewer review findings * Fix diff viewer interaction regressions * Preserve header-like diff content * Fix diff hunk parsing and image vulnerability * Strengthen diff blank-context coverage * Fix diff rendering edge cases |
||
|
|
cae0269e77 |
fix(tui): a copied NUL byte no longer panics the TUI on Windows (#876)
syscall.StringToUTF16 PANICS rather than returning an error when the string contains a NUL — documented standard-library behaviour — and github.com/atotto/clipboard's Windows path calls it directly. So one NUL anywhere in a copied transcript selection killed the whole program: "zero: tui error: program was killed: program experienced a panic", reported from a real Windows machine. The NUL reaches the clipboard because ansi.Strip removes escape SEQUENCES and leaves C0 bytes untouched, and the transcript carries them from two independent directions: tool output (a binary read, git's -z NUL-separated listings — which this repo itself parses in files_git_sweep.go) and Zero's own card protocol, whose row prefixes are literally "\x00command-card\x00" and "\x00plan-card\x00". Sanitised at the WRITE rather than at each source, because the sources are unbounded: any tool result a user can select from is one, and a fix per source is a fix the next tool re-breaks. The same pass protects the OSC52 fallback, where a stray control byte terminates the escape sequence early and silently corrupts the copy instead of crashing — which is likely why this went unnoticed off Windows, where the identical NUL produces a bad copy rather than a dead process. \n and \t survive: a multi-line selection is the normal case and both are legal clipboard content. Fixes #875 |
||
|
|
588635487f |
feat(tui): replace /retitle with local /rename (#826)
* Improve session naming and resume picker * Address session rename review feedback |
||
|
|
583e6530bd |
fix(lint): resolve static analysis baseline (#769)
* fix(lint): resolve static analysis baseline * fix(lint): clear linux staticcheck findings Amp-Thread-ID: https://ampcode.com/threads/T-019fa018-b2ee-7442-8a31-80b2cf9dd270 Co-authored-by: Pierre Bruno <[email protected]> --------- Co-authored-by: Amp <[email protected]> |
||
|
|
97ef90d27a |
chore: remove code unreachable on every supported platform (#706)
* chore: remove code unreachable on every supported platform The Security and code health job's advisory deadcode step reports 152 unreachable functions on main. Running the same check under GOOS=linux, windows, and darwin shows 13 of those are false positives whose callers live in windows build-tagged files, so only functions unreachable on all three platforms are touched. Functions with no references anywhere are deleted, including the internal/reasoning package, which nothing imports. Functions that only tests reference move into per-package export_test.go files so tests keep their seams while the production binary sheds the code. Test call sites of deleted convenience wrappers are ported to the live APIs (scoped tool constructors, tools.DeferredSource, FinishReason checks). Kept despite the report: cron.Store.Runs (only reader of runs.jsonl, which production writes), and agenteval.TraceEventKeys and MissingTraceEvents (used across packages by the trace contract test). * docs(config): correct ValidateFile's errors.As claim Message is a flattened string (.Error() already called on the parse error), not a wrapped error, so callers can't recover the concrete json error type via errors.As. Fix the comment to match. Co-Authored-By: Claude Sonnet 5 <[email protected]> * chore(trace,skills): drop obsolete Sink adapters and fix stale doc references The dead-code pass removed the Emit methods but left Sink, NDJSONSink, and TextSink exported with a contract nothing implements; remove the types with their contract. Also remove the stray blank line at EOF of trace.go flagged by gofmt, and update skills package docs and HOW_ZERO_WORKS.md to stop directing callers to DiscoveryRoots/Duplicates, which are now test seams. Co-Authored-By: Claude Fable 5 <[email protected]> * chore: clean up remaining review nits on dead code removal Replace a pointless fmt.Errorf(...%w...).Error() with fmt.Sprintf using %v, since the wrapped error was discarded immediately anyway. Remove a comment block that was duplicated verbatim. * docs(skills): drop remaining references to test-only List/Get Load and MergedSkills comments still named List/Get as production entry points after they moved to export_test.go. Point callers at Load and ListFromRoots instead. * chore: remove now-unreferenced export_test.go seams execSessionManager.len() and likelySandboxDenied/sandboxDenialKind had no remaining callers after the sandbox unify-command-execution rebase; the production code they wrapped for tests no longer exists. --------- Co-authored-by: Claude Sonnet 5 <[email protected]> |
||
|
|
3967d49d64 |
feat(tui): permission prompt takes free-text feedback inline (#780)
* feat(tui): let the permission prompt take free-text feedback inline "No, and tell Zero what to do differently" now opens an inline input on the permission card instead of silently cancelling: type an instruction, Enter sends it, Esc returns to the option list. The text is delivered as a Deny decision whose Reason is the typed instruction, so the agent surfaces it via deniedPermissionResult as the tool result and the model adjusts in the SAME turn — where the old cancel returned an error and aborted the run. Empty input + Enter falls back to a plain cancel (the prior meaning). No agent-side change: PermissionDecision.Reason already carries free text to the model. The UI mirrors the ask_user "type your own answer" pattern — the feedback field reuses the shared composer input and the card renders it in the transcript body. Also: a focused permission prompt now suppresses the composer (and idle hints/plan panel), keeping only the status line, exactly like the ask_user modal. Its keys are inert anyway, so a "describe a task for zero…" box below the card was misleading — and in feedback mode the shared input would otherwise echo in two places. Tests cover open / submit-as-Deny-with-text / empty-cancels / Esc-returns, the composer suppression in both modes, and that the feedback text renders exactly once. * fix(tui): preserve the composer draft across permission feedback (#780 review) anand + CodeRabbit (Major): entering feedback mode cleared the shared composer input via SetValue(""), and neither submit nor cancel restored it — so a user with a half-typed or queued next-turn message who chose "tell Zero what to do differently" lost that draft. Snapshot the composer value into pendingPermissionPrompt.savedDraft when feedback mode opens, and restore it on BOTH exits (submit and Esc-cancel). Only the feedback text is consumed, delivered via the decision Reason. Adds regression tests for both paths; mutation-verified against the SetValue("") regression. * fix(tui): guard permission-feedback backspace and clicks (#780 review) Vasanthdev2004 review, all on the permission path: 1. Real bug: Backspace on an empty feedback field dropped a staged attachment. The keyBackspace case runs before the typing branch and, on an empty composer (feedback mode clears it), fell to removeLastAttachment — silently dropping a staged image/doc that savedDraft does not restore. Route Backspace to the shared input while typing so it is a plain edit of the feedback text. 2. Defence-in-depth: the mouse click resolver in handleTranscriptSelectionMouse resolved a clicked permission option (Allow included) with no typing check. It is safe today only because renderFocusedPermissionPrompt returns nil clickable offsets in feedback mode — a single early-return in a function other PRs also edit. Add an explicit !typing guard on the click path so the safety is stated, not emergent. 3. Test hardening (also raised by CodeRabbit): assert len(got) == 1 before indexing got[0] in the draft-preservation test, so a regression fails cleanly instead of panicking. Tests: TestPermissionFeedbackBackspaceKeepsStagedAttachment (mutation-verified against the missing guard) and TestPermissionFeedbackRendersNoClickableOptionsWhileTyping. --------- Co-authored-by: KRATOS <[email protected]> |
||
|
|
d74ceb1127 |
fix(tui): cache settled alt-screen transcript (#647)
* fix(tui): cache settled alt-screen transcript * fix(tui): invalidate settled alt-screen cache on in-place status row updates setDoctorStatusRow, setSandboxSetupStatusRow, and setCompactStatusRow all mutate an already-flushed transcript row in place (m.transcript[i] = row) without invalidating the alt-screen settled cache. Since none of frontier, width, or row count change on such an update, settleTranscript's rebuild guard never fires and the View fast path serves the stale cached snapshot - e.g. the doctor connectivity spinner freezes on its first frame. Zero altScreenSettledWidth on the in-place path, mirroring the existing pattern in toggleTranscriptRow, so the next settle rebuilds the cache. Addresses PR #647 review feedback from Vasanthdev2004. * fix(tui): invalidate settled transcript cache * fix(tui): invalidate settled file selection cards |
||
|
|
30bff28efc |
Fix/detailed view scroll select (#498)
* fix(tui): strip all ANSI escapes from copied transcript text renderAssistantMarkdownText embeds bold markers for prose and ANSI color sequences from chroma for fenced code blocks. stripMarkdownRenderControls only stripped the bold markers, leaking ANSI codes into clipboard text. Switch to ansi.Strip() to remove all escape sequences. * refactor(tui): extract rendering dispatch and add detailed bool to transcriptBodyItems Extract rowRenderFn and transcriptRowDispatchFn types and refactor rendering methods into *Fn variants parameterized by the render function. Add detailed bool parameter to transcriptBodyItems to prepare for the detailed transcript viewport. All callers pass false so there is no behavior change yet. Extract transcriptRowBodyHeightCacheKeyOpts from transcriptRowBodyHeightCacheKey so the body cap can be configured per caller. * feat(tui): scrollable detailed transcript viewport with sidebar suppression Enable keyboard scrolling (PgUp/PgDown/Up/Down) in detailed transcript mode by routing through scrollChat instead of swallowing the keys. Rewrite detailedTranscriptView to use the transcriptBodyItems pipeline so the body renders through the same viewport/scroll engine as the live view. Implement detailedTranscriptFooter with copy-status and jump-to-bottom hint integration. Suppress the sidebar in detailed mode so the full chat width is available for reading uncapped tool output. * test(tui): detailed transcript scroll with PgUp/Down and Up/Down arrows TestPgUpDownScrollsDetailedTranscript verifies that PgUp scrolls toward older content and PgDown returns to the bottom in detailed mode. TestUpDownArrowsScrollDetailedTranscript verifies that arrow keys scroll one line at a time and bottom is clamped at offset 0. * test(tui): ANSI strip verification for markdown rendering controls and selectable text TestStripMarkdownRenderControlsStripsAllANSI verifies bold markers, color sequences, and combined sequences are stripped. TestSelectedTranscriptTextStripsANSIFromHighlightedCode verifies that chroma-highlighted code blocks produce clean plain text in the selectable layer. * fix for coderabit, and better fewer code. * fix(tui): address PR review feedback dead code, hover clear, and rebindable key label * format * addressing the review issue 1. the ctrl + b in detailed mode, added the guard to not be able to do it in detailed mode so it affect its state in the normal mode. 2. added a test coverage for keyDown after `keDown then keyUp` so it covers that. * fix(tui): close file drill-in when entering detailed transcript toggleDetailedTranscript now calls exitFileView before returning, so an active file drill-in is automatically closed when the user enters detailed transcript mode. * test(tui): verify file drill-in closes on detailed toggle Adds TestDetailedTranscriptClosesFileView and TestDetailedTranscriptStaysClosedOnSecondToggle to guard against the file-view bypass bug. * fix(tui): block composer hit-test while detailed transcript is active composerMouseSelectionBlocked and mouseOverComposer now check m.transcriptDetailed so mouse clicks and wheel events in the composer area reach the transcript body instead of being silently consumed. * test(tui): verify composer mouse is ignored in detailed mode Adds TestComposerMouseSelectionBlockedInDetailedMode to guard against composer hit-test swallowing mouse events during detailed transcript viewing. |
||
|
|
afb5010a46 |
polish v2: smoothness & delight (#484)
* feat(tui): actionable hints on provider/model errors Classify a failed turn's provider error into auth / rate-limit / connectivity / model-not-found / context-overflow and render a one-line next step below the red error row, instead of dumping an identical raw blob for every failure mode. The classifier lives in a new internal/errhint package so both the TUI error row and the CLI exec path can share it (TUIHint references slash commands, CLIHint references zero subcommands). Classification is a conservative string heuristic keyed off providerio.ClassifiedError's prefixes plus lower-level DNS/TLS/timeout/context-length signatures, since the numeric HTTP status is gone by the time the error reaches a UI surface. * feat(tools): budget bash output with head+tail truncation bash was the one tool that returned command output with no byte cap — 'cat large.log' or a verbose test run could dump megabytes into context and force compaction, while every other read/search tool already applies a budget. Cap stdout and stderr at 96KB each, keeping the head and tail of an oversized stream (build/test failures usually land at the tail) and dropping the middle behind a marker that points at redirect-to-file + read_file. Record raw_bytes/emitted_bytes/estimated_tokens/truncated in Meta like the other tools. Sandbox-denial and shell-issue detection still run on the full raw output before budgeting. * fix(gemini): retry 401 with refreshed OAuth token The Gemini provider used providerio.SendWithRetry (429/503/529 only), so an expired OAuth token surfaced as a terminal 'API key not valid' auth error while OpenAI and Anthropic silently recovered via SendWithAuthRetry's force-refresh-and-replay on 401. Plumb OAuthResolver through gemini.Options/Provider and the factory (exactly as the other two providers already do) and switch the stream send to SendWithAuthRetry. With a nil resolver (API-key users) behavior is unchanged. * fix(errhint): gate classification on provider-origin marker Address CodeRabbit review on #1: agentResponseMsg.err can also carry local failures (a tool's 'permission denied', a 'file does not exist', a config error), and broad substrings would have attached a bogus /provider or /model hint to those. Classify now returns Unknown unless the message carries a provider marker the provider layer always attaches (auth error: / rate limit error: / provider error: / provider request error: / provider stream error:). Local errors never draw a provider hint; provider errors sub-classify as before. Added local-failure test cases proving they stay Unknown. * feat(cli): actionable hint on text-mode exec provider errors zero exec printed provider failures as a bare '[zero] <raw error>' with no next step. Append a one-line hint (reusing the shared errhint classifier) for recognized provider failures — 'run `zero auth`', 'run `zero doctor`', etc. The provider-origin gate means non-provider codes (sandbox_error, mcp_error) and JSON/stream-json output are untouched. * fix(tools): set Result.Truncated for budgeted bash output Address CodeRabbit review on #3: budgetBashOutput recorded truncation in meta["truncated"] but never set the Result.Truncated struct field, so consumers reading the struct (like glob/web_fetch/read_minified do) would miss truncated bash results. Return the bool from budgetBashOutput and set Result.Truncated on every return path. * perf(tui): reuse one LSP manager across prompts runAgentWithOptions built a fresh lsp.NewManager per run and shut it down when the run returned, so gopls (and every other language server) cold-started on the first edit of every turn — 200ms-2s of latency the manager's own design (long-lived, reused servers) exists to avoid. Build one manager per session in newModel (cheap: servers start lazily on first Check) and reuse it across runs; the SelfCorrector still gets a fresh checker wrapping the shared manager each run. Torn down with a short deadline in quit(). Runs fall back to a per-run manager when the session manager is absent (cwd unknown, or a directly-built test model). * perf(tui): accumulate streamingText as []byte (O(1) append) m.streamingText += msg.delta was O(len) per delta -> O(n²) across a long generation (a 10k-token code gen allocates hundreds of MB of intermediate strings and the UI gets progressively laggier). Accumulate into a []byte with append (O(1) amortized) and read via streamingTextString(). A []byte rather than strings.Builder because the TUI model is copied by value on every Update, which would trip strings.Builder's copy check; []byte is nil-safe and copies cleanly like the other slice fields. * perf(tui): coalesce streamed text deltas to one frame Every OnText delta was its own tea.Msg, so a fast provider (100+ tok/s) drove 100+ full Update->View cycles per second, each re-parsing the growing markdown — visible stutter over SSH and wasted CPU. Batch agentTextMsg deltas at the runtime sink over a ~16ms frame and forward them as a single message, decoupling render rate from token rate. Any non-text message flushes pending text first so ordering with tool-call / reasoning / row messages is preserved; the final agentResponseMsg (a tea.Cmd return, not a sink message) is safe because the model already drops deltas for an inactive runID. * fix(agent): raise tool-failure stop threshold 4->6 guardrails.go halted a run after 4 consecutive same-error tool failures. A corrective hint fires at 2, and a model iterating on a genuinely tricky edit can legitimately fail a couple more times after the hint while converging — stopping at 4 cut those runs short. Raise to 6; the streak still resets the instant the tool succeeds or hits a different error, so only true same-error loops are affected. * fix(agent): more reconnect retries with jitter; narrow to transport errors #19: bump maxStreamReconnects 2->4 and add up-to-50% jitter on the exponential backoff (capped at 8s), so a multi-second network blip is ridden out instead of killing the run on a 2s hiccup, and concurrent runs (swarms, cron fleets) don't reconnect in lockstep. #34: drop the '502'/'503' substring matches from shouldReconnect. 503 already exhausted providerio.SendWithRetry (retrying here is a redundant double-retry) and 502 is non-idempotent by providerio's rule (the POST may have been processed). Only genuine transport failures — where no response was received — reconnect now. * fix(agent): reduce stall retries 2->1 to bound stuck-session time A no-output stream stall is detected only after the full stream idle timeout (~5min) elapses, and each retry can idle again — so 2 retries left an interactive session frozen for ~15min. Drop to 1 retry: keeps the common single-hiccup recovery while bounding the worst case to ~2x the idle timeout. * fix(tui): serialize coalescer forwarding to preserve message order Address CodeRabbit review on #7: flush() and the run-switch path drained the text buffer under c.mu but called forward() after releasing it, so a timer-fired text flush could race a concurrent non-text send() and land after the tool-call/reasoning message it should precede. Hold c.mu across drain AND forward (drainAndForwardLocked), so whoever holds the lock delivers atomically and the other caller blocks until it is done — text can never overtake a following non-text message. Added a concurrency stress test (timer racing an inline send) that passes under -race. * feat(tui): accept /compact now Bare /compact already triggers compaction, but /compact now — what users reach for when the context gauge climbs — hit the usage-error branch. Accept the 'now' keyword and advertise it in the command usage. * feat(tui): add /retry, /edit, /copy, /export commands /retry resends the last prompt; /edit recalls it into the composer to tweak and resend — both read a new lastPrompt field captured verbatim (pre-expansion) in launchPrompt. /copy puts the last answer on the clipboard via the existing copy machinery; /export writes a plain role-prefixed transcript to a file (timestamped default, or a given path). Real gaps for SSH users with no mouse select. * feat(tools): separator-insensitive tool_search matching tool_search ranked deferred tools by exact substring, so a model that typed 'webfetch' (dropping the underscore) matched nothing and looped. Add a separator-squashing fallback ('web_fetch' -> 'webfetch') scored below an exact substring match, so precise queries still rank first. * feat(tools): signal ask_user dismissal distinctly from a blank field Empty answers all rendered as '(no answer provided)', so the model couldn't tell a wholesale dismissal (user closed the prompt without answering anything) from one field left blank amid real answers — and might invent a default. FormatAskUserAnswers now flags a full dismissal up front as a skip and marks individual empties '(left blank)' vs '(skipped)'. * feat(tui): fuzzy ranking in model/session pickers The pickers filtered by flat strings.Contains with no ranking, so finding 'sonnet 4.5' among 50+ models meant an exact prefix or scrolling. Rank matches (exact < prefix < contains < subsequence, reusing fuzzySubsequenceGap) so the closest match lands on top, and 'snt45' now matches 'Sonnet 4.5'. Groups stay contiguous — ordered by their best match, never split — so the grouped model picker still renders one header per provider. Covers both /model and /resume (shared commandPicker). * perf(agent): cache per-tool schema render across turns partitionTools re-ran the recursive schema->map conversion (schemaToRuntimeMap) for every tool on every turn — redundant, since a tool's advertised name/description/schema is stable for the run. Add a per-run definition cache keyed by tool name (nil-safe; the plain partitionTools entrypoint and tests pass nil for fresh renders). The partitioning itself (visibility, deferral, ordering) still recomputes every turn — it must, because a tool's deferred state can flip mid-run (swarm tools un-defer once a swarm is active). Only the expensive schema render is memoized. tool_search is excluded by its callers (dynamic description) so it never poisons the cache. * perf(agent): calibrate compaction estimate against real prompt tokens The byte/4 heuristic (ApproxTextTokens) over-counts code-heavy content ~15-20%, so with triggerRatio=0.7 compaction fired at ~60% of true capacity — premature summarizer calls that degrade quality. Each turn now folds (rawEstimate, provider InputTokens) into an EMA calibration ratio, clamped per-sample to a sane band; maybeCompact and the reactive path scale their estimates by it. Turn 1 uses the raw estimate (no data yet); later turns compact near real capacity. * style: gofmt the new agent test files The CI gofmt check flagged the #22/#25 test files (struct-field and switch alignment). No behavior change. * feat(agent): turn-based plan staleness reminder The stale-plan nudge only fired after 10 tool calls since the last update_plan, so a plan that drifts stale across many low-tool-call turns (one call per turn) took many turns to catch — the 'plan set turn 1 stays pending forever' case. Add a turn-count complement: after stalePlanTurnThreshold (8) turns without an update AND with items still pending, fire the same one-shot reminder. Gated on pending items so a fully-completed plan is never nagged; the existing tool-call trigger is unchanged. (The roadmap's fuzzy 'tool calls unrelated to plan items' relevance match is deliberately omitted — too false-positive-prone.) * feat(agent): preserve recent-edits summary across compaction Compaction preserved the plan, loaded skills, tool schemas, and project instructions, but not file edits — so after the editing turns were elided the model knew it had changed a file only vaguely, and would re-read to rediscover its own footprint. Extract write_file/edit_file targets from the elided middle with a one-line note from each tool result, merge them across repeated compactions (newer note per path wins), and carry them in the preserved-state JSON (capped at 20 paths / 160 bytes per note). * feat(tui): workspace-scoped /resume + fewer event reads /resume listed sessions from every project globally, and read the full event file for each of them to test for resumable content. Filter the picker (and /resume latest) by the current workspace Cwd, checked BEFORE the per-session read so a large global history no longer pays N full file reads to build one workspace's list; also skip zero-event sessions via metadata without any read. Sessions with no recorded Cwd (older runs) and an unknown current workspace stay visible so nothing is hidden that can't be confidently placed. Explicit /resume <id> still resolves any session. * feat(tui): warn on model switch that drops staged images Staged images were silently dropped at submit when the active model had no vision support — the user only learned after sending. Chips already render above the composer (renderAttachmentChips); add an immediate amber warning line to the /model switch status (both same-provider and cross-provider paths) when images are staged and the newly selected model can't accept them, so the drop is surfaced at switch time. * feat(tui): inline recovery affordance on setup errors A first-run setup error rendered as a bare red 'error: <text>' with no pointer to the fix. Append a faint, stage-tailored recovery line (edit the endpoint/name/key above then Enter, or pick another model) that names only keys the stage's footer confirms, so the guidance is always accurate. Empty for stages whose footer already makes recovery obvious. * fix: address CodeRabbit review (5 findings) - loop.go: recompute the compaction calibration estimate at calibration time, not request-build time, so a reactive-compaction reissue that shrinks the request feeds the EMA the sample that actually completed. - errhint: match bare HTTP status codes (401/403/429/529) only at digit boundaries, so an incidental number ('4290ms', 'id 14015') isn't mis-bucketed as auth/rate-limit. - bash: drop the stray unmatched ']' in the output-truncation marker. - tui /retry: apply the same exiting/compactInFlight guards commandPrompt has, so a retry can't race compactResultMsg's rewrite of session state. - tui /export: write the transcript 0o600 (it may contain echoed secrets), not world/group-readable 0o644. * test(tui): skip export file-mode assertion on Windows os.WriteFile ignores Unix permission bits on Windows and Stat reports 0666, so the 0o600 assertion failed there. The production 0o600 is honored on Linux/macOS (where it matters); guard the check with runtime.GOOS so Windows smoke passes. * fix(tui): align /resume latest filters + case-insensitive workspace match on Windows Address CodeRabbit review on #21: - latestResumableInWorkspace now applies the SAME filters as the picker (skip zero-event metadata and empty/failed runs), so /resume latest can't land on a session the picker intentionally hides. - sessionMatchesWorkspace compares paths case-insensitively on Windows (its filesystem is), so the same workspace spelled with different casing no longer hides resumable sessions; other platforms stay case-sensitive. * fix(tui): /retry resends the last prompt's image/PDF attachments /retry relaunched only lastPrompt's text, but launchPrompt clears the pending image/document queues once a turn is sent. A vision- or PDF-backed prompt that failed would silently retry as text-only and answer a different task. Snapshot the consumed attachments at launch and re-stage them on /retry so launchPrompt rebuilds an identical request (document preamble, images, and the submit-time vision re-check). startNewSession drops the snapshot so a post-/new /retry cannot leak the previous session's attachments. Also correct the OAuthResolver factory comment, which omitted the Gemini provider it is now passed to. * fix: address jatmn review (reconnect 5xx, /edit attachments, edit-cap, coalescer test) - reconnect: exclude HTTP 5xx (500/502/503/504) before the transport substring match so "504 Gateway Timeout" no longer slips through on the generic "timeout" needle. A gateway timeout is non-idempotent — the completion POST may already have reached the model — so replaying the connect risked duplicate billable work. Reuses a digit-boundary status matcher exported from errhint (HasStatusCode). Test asserts 504/500 stay non-reconnectable. - tui /edit: re-stage the remembered image/PDF snapshot alongside the recalled prompt text, mirroring the /retry fix — editing a vision- or document-backed prompt no longer silently resends a text-only version. - compaction recent-edits: order edits by LAST edit, not first, and move a re-touched path to the newest position on merge, so the maxRecentEdits tail cap keeps the file the model most recently edited instead of dropping it. Adds mergeRecentEdits (edit-specific) and a capped-list regression. - coalescer test: inject the frame timer via afterFunc so TestCoalescerBatchesDeltas proves buffering deterministically instead of racing the real 16ms wall-clock timer. * fix(tools): bound bash output capture in memory, not just model-visible text Previously stdout/stderr streamed into unbounded bytes.Buffers and were only truncated to the 96 KiB budget AFTER the command finished, so a runaway command (cat huge.log, yes) could grow Zero's memory until it stalled or OOMed before budgetBashOutput ever ran. Replace the capture with boundedBuffer, an io.Writer that keeps only the head+tail each stream will surface and discards the middle as it arrives, while counting the true total for the truncation marker. Memory is now bounded to ~head+2×tail per stream regardless of output size. truncateHeadTail is refactored onto a total-aware core so the string path (and its tests) are unchanged, and budgetBashCapture reports the true raw_bytes even though only a bounded slice was held. Adds boundedBuffer/capture unit tests and an end-to-end test that streams ~5× the budget and asserts bounded emission with an accurate raw_bytes. |
||
|
|
142c548c89 |
feat(tui): FILES sidebar panel with click-to-select and file drill-in (#365)
* feat(tui): FILES sidebar panel with click-to-select and file drill-in
Add a FILES section to the right context sidebar showing the files the
session has touched — newest first, with A/M status badges, aggregated
+/− diffstats, and a pulsing row for the file being written right now.
Clicking a row selects the file (accent marker, edit cards tint in the
chat, transcript scrolls to the latest edit); a second click opens a
drill-in view: the chat column swaps to the file's stacked edit cards,
`f` toggles to the syntax-highlighted on-disk file with markers on
session-added lines, `d` back to diff, Esc returns to the chat.
The roster is fed from two sources: changedFiles carried by
write_file/edit_file/apply_patch results (persisted per session, so it
survives /resume), and a git sweep — baseline snapshot at startup,
re-check after command tools and at turn end — so files created through
bash/exec_command scaffolding or subagents show up too.
* fix(tui): address FILES sidebar review findings
Review fixes for the FILES panel / drill-in / git sweep:
- touchedFiles: split multi-file diffs per file (perFileDiffStats) so an
apply_patch spanning several files no longer charges the whole patch's
+/- totals to every file it touched; order by lastRowIndex descending
so a re-touched file lists first (reversing first-seen order kept it in
its original slot).
- sidebarFileLines: filter the live-writing row out before computing the
"+N more" trailer so the skipped entry no longer inflates the count.
- git sweep: run `git diff --numstat -z` and parse the NUL-terminated
rename records (counts + preimage + postimage) — plain --numstat
brace-mangles renamed paths ("src/{old => new}"), so their diffstat
never matched a porcelain path and renamed files stayed at 0/0;
gitTouchedFiles now yields newest detection first to match the
roster's ordering.
- file view: stream the full-mode read and stop at fileViewMaxLines
(os.ReadFile loaded a multi-GB file wholesale before truncating — the
exact freeze the cap exists to prevent); re-opening the file already
being viewed is a no-op so a stray re-click can't bounce full mode
back to diff or reset scroll; skip sub-4-char lines in the changed-line
markers so common braces/returns don't mark unrelated occurrences.
- keys: gate the drill-in's d/f and Esc handling on noBlockingModal so a
permission prompt / ask-user / wizard keeps its own key handling.
- sidebar: a live in-flight write counts as content, so the FILES pulse
shows for the session's first mutation.
- tests: numstat test now feeds real -z record shapes and asserts the
rename postimage key; new coverage for per-file stats, re-touch
ordering, live-row overflow, same-file reopen, truncation, modal
gating, live-write content, and a real-repo rename sweep.
---------
Co-authored-by: Gnanam <[email protected]>
|
||
|
|
1c65f59be6 |
fix(tui): subchat mouse selection, hover highlighting, and drag-to-edge auto-scroll (#350)
* fix(local): subchat mouse selection + hidden plan panel + scroll-extend selection Three bugs in the subagent/swarm drill-in (subchat) view and mouse text selection, all from the same root: newer features (subchat's row-source swap, wheel-scroll) were never integrated into the older selection/footer code. 1) Can't copy anything in the subchat view. transcriptLineAtMouse (and selectedTranscriptText, the copy-time text extraction) always hit-tested against m.transcript (the parent), never m.subchat.childRows, even while the screen was showing the child session. New transcriptHitTestSource() mirrors transcriptView's own branching (nav bar + child rows + full width vs pinned title bar + parent rows + sidebar-aware width) and is now the single source both functions use. 2) Plan panel shown above the composer inside the subchat view. footerView unconditionally rendered the PARENT run's pinned plan panel; only pendingAskUser was gated. Added a !m.subchat.active guard (m.plan belongs to the parent run, not whatever subagent/swarm session is being viewed). 3) Selection didn't extend across a scroll (reported as "stopped at the end of the screen"). transcriptSelection.cursor was only ever updated by a mouseMotion event; a wheel event is a distinct message type that never reached that code, so scrolling while selecting left the selection frozen at whatever line the mouse last physically moved over. New scrollChatExtendingSelection scrolls AND re-evaluates the cursor at the mouse's current position against the post-scroll viewport. Uses a new nearestTranscriptLineAtMouse (falls back to the closest visible selectable line) rather than an exact match, since the recomputed position can land exactly on a blank spacer row between messages after an odd-length scroll shifts text/spacer parity - an exact-match miss there must not silently no-op the extension. Tests: subchat selection resolves the CHILD session's text, not the parent's; the plan panel is absent from the footer during subchat; a selection's cursor bodyY moves (extends) after a wheel-scroll instead of staying pinned. Full gate green (73 pkg, -race). * feat(local): highlight clickable rows on mouse hover Moving the cursor over an interactive row now highlights it (accent underline) before you click, across every clickable surface: - specialist cards and collapse/expand toggle headers in the transcript - AGENTS sidebar rows (swarm members) - PLAN sidebar step rows - permission-prompt options (reuses the EXISTING keyboard-cursor highlight by moving pendingPermission.cursor on hover, instead of new styling) Requires switching the mouse-tracking mode from CellMotion (only reports motion while a button is held) to AllMotion (reports idle cursor movement too) - the existing 15ms mouse-event throttle already covers the added motion events. hover.go: hoverTarget + mouseHover predicate + updateHoverTarget, which reuses the SAME hit-testers and priority order as the existing click handlers so hover and click never disagree about what's clickable. Sidebar hover is resolved by STABLE IDENTITY (sessionID / stepIndex), not a cached line offset: hoveredSidebarLineOffset re-matches the identity against a freshly computed hit list on every render, so a row that's disappeared (a swarm member's linger elapsed, a plan step completed) simply doesn't highlight rather than a coincidentally-matching unrelated row lighting up. Transcript hover stays bodyY-keyed (matching the existing selection-highlight convention) with explicit clearHover() calls wherever bodyY numbering can shift without an intervening mouse motion: wheel-scroll, PgUp/PgDn, window resize, and transcript rebuilds (/clear, /resume, /rewind, /compact via resetFlushFrontier). Adversarial review caught two real bugs before ship, both fixed: - BLOCKER: plan-step hover stored the step's own index (0,1,2...) but the sidebar render treated it as a raw line offset into the rendered rows - every plan-step hover highlighted the wrong row (the AGENTS header/body). Fixed by resolving fresh at render time via the stable-identity redesign above, which also closes the related sidebar-drift issue. - MAJOR: hover wasn't cleared on keyboard PgUp/PgDn scroll. Tests: predicate correctness; hover resolves the right target across all 4 surfaces (specialist card, toggle row, sidebar agent, plan step, permission option) with the correct priority order; hover changes the actual render output (transcript + sidebar); hover clears on wheel-scroll and subchat exit; a regression test proving hoveredSidebarLineOffset self-heals (no highlight) when a hovered row's identity no longer resolves - the exact scenario the blocker fix targets. Full gate green (73 pkg, -race). * fix(local): soften hover color + auto-scroll a drag past the transcript edge 1) Hover highlight was brand-lime (bright, glaring against the near-black surface) and re-paints on every mouse movement. Switched to amber (an existing semantic token, "light orange") and dropped the underline per feedback. 2) Dragging a text selection past the top/bottom edge of the visible transcript now auto-scrolls toward that side and extends the selection to follow, matching the browser/editor "drag past the viewport edge" pattern. transcriptEdgeScrollDelta detects the drag is vertically outside the body rect (but still within its horizontal span, so it isn't just "off to the side" over the sidebar); dragToEdgeScroll scrolls one step and anchors the cursor to whichever line now sits at that edge of the NEW viewport - deliberately not a re-resolve of the drag's physical mouse position, which stays outside the body rect after scrolling (scrolling changes what content occupies a screen row, not the mouse's screen position) and would keep finding nothing. Extracted nearestTranscriptSelectableAt as the shared "closest selectable line to a target bodyY" core, now used by both nearestTranscriptLineAtMouse (target derived from a mouse position that may have landed on a spacer row) and dragToEdgeScroll (target derived directly from the viewport bound, no mouse position involved at all). Tests: dragging past the top edge scrolls toward older content and extends the cursor upward; the symmetric bottom-edge case scrolls toward newer content and extends downward. Full gate green (73 pkg, -race). * fix(local): smooth-glide the drag-to-edge auto-scroll instead of jumping The initial edge-scroll implementation (previous commit) applied a single chatWheelScrollLines (5-line) jump per raw mouse-motion event, which reads as sudden/jerky since motion events during a real drag arrive in bursts, and stops dead the instant the physical mouse holds still even while still parked past the edge (a terminal only reports motion on actual cursor movement). Replaced with a self-scheduling tea.Tick chain (dragEdgeScrollTickMsg, gated by edgeScrollSeq mirroring the existing exitConfirmSeq/copyStatusSeq idiom) that steps a small, fixed increment (dragEdgeScrollStep=1 line) on a short cadence (dragEdgeScrollInterval=70ms) for as long as the drag holds past the edge, independent of whether new raw motion events keep arriving. reducedMotion still gets the original single-bigger-step-per-event behavior (no animated glide). Adversarial review of the new self-perpetuating tick chain (the risk being an orphaned/stuck chain) found one real bug: a keypress mid-drag clears m.transcriptSelection.active directly (the KeyPressMsg handler in model.go) without going through stopEdgeScroll, leaving edgeScrollDelta stale. The in-flight tick itself terminates safely via its own liveness check - the bug was that the STALE nonzero edgeScrollDelta then fooled startEdgeScroll's "already running" fast path on the NEXT same-direction drag into silently doing nothing (no immediate step, no scheduled tick) for that whole hold. Fixed at the press site (transcript_selection.go's mouseLeftPress case): every fresh press now resets glide state via stopEdgeScroll, so a new drag always starts clean regardless of how the previous one ended - a single targeted fix rather than chasing every place transcriptSelection can be cleared. Tests: top/bottom edge glide starts and extends the cursor over several ticks (a single small step can land on a blank spacer row between messages, same as real usage); the chain stops when the drag returns to the body and a stale in-flight tick after that is a safe no-op; the keypress-interrupt regression (reproduces the adversarial review's finding, confirms the fix). Full gate green (73 pkg, -race). * fix(local): soften the brand-lime accent color across the whole UI The accent color (#caff3f) was fully saturated at fairly high lightness (HSL: L 62%, S 100%) - the kind of value that reads as neon/glaring everywhere it filled a block or bold-text surface, not just the one spot originally flagged: the composer's "typed" prompt glyph, the permission popup's focused-option badge, and the text-selection highlight all use it, plus the spinner, the brand chip, tool-card prompts, and more. Rather than patch each of those consumers individually, softened the single palette source (dark theme only; the light theme's accent is a separate, already-darkened value for its own contrast needs, untouched) to L 50% / S 80% (#ade619) - still clearly on-brand lime, and contrast against both black (onAccent text on accent fills) and the near-black panel stays far above WCAG AA (~14:1 and ~13:1, both comfortably over the 4.5:1 floor). Updated TestStreamingFadePaletteMonotonic's pinned RGB assertion (it checked the fade ramp's first bucket equals the literal old accent bytes) to the new values. Full gate green (73 pkg, -race; make lint/go vet clean except an unrelated stray file that isn't part of this branch or git history at all). * fix(local): darken the brand-lime accent further per live feedback Two prior passes (#ade619 at L50/S80, #8db620 at L42/S70) were each tested live against the original #caff3f via raw truecolor escape codes and confirmed too subtle in turn. Settled at L36/S62 (#759523) - a noticeably darker, more olive lime, confirmed against the original side by side. Contrast vs black (onAccent) and the near-black panel stays comfortably above WCAG AA (~6.1:1 and ~5.6:1). Updated the streaming-fade test's pinned RGB assertion to match. * fix: address PR #350 review comments and CI smoke test failures Smoke tests (macOS + Ubuntu): the 4 new edge-glide tests implicitly relied on m.reducedMotion being false, but mouseTestModel() inherits it from defaultReducedMotion(), which forces true whenever colorprofile.NoTTY is detected - true in CI's non-TTY test runners, false in a local interactive dev shell. Set reducedMotion = false explicitly in each of the 4 tests so they're deterministic regardless of environment. Verified by re-running locally under ZERO_REDUCED_MOTION=1 (reproduces the CI condition) - passes. CodeRabbit actionable comment: handleTranscriptSelectionMouse's mouseMotion case gated only on transcriptSelection.active, which stays true through the async copy-command grace window after release (a non-empty selection isn't reset until transcriptCopiedMsg lands) - so a genuine hover motion arriving in that window would be misrouted as a drag update. The suggested fix (gate on msg's own Button field) turned out to be wrong in a way only a full test run caught: TestTranscriptSelectionUpdatesOnGenericMotion is a pre-existing, deliberate test - some terminals don't restate the button on every motion event during a real drag, so trusting the per-event Button field breaks those terminals. Fixed properly instead: added a `dragging` field to transcriptSelectionState, tracking the app's own press/release bracket (true at press, false at release) independent of active and independent of any single event's Button field. mouseMotion now gates on dragging. CodeRabbit nitpicks: routed the two remaining raw `m.hover = hoverTarget{}` subchat-exit assignments through the existing clearHover() helper, so hover invalidation stays in one place. Left the third nitpick (hit-test layout recomputed per hover-motion tick) as a documented follow-up per the reviewer's own "trivial/poor tradeoff" framing - a proper per-frame memoized cache needs correct invalidation on scroll/resize/content-change to avoid reintroducing the exact staleness class of bug already fixed twice in this branch, and isn't worth rushing. New tests: TestTranscriptHoverAfterReleaseDoesNotMoveSelection (the actual bug CodeRabbit found - a post-release hover must not alter the just-copied selection). Full gate green (73 pkg, -race); TestTranscriptSelectionUpdatesOnGenericMotion (the test the first fix attempt broke) verified still passing. |
||
|
|
c0a0389b09 |
Polish TUI transcript rendering (#334)
* Polish transcript rendering Tighten transcript spacing, collapse noisy permission/tool rows, and render tool activity with cleaner action-oriented labels. Improve code and diff rendering with markdown highlighting, cleaner command output blocks, and grouped exploration summaries. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check * Fix grep exploration rendering on Windows Keep search query text out of path normalization so regex escapes render consistently across platforms. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check * Address transcript rendering review Cache highlighted streaming markdown so closed code blocks are not re-lexed on every live render. Tighten bare-code detection to avoid highlighting ordinary prose, restore manual permission audit rows, and make exploration details expandable. Also align diff content/header predicates and use display-width truncation for compact tool output. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: env GOCACHE=/tmp/zero-go-cache go test -race ./internal/tui Tested: env GOCACHE=/tmp/zero-go-cache go vet ./... Tested: git diff --check |
||
|
|
fb3277f46e |
feat(tui): tabbed ask_user questionnaire in the composer with suggested answers (#327)
* feat(tui): tabbed ask_user questionnaire rendered in the composer
Redesign the interactive ask_user prompt to match the requested mockup: when a
question is raised the composer text box becomes the questionnaire.
- Multi-question prompts render as a row of TABS (one per question + a trailing
Confirm tab). Tab / Shift+Tab switch questions, answers are given in any order,
and Enter on Confirm submits them all. A single-question prompt submits on
answer (no Confirm step) and shows no tab row.
- Each option can carry a one-line DESCRIPTION (contract: per-option
optionDescriptions, or option objects {label, description}); options render
numbered with the description dimmer underneath and the recommended one tagged.
- Per-question optional `header` gives each tab a short title (falls back to a
trimmed question).
- Rendered in the composer/footer region (footerView), replacing the text box
while active, instead of a scrolling transcript card.
- Keys: Tab move · ↑↓ select · Enter confirm · Esc dismiss; a printable key in
the picker drops into free-text; Esc from "type your own" returns to the picker;
multi-select questions are free-text with the options shown as suggestions.
- Headless unaffected (zero exec never wires OnAskUser).
Removed the old single-question sequential state (cursor/typing/index,
renderFocusedAskUserPrompt, resolveAskUser). Tests rewritten for the tabbed flow
(picker default, type-your-own, multi-question tabs + Confirm, Esc dismiss,
descriptions render). make build / go vet / go test ./... -race / make lint green.
* fix(tui): ask_user questionnaire renders on the black terminal canvas
It was painting a gray panel background (styledBlockFill with zeroTheme.panel +
onPanel-wrapped foregrounds). Since the questionnaire replaces the composer, it
should match it: bare foregrounds on the terminal canvas (black) with the
composer's lineStrong border and no fill. The lime cursor/badge highlights and
the recommended tag stay.
* fix(tui): address PR review — schema/parser alignment, single-question Tab
- ask_user schema: options.items is declared as string, so advertise only the
string form (descriptions go via the parallel optionDescriptions array). The
parser stays object-tolerant defensively, but the schema no longer tells
strict-schema providers to send objects it would reject.
- moveAskUserTab no-ops for single-question prompts (no tab strip / Confirm tab),
so Tab/Shift+Tab can't move the prompt into the hidden Confirm state. Test added.
|
||
|
|
95aaca6055 |
fix(tui): paint transcript selection once, not twice (#328)
Selecting text in the transcript lit up in two places: the real selection plus a copy shifted right by the reading-column gutter (e.g. selecting "help" also highlighted "today"). Root cause: assistant, user, and reasoning rows self-painted the selection highlight in their render functions using the UNSHIFTED textStart, but the selection points (anchor.x/cursor.x) are in the gutter-shifted screen coordinate the mouse maps to. So the self-paint landed gutter cells off, and finalizeTranscriptBodyRow then painted the highlight AGAIN at the correct shifted position — two highlights, one gutter apart. The rendered/toolResult/ specialist rows were already converted to paint-once-at-finalize; these three were left behind. Fix: stop self-painting in renderSelectableUserRow, renderSelectableAssistantRow, and renderSelectableReasoningBlock. They now return the canonical renderRow output (already line-aligned with their selectable spans) and let finalizeTranscriptBodyRow be the single painter, in the same shifted coordinate the mouse uses. Removes the now-dead renderTranscriptSelectableText / renderTranscriptSelectableMarkdownText helpers. Regression test TestTranscriptSelectionPaintsHighlightOnceNotTwice selects a sub-range of an assistant row and asserts the selection style is emitted exactly once; it fails on the pre-fix code (painted 3x, including the gutter-shifted copy). |
||
|
|
1d1fa7c514 |
feat(tui): ask_user suggested answers with a recommended default (#326)
* feat(tui): suggested answers with a recommended default for ask_user When the agent asks a clarifying question mid-task, it can now offer 2-4 suggested answers and mark one recommended; the interactive TUI renders a selector (arrow keys, lime cursor, "(recommended)" tag) with the cursor resting on the recommended option, plus a trailing "✎ Type my own answer…" entry that drops into the existing free-text composer. Whichever the user picks — a suggested option's text or their typed answer — flows through the same answer channel the agent loop already waits on. Optional and backward-compatible: a question with no options renders the plain free-text prompt exactly as before. Headless `zero exec` is unaffected (it never wires OnAskUser; the non-interactive best-assumption fallback stands). - contract: agent.AskUserQuestion / tools.AskUserQuestion gain Recommended; the tool schema advertises optional options + recommended; parser keeps recommended only when it resolves to one of the options (canonical text). - system prompt: tells the model it MAY supply options + a recommended one. - TUI: pendingAskUserPrompt gains cursor/typing; ask_user_prompt.go holds the selector logic mirroring permission_prompt.go; renderFocusedAskUserPrompt renders the picker or the free-text prompt. Tests: selector defaults to recommended and returns its text; "type my own" falls back to free-text and returns the typed text; no-options stays plain free-text. make build / go vet / go test ./... -race / make lint green. * fix(tui): ask_user — single input, Esc steps back from "type my own" Two UX fixes from interactive testing: - No double display: the focused ask card no longer echoes the in-progress free-text answer (it was shown both inside the card AND in the composer text box). The composer is now the single input; the card just shows the question and a hint. renderFocusedAskUserPrompt no longer takes the input string. - Esc from "type my own" returns to the selector for that question instead of cancelling the whole questionnaire (escapeAskUser); a question with no options, or the selector itself, still cancels on Esc as before. Tests: type-my-own asserts the card does not echo the typed text and the hint flips to "type your own answer"; new test covers Esc returning to the selector (no cancel, no answer delivered) then selecting a suggested option. * fix(tui): ask_user review fixes — multi-select, selector typing, dead code Address the three confirmed findings from the code review: - multi-select no longer silently narrowed to one option: a single-pick selector cannot represent MultiSelect, so a multi-select question now renders as free-text with its options surfaced as suggestions; the user types one or more and the answer is returned verbatim (syncQuestionState; escapeAskUser and the renderer updated to match). - typing in selector mode no longer discarded: a printable keystroke while the selector is showing flips into free-text and captures the key (model.go key capture), instead of accumulating in a hidden input that Enter then overrode with the highlighted option. - removed dead submitAskUserAnswer (the Enter handler now routes through confirmAskUser; the orphan had no callers). Tests: multi-select falls back to free-text and returns the typed answer; a printable key in the selector switches to free-text and is captured. Full gate green (build / vet / go test ./... -race / make lint). * fix(tui): address PR review — element-wise option compare, clearer Esc copy - ask_user_test: compare option slices with slices.Equal instead of strings.Join, so the parser test can't be fooled by ["A,B"] vs ["A","B"] or nil vs [""]. - ask_user prompt: the Esc hint said "skip", which reads like "skip this question" on a multi-question prompt even though Esc ends the rest of the questionnaire (remaining answered by best-assumption). Reworded to "skip the rest" in the selector and free-text hints; "type my own" keeps "go back". |
||
|
|
0e7318ff72 |
feat(tui): clickable plan-step detail + plan/sidebar UX & rendering polish (#315)
* fix(tui): plan panel reflects live progress + prompt cadence
The plan panel could read as stuck mid-task even while work continued. Three
fixes:
- Header showed steps[0] forever, so the title never advanced as the plan
progressed; show the current step (in_progress, else first incomplete).
- updateFromItems matched steps by content only, so an in-place reword reset a
step's elapsed timer to zero; add positional carry-over when the step count is
unchanged.
- Prompt: tell the model to mark each step completed + the next in_progress
before starting it, so the plan tracks real progress instead of jumping at the
end.
Regression tests included.
* feat(tui): click a plan step to see what was built for it
Each plan step now records the file mutations (write_file/edit_file/apply_patch)
made while it was in_progress, captured from tool-result rows. Clicking a step
row in the context sidebar drops a transcript card listing those changes — the
implementation for that step. Step->line mapping mirrors sidebarAgentSelectables'
offset accounting (one line per step); capture is keyed to the active step and
cleared per run. Read-only and additive. Tests cover the offset math + capture.
* feat(tui): plan-step detail shows diffs + commands
Extend the clickable plan-step detail: capture bash/exec_command runs in addition
to file mutations, store each tool's full output (the diff for edit/apply_patch,
stdout/stderr for commands), and render the card grouped into Changes and
Commands with a short truncated excerpt of each item's diff/output.
* fix(tui): plan-step detail toggles instead of stacking duplicates
Re-clicking a plan step now hides its card (a stable transcript id + drop-by-id),
and clicking a different step replaces it, so at most one detail card is shown.
Previously each click appended a fresh card and they piled up.
* feat(tui): elaborate plan-step detail by status (what we did / will do)
* feat(tui): plan-step card leads with a prose explanation (agent's own narration)
* feat(tui): write plan-step explanations with the model on click
Clicking a PLAN step now shows a fresh, plain-English write-up of that
step, generated on demand by the active model:
- past tense ('what we did') for completed/failed steps, future tense
('what we'll do') for pending/in_progress steps
- immediate feedback: the card drops in showing 'Writing explanation…'
and updates in place (by stable row id) when the text returns
- cached per step (content+status) so re-clicking is instant with no
second model call; re-clicking still toggles the card closed
- the prompt is built from already-captured local data (step text,
status, notes, plus a compact digest of file edits, commands, and the
agent's narration) and asks for short, non-technical prose
- reuses the TUI's existing provider via a one-shot StreamCompletion;
no new client, no hardcoded provider. Falls back to the local summary
when no provider is configured or the request fails.
* fix(tui): minimal plan-step card styling (dim border, status-tinted title)
* feat(tui): auto-hide context sidebar when it has no agents or plan
* feat(tui): widen chat reading column to fill wide terminals with breathing room
* fix(tui): toggle context sidebar silently (no transcript notice)
* fix(tui): mark plan complete when a finished task forgot the final update_plan
* fix(tui): remove the lime streaming-text glow (render responses in static ink)
* fix(tui): remove the shaded panel band behind tool-result cards
* fix(tui): Ctrl+B hides the plan entirely (no pinned-panel fallback)
* style(tui): gofmt-align model struct fields
* feat: narrate the build as a clean story (hide plumbing cards, live plan progress, narration prompt, write_file line counts)
* feat(tui): render agent narration bright (ink) so the build reads as a story
* feat(agent): demand an elaborate, structured closing summary (not a single line)
* feat(tui): mark agent narration with a leading bullet (selection-aligned)
* feat: post-turn recap line (※ recap:) with /config recaps on|off toggle
* feat(tui): show 'still generating…' during quiet stretches + count tool-call streaming
* feat(tui): ACTIVITY panel under PLAN + push the model to step the plan per file
* fix(tui): show the quiet 'still generating' hint only when the sidebar is hidden
* feat(tui): code preview in write/edit/apply cards (card-only, no model token cost)
* fix(tui): address CodeRabbit review — stale plan timestamps, stale explanation, Ctrl+B persistence, UTF-8 truncation
* fix: address CodeRabbit re-review round 2 (7 actionable + 2 nitpicks)
* chore: drop accidentally-committed WIP internal/cli/dryrun_test.go
It is an unrelated, pre-existing dry-run test that does not compile (references a
non-existent agent.Options.DryRun field, an unused import, and out-of-scope
sandbox symbols). It was swept into the prior commit by 'git add -A' and is not
part of this PR's scope.
|
||
|
|
11926cc5d2 |
feat(tui): click a swarm member in the sidebar to open its subchat (#311)
* feat(tui): click a swarm member in the sidebar to open its subchat Specialists were drillable (click the card → subchat shows the child session), but swarm members shown in the AGENTS sidebar were not — even though each member already runs as a `zero exec --init-session-id <sid>` child that persists a durable session in the same store the TUI reads. This wires that session through so a member row is clickable. - swarm: Coordinator records the member's child session id on completion (CompleteWithSession / Task.SessionID); swarm_collect emits a task_id → session_id map in its Result.Meta. - tui: the OnToolResult closure forwards that map via swarmSessionsMsg into m.swarmSessionMap; swarmAgent carries the session id; the AGENTS sidebar records a clickable hit per member whose session is known. - tui: sidebarLineAtMouse maps a left-click in the sidebar column to a member row (screen-Y == sidebar line index; sidebar starts at chatColumnWidth + 3), and the existing specialist-card drill-in path (subchat.enter) is reused verbatim. Members are clickable only once their session id is known (after swarm_collect), so in-flight members simply aren't clickable — no dead clicks. The subchat machinery, Result.Meta transport, and the click handler are all reused; no new persistence or agent-loop changes. * test(tui): assert sidebar member click drills into the member session Address review: the routing test only checked the click was handled, so it could pass even if the drill-in didn't enter the expected subchat. Seed a real session in the store and assert subchat.active + childSessionID == the member's session after the click. |
||
|
|
c498a8b870 |
fix(tui): cap the expanded live reasoning to ~half screen (#303)
Clicking the live 'Thinking…' line expanded the streaming reasoning to its full length, which filled the terminal as it grew and scrolled the clickable toggle header off-screen — so it couldn't be collapsed again. Cap an EXPANDED live reasoning block to ~half the screen height, keeping the LATEST lines plus a faint '… N earlier lines · Ctrl+O for all' marker, so the header stays on-screen and clickable. Applied identically to the display and selectable render paths so the gutter highlighter stays line-aligned; committed rows (expanded in the scrollable history) are uncapped. Regression test added. |
||
|
|
63c0e278ff |
feat(tui): chat + context sidebar redesign, AGENTS panel, animations (#300)
* 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.
|
||
|
|
c3646e3a94 |
Fix TUI UX issues (#290)
* Improve Ctrl+C exit confirmation UX Make the first Ctrl+C arm a three-second footer confirmation instead of exiting immediately. A second Ctrl+C during that window exits, while active runs still cancel on first press and preserve checkpoint/session flush safety before any deferred quit. Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/tui -run 'TestCtrlC|TestStatusLineGroups|TestGenericCustomProviderDisplayUsesEndpointName' -count=1 Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/tui -count=1 Tests: git diff --check * Clear composer before Ctrl+C exit confirmation Treat Ctrl+C as draft cancellation when the composer has text: clear the composer and suggestions without arming the exit warning. Empty composer behavior still uses the three-second second-press exit confirmation. Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/tui -run 'TestCtrlC|TestStatusLineGroups|TestGenericCustomProviderDisplayUsesEndpointName' -count=1 Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/tui -count=1 Tests: git diff --check * Document Ctrl+C deferred quit safety Restore the reasoning near handleCtrlC for why a confirmed exit waits on flushRunIDs before quitting: cancelled runs may still need to persist checkpoint/session events for rewind safety. Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/tui -run 'TestCtrlC|TestStatusLineGroups|TestGenericCustomProviderDisplayUsesEndpointName' -count=1 Tests: git diff --check * Add composer mouse selection Support normal chat-composer mouse behavior: clicks place the cursor, drags select typed composer text with the existing selection styling, and release copies the selected text before clearing the temporary highlight. Keep this as app-level mouse handling so transcript selection, suggestions, and right-click paste continue to work. Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/tui -run 'TestComposerMouse|TestTranscriptSelection|TestMouseCapture|TestCtrlC' -count=1 Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/tui -count=1 Tests: git diff --check * Make transcript tool output copyable * Tighten TUI exit and composer mouse UX |
||
|
|
0aa45695a8 |
feat(tui): declutter the chat — drop the gimmicky billboard chrome (#291)
Make the chat read like a clean status surface (Claude Code / codex / opencode) instead of a branding billboard. The audit's through-line was "noise that restates what's already on screen, plus brand/meme flavor with no off-switch"; the fix is almost entirely deletion: - Replace the 40-slot meme/brand working-verb ring (gitlawbmaxxing, openfablemaxxing, maxxing, pilled, aura-farming, vibe-checking, tomfoolering, booping, ...) with one calm static "Working". Deletes working_words.go; the spinner + advancing clock are the only motion. - Quiet the user turn: drop the full-width painted prompt band and its two padding bands; keep just the ▌ accent gutter with a single plain blank line as the delimiter. - Stop stamping "completed in Ns · K tools" under every turn. Only a turn over 60s earns a faint "worked for ..." bookend; short turns get none (the next user prompt is the separator). Drops the tool-count segment. - Remove the per-card [auto] badge (the permission mode is already shown in the composer divider). - Drop the duplicate plan progress bar; the header done/total + per-step icons already convey progress. Deletes renderPlanProgressBar. - Don't double-mark failed turns: the bordered error note already signals failure, so drop the extra red done-line. Tests updated to the clean behavior. Full internal/tui suite green; gofmt/vet/build/deadcode clean. Deferred to a follow-up: the empty-state figlet/tagline, the all-caps reverse-video badges, model/provider/context consolidation, terser permission labels. |
||
|
|
efe48ade0c |
feat(tui): syntax-highlighted code, word-level diffs, reduced-motion gate (#289)
* feat(tui): syntax-highlighted code, word-level diffs, and a reduced-motion gate
The remaining three items from the reference-TUI comparison, focused on
"real content rendering" + motion accessibility.
1. Syntax-highlight fenced code blocks. renderMarkdownCodeBlock passed code
through plain; now the fence language is captured and the block is tokenized
with chroma (new dep, pure Go) and colored with Zero's OWN audited palette —
keyword=accent, string=green, number=amber, comment=faint, function=blue,
punctuation=muted — so it stays on-brand and degrades through the same
lipgloss profile path (truecolor→256→16→plain). Wrapping is token-level so a
color never splits across a wrap. Unknown/missing language falls back to the
old plain rendering, so it is never worse than today.
2. Word-level diff highlighting. diffCardBody tinted whole add/del lines; now an
isolated 1:1 replacement (one "-" followed by one "+") highlights only the
changed span on each side via a brighter changed-span bg (new addBgWord/
delBgWord tokens), after trimming the common prefix/suffix. Gated to <=60%
change so a near-rewrite is not confettied, and block changes keep whole-line
tint. Changed-span colors validated by WCAG ratio (fg >=4.6:1, clear
separation from the base band).
3. One reduced-motion gate. ZERO_REDUCED_MOTION (and no-TTY) now swaps the
animated spinner for a steady dot via m.spinnerGlyph() at every render site
(working line, plan, tool/specialist cards) and forces the fade off — one
switch for all motion, where ZERO_NO_FADE only governed the fade. Liveness is
preserved by the advancing elapsed timer, so a static frame never reads as
frozen.
Tests cover the reduced-motion resolver + static glyph, changedSpan/word-diff
pairing + the near-rewrite fallback, and the highlighter's unknown-language
fallback + wrapping. Full internal/tui suite green; gofmt/vet/build clean.
* fix(tui): address render-polish review — highlight committed rows only
Adversarial review found a real interactive-jank regression and three smaller
issues; all fixed:
- PERF (blocker): the live streaming block re-tokenised the whole accumulated
code via chroma on every frame. renderAssistantMarkdownText now takes
allowHighlight; the interim/streaming path (model.interimBlock) and reasoning
bodies pass false (plain, cheap), and only committed/cached rows
(renderAssistantRow, selectable rows) highlight — so chroma runs once per row,
never per frame.
- PERF: cache language->lexer lookups (including nil) and short-circuit an empty
language before chroma's slow registry Match scan.
- Fences with metadata: take the first whitespace token as the language
("```go title=x" -> "go") instead of the whole info string.
- Reduced motion: freeze the compact and doctor braille rings too (their frame
counters no longer advance under ZERO_REDUCED_MOTION), honoring the one-switch
contract.
Tests: streaming code renders verbatim/plain; diff word-span colors AA on both
themes. Full internal/tui suite green; gofmt/vet/build clean.
* test(tui): opt the doctor/compact animation tests into motion
The reduced-motion gate auto-enables under no-TTY (colorprofile.NoTTY ->
reducedMotion), which is exactly the CI environment. That froze the compact and
doctor status rings, so two existing tests asserting "animates on tick" failed on
the Linux/macOS runners (Windows CI detects a color profile, so it passed there —
the platform split). Both tests exercise the animation mechanism, which is only
active when motion is on, so set m.reducedMotion = false in their setup to test it
deterministically regardless of the runner's detected profile.
|
||
|
|
96a851df2d |
feat(tui): transcript polish — left-rule cards, tool-call density, pinned plan (#269)
* feat(tui): unify tool cards onto the left-rule card style Tool result/running cards were the last transcript surface still drawn as a full rounded box (╭─ head ─╮ / │ body │ / ╰─ footer ─╯), while specialist cards (#255) and the reference TUIs (opencode, codex) use a lighter left-rule card. Mixing two card languages in one transcript reads as un-designed, so converge tool cards onto the same left-rule shape. toolCard() now draws a status-tinted "│ " rail down the left edge with the head on the first line and the status glyph (✓/✗/spinner) right-aligned to the card edge — no top, right, or bottom borders. The glyph moves out of toolCardHead onto the rule line. Width math is preserved: every emitted line is still exactly `width` cells (the two cells where the right border sat become panel background), and the inner content budget stays width-4, so the diff/read/bash/grep body renderers — which assume width-4 — are untouched. The tiny tier keeps its existing borderless behavior (no leading "│"), so TestTinyToolCardDropsSideBorders still holds. Click-to-expand/collapse is unaffected: the toggle attaches to the head line by relative offset, which is still the first line. Tests: update TestRunningToolCardShowsHeadAndSpinnerSlot to assert the left rule (no box corners); add TestToolResultCardRendersAsLeftRule for the result-card shape + glyph-on-rule; the all-lines-equal-width invariant (TestToolCardLinesAllSameWidth) is unchanged and still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * feat(tui): cut tool-call transcript density (paths, auto-approve rows, redundant bodies) Follow-up to the left-rule card frame in this PR — same surface, the content inside/around the cards. A run of MCP file ops rendered ~5–7 lines each, with the same absolute path repeated 3× and the tool name twice. The reference agents (opencode, codex) collapse a successful call to ~1 line, use workspace-relative paths, and only surface approval when the user is actually prompted. Five changes bring tool cards in line: 1. Auto-approved permission rows: the OnPermission handler always recorded the audit event but only renders a visible row when the event is noteworthy (a real prompt, a deny/cancel, an explicit durable decision, or a sandbox block) — not for silent auto-approves. A new permissionEventIsNoteworthy predicate gates both the live path (model.go) and the resume rebuild (session.go) so resumed sessions match the live view. The session log is unchanged — only the rendered row is suppressed. 2. displayPath relativizes a tool card's target to the workspace (D:\...\examples\calc\calc.go -> examples/calc/calc.go), falls back to ~/... under home, then to …/last-3-segments for external paths. Display only; the hyperlink still points at the original absolute path, and the path sent to the tool / stored in the session is untouched. 3. A successful call whose output is a one-line confirmation ("Successfully created directory …", "Created x (N bytes).") drops its body — the head already shows the action, target, and ✓. Narrow verb allowlist; errors and multi-line/real output always keep their body. 4. displayPath's external-path tail (…/segments) — folded into (2). 5. One blank line between consecutive tool cards in a turn. Separators were only inserted at turn boundaries, so back-to-back cards stacked with no gap (the dense "wall"). Tool-card↔tool-card only; specialist cards keep their own grouping. Net: each MCP file op goes from ~5–7 lines to ~1, paths relative, no auto-approve noise. Tests cover the predicate (suppress auto / keep real decisions), displayPath (cwd / home / external tail / relative passthrough), body collapse (confirmation vs real output vs error), and the inter-card blank. Two existing permission tests updated for the new intent; full internal/tui suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * feat(tui): pin the plan panel above the composer The plan panel was injected inline into the scrolling transcript body, so a streaming turn pushed it up and out of view exactly when it was most useful. Pin it in the footer, directly above the composer, so it stays visible while the transcript scrolls underneath. - footerView renders the pinned plan above the composer; the scrollable frame already reserves footer height, so no extra layout plumbing is needed. - A height budget (pinnedPlanMaxHeight: at most a third of the screen) keeps a long plan from crowding out the transcript or the input — when the full panel would exceed the budget it collapses to a one-line summary (renderPlanSummaryLine: spinner/✓, done/total, current step). Ctrl+P still expands, but the budget always wins so the composer stays on screen. - The two inline-injection sites in transcript_selection.go are removed (the panel no longer lives in the body), along with the now-unused planPanelEmitted flag. Tests: full-when-it-fits, collapse-to-summary-when-too-tall, hidden-when-empty, and footer-renders-plan-above-composer. Existing plan-panel tests unchanged and green; full internal/tui suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
b237e9849a |
feat(tui): sticky plan panel + specialist task cards + task table overlay (#255)
* feat(tui): sticky plan panel + specialist task cards + task table overlay Three new TUI surfaces that give users live visibility into agent progress and specialist delegation, so long turns and subagent work never look frozen. Sticky plan panel (plan_panel.go): - Pinned between the title bar and transcript body, shows the agent's step-by-step plan from the update_plan tool as a live checklist. - Header with spinner + progress count + elapsed; text progress bar; per-step list with status icons (✓ completed, ⠹ in_progress, ○ pending, ✗ failed) and individual step timings. - Auto-collapses to a one-line green summary 30s after completion. Ctrl+P toggles expand/collapse. - Plan state synced from update_plan tool results via CurrentPlan(); timestamps preserved across the tool's full-replacement updates by matching on content. Specialist cards (specialist_card.go): - New rowSpecialist transcript row kind; EventSpecialistStart/Stop now handled in session.go hydration (previously fell through to default and were silently dropped on resume). - Animated card with spinner + name + task + elapsed + token count + tool call count. Blue while running, green when done, red on error. - specialistTracker tracks live state; Task tool calls/returns are intercepted in the OnToolCall/OnToolResult callbacks to start/complete tracking and emit card rows. - Height cache marks running specialist rows unstable so the spinner animates. Task table overlay (task_table.go): - Ctrl+G toggles a full-width overlay showing all specialists in the session as a sortable table: #, Specialist, Task, Status, Time, Tokens, Tools. - Summary bar with running/completed/error counts, total tokens, aggregate elapsed. - Arrow keys navigate rows; Esc closes the overlay. Tests: plan_panel_test.go (7 tests), specialist_card_test.go (11 tests), task_table_test.go (8 tests). All existing TUI tests pass. * fix(update_plan): expose structured nested item schema so models stop sending bad arguments The plan property's Items schema was omitted because of an outdated comment claiming the serializer drops nested Properties/Required. The serializer (propertyToRuntimeMap) already handles them recursively. Without the nested schema, weaker models guessed the item structure from a text description and sent malformed arguments, hitting the 4-strike guardrail halt. Now exposes Items as an object with content (string, required), status (enum), and notes (string). * fix(tui): route plan + specialist updates through message sink, not stale closures The OnToolCall/OnToolResult callbacks captured model by value, so m.plan.updateFromItems() and m.specialists.start/complete() mutated stale copies that never reached the live model. Introduced planUpdateMsg, specialistStartMsg, and specialistCompleteMsg tea.Msg types sent through runtimeMessageSink, handled in updateModel() where they mutate the live model. * feat(tui): subchat drill-in for specialist sessions Enter on a task table row loads the child session's events and swaps the transcript view to show the subagent's full conversation (user messages, agent text, tool calls, results). A nav bar at the top shows the specialist name/task and a back hint. ArrowUp or Esc pops back to the main chat at the saved scroll position. - subchat.go: subchatState with enter/exit, renderSubchatNavBar - transcript_selection.go: transcriptBodyItemsFromRows renders arbitrary rows (reuses the same rendering pipeline as the main transcript) - model.go: Enter on task table row triggers drill-in; ArrowUp/Esc exits; transcriptView() short-circuits to child rows when subchat is active * fix(ci): strip CRLF line endings and BOM from plan_tool_test.go and update_plan.go gofmt and git diff --check failed on ubuntu CI because PowerShell's Set-Content wrote UTF-8 with BOM and CRLF. Stripped both to plain LF without BOM. * feat(tui): add renderLeftRuleCard helper for borderless specialist cards * feat(tui): switch specialist cards to left-rule rendering, drop hint line * feat(tui): add renderSpecialistSummary one-line rollup for specialist cards * fix(tui): correct slice offset in renderSpecialistSummary and add pluralization test The muted tail was sliced at summary[len(spinnerView)] but the spinner sits at byte offset 2 (after the 2-space indent), so the slice cut into the spinner's UTF-8 continuation bytes and dropped the indent. Slice at 2+len(spinnerView) instead. Also add a test asserting '2 errors' is present and '2 error' (singular) is absent when two specialists errored. * refactor(tui): remove task table overlay — cards carry everything now * feat(tui): render specialist summary line above cards in transcript * feat(tui): mark specialist card lines as clickable selectable rows * feat(tui): click specialist card to drill into subchat, Enter as keyboard fallback * tui: clear selectedSpecialistID after Enter drill-in Fixes Task 7 review issue: selectedSpecialistID persisted after Enter drill-in, causing a subsequent Enter to re-drill into the specialist's subchat instead of submitting the composer. Clear it immediately after subchat.enter succeeds so the keyboard fallback is one-shot per click; click re-sets it, so click->Enter still works. * chore: gofmt model.go + test files * chore: gitignore .superpowers/ and docs/superpowers/ — local skill artifacts * fix(tui): remove Enter keyboard fallback for subchat drill-in — mouse click only * fix(tui): reconcile specialist childSessionID from tool call ID to real session ID on completion * feat(specialist): add Progress callback field to RunOptions and TaskRunOptions * feat(specialist): stream child stdout line-by-line via Progress callback * feat(agent): wire OnToolProgress callback for specialist child events * feat(tui): show live tool-call progress in specialist cards * fix(swarm): update RunChild mock signature for new progress callback param * fix(tui): render plan panel inline in transcript, not pinned at top * chore: gofmt model.go, specialist_card.go, specialist_click_test.go * fix(tui): address PR review - plan panel without specialists, stream reader safety, resume dedup + payload keys |
||
|
|
2d62387e08 |
Align permission session approvals (#244)
* Align permission session approvals Add in-memory session grants, workspace-write auto-allow for built-in file mutation tools, and dynamic permission choices for once/session/future approvals. Persist future approvals only when a grant store is configured, while keeping session approvals scoped to the active engine. Tested: GOCACHE=/tmp/zero-go-cache go test ./... * Guard workspace write auto-allow Exclude protected workspace metadata from built-in file-tool auto-allow so writes under .git, .zero, and .agents re-prompt instead of silently running. Derive apply_patch targets from patch headers inside the sandbox engine so patch-body escapes and symlink traversal are denied before workspace auto-allow. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestEngineAutoAllowsWorkspaceFileMutationTools|TestEngineDoesNotAutoAllowProtectedMetadataWrites|TestEngineDeniesApplyPatchEscapesFromPatchBody|TestEngineSessionGrantDoesNotMatchSiblingFile|TestEngineDeniesAutoAllowedSymlinkEscape|TestEngineDeniesWorkspaceSymlinkTraversal|TestDeriveScope' Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/agent ./internal/tui ./internal/tools Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: git diff --check |
||
|
|
b1cf24831d |
fix(tui): selectable + clickable permission popup (#232)
The permission prompt now has a moving cursor and clickable options, on top of the existing a/y/d hotkeys (which still resolve directly): - ↑/↓ and Tab/Shift+Tab move the highlight across allow once / always / deny (resting default: allow once); Enter confirms the highlighted option. - each option is rendered on its own row, and a left-click on a row resolves that choice directly. - permission_prompt.go: permissionOptions + cursor move/clamp/confirm helpers. - model.go: pendingPermissionPrompt gains a cursor; Enter confirms it, ↑/↓/Tab move it; a/y/d are unchanged. - rendering.go: renderFocusedPermissionPrompt renders per-option rows with the cursor highlighted (▸ + reverse label) and returns each option's card-relative Y offset so the caller can register clickable rows. - transcript_selection.go: transcriptSelectableLine gains permOption/permChoice; the pending-permission item registers each option row as clickable, and a left-click on one resolves the permission. Tests: cursor default/move/wrap, Enter confirms the highlight, hotkeys still resolve directly, render emits highlighted clickable offsets. Existing card/ submit-block tests updated for the new per-option layout + Enter-confirms. |
||
|
|
0b496ce87d |
Render transcript viewport from measured items (#227)
Add a bounded per-model transcript body height cache keyed by the existing row render fingerprint, then use measured spans to render only the visible alt-screen item window instead of building the full body string every frame. Mouse hit testing now uses the same visible item layout, while inline rendering and selection copy keep the full layout path for behavior parity. Tests cover height cache reuse, visible-only item rendering, absolute selectable body coordinates, and output parity with the previous full-layout renderer. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..." |
||
|
|
724167e90e |
Harden TUI layout and mouse routing (#226)
* Centralize TUI frame layout Add a reusable transcript frame snapshot with terminal regions for header, body, footer, composer, and status lines. Mouse composer hit testing, overlay positioning, and transcript viewport mapping now consume the same frame geometry instead of recalculating pinned-header and clipped-footer offsets separately. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui * Route TUI mouse hits through local regions Return overlay hit coordinates relative to the overlay block and derive transcript hit rows from the shared body rect. This keeps the current scroll behavior while making mouse handling consume component-local geometry from the frame layout. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui * Cover TUI frame geometry Add focused tests for the shared frame snapshot: pinned header/body/footer regions, tiny-terminal footer clipping, composer mouse hit regions, overlay centering, and transcript mouse row mapping through the body region. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui * Extract transcript viewport line window Introduce a line-based transcript viewport adapter that preserves Zero's existing bottom-anchored scroll offset. Render slicing, chat scroll metrics, scrollChat, and transcript selection viewport mapping now share the same clamping and visible-window calculation. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui * Build transcript body from layout items Introduce a compatibility transcript body item layer for title, empty state, separators, transcript rows, pending prompts, streaming interim content, and spec review prompts. The body still renders to the same line/selectable output, but layout now records item spans and local selectable offsets for the next visible-range step. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..." * Address TUI layout review findings Clamp overlay mouse hit regions to the visible transcript body and block transcript selection while the MCP add wizard is open. Strengthen layout and viewport edge-case tests, including full composer containment and degenerate dimensions. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..." * Make MCP wizard overlay test hit transcript text CodeRabbit caught that the MCP add wizard blocking test clicked at x=0, which could miss selectable transcript text and pass without proving the overlay guard. This updates the test helper to return both textStart and y, then clicks that known selectable point before asserting the wizard blocks transcript selection behind the modal. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check Tested: GOCACHE=/tmp/zero-go-cache go test ./... * Route transcript windows through body layout Thread the structured transcriptBodyLayout through alt-screen rendering, scroll metrics, and transcript mouse lookup instead of repeatedly joining and splitting the rendered body string. This preserves the current line-based viewport behavior while making the body layout the source of truth for visible windows and selectable line lookup. This is an incremental step toward visible-item rendering and item-level caching; it does not change the transcript visual output or input flow. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..." * Reuse row cache for selectable transcript rows Selectable user and assistant rows need selectable metadata for mouse interactions, but when no transcript selection is active their rendered strings are identical to the normal row render path. Route those rendered strings through renderRow so stable rows reuse the existing row cache while selection-active frames still render highlight-aware output. This keeps transcript behavior unchanged and adds a regression test that verifies selectable assistant rows produce stable metadata while hitting the render cache on the second render. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check Tested: /bin/bash -lc "GOCACHE=/tmp/zero-go-cache go test ./..." |
||
|
|
7af2a1b3b6 |
TUI polish and fixes (#220)
* Persist TUI model switches * Pin TUI title bar in chat view |
||
|
|
9c1cc69b59 |
Migrate TUI to Bubble Tea v2 (#222)
* Migrate TUI to Bubble Tea v2 Move the TUI stack to charm.land Bubble Tea, Bubbles, and Lip Gloss v2, including key, paste, mouse, and view handling changes for the v2 APIs. Add compatibility helpers for the new input event shapes and update TUI tests accordingly. Refresh related Go dependencies and make the PDF truncation fixture deterministic across poppler and pure-Go extraction. Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/tui Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/imageinput -count=1 -v Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./... * Address provider wizard OAuth review feedback Remove the unused mouseButton compatibility helper flagged by lint. Correlate provider wizard OAuth and device-code async results with the active provider and attempt id so stale results from abandoned attempts are ignored. Add regression coverage for stale browser OAuth and stale device-code results. Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/tui -run 'ProviderWizard.*OAuth|ProviderWizard.*Device|ApplyProviderWizardOAuth|RenderCredentialStepShowsOAuth' -count=1 -v Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./internal/tui Tested: GOCACHE=/tmp/zero-go-cache GOMODCACHE=/tmp/zero-go-mod-cache go test ./... Tested: git diff --check * Smooth TUI mouse scrolling |
||
|
|
efb6b79164 |
web_search robustness + chat UX: collapse, clipboard, scroll-pin, proactive search (#218)
* web_search: fix #209 follow-ups (tolerant score, render gate, redaction, FQDN) Verified-and-fixed defects from a review of #209: - Tolerant score decode (lenientScore): a stringified/odd/null "score" no longer makes json.Unmarshal discard the entire response; it degrades to absent. - Score render gate rounds first (>= 0.01) so tiny positives and negatives no longer print a noisy "score 0.00". - Domain-filter error now goes through redactWebSearchText, matching the other error paths. - Trailing-dot FQDN symmetry: "react.dev." and "react.dev" normalize the same on both the allowlist and result sides. Left as-is (intended, tested): scheme-less host:port allowlist entries. Includes mixed-type domains-array tests. * web_search: only register the built-in tool when a backend is configured When no search backend is set (ZERO_WEBSEARCH_BASE_URL / ZERO_WEBSEARCH_BACKEND), CoreNetworkTools no longer offers the built-in web_search. Previously it was always registered, so a model with an MCP search tool (e.g. Exa) would burn several calls + high-risk permission prompts on the built-in tool — which can only return "no backend configured" — before falling back to the working MCP tool. Now: no backend -> only web_fetch (+ any MCP search tool) is offered, so the model uses the MCP search directly. A configured backend behaves exactly as before. Tests: registry/web_search/specialist tests that asserted web_search is always in CoreTools now configure a backend; added a test that it is omitted when none is set. (Pre-existing, unrelated: internal/cli doctor/exec tests fail in the sandbox on origin/main too — network/provider-dependent, not touched here.) * tui: copy selected transcript text to the native OS clipboard Selecting transcript text auto-copies on mouse release, but the copy went only through OSC52 (ansi.SetSystemClipboard), which macOS Terminal.app doesn't support at all and iTerm2 / some Windows Terminal builds have off by default — so the selection silently never reached the clipboard. Copy via the native clipboard (pbcopy / clip.exe / xclip) first, which works on local terminals regardless of OSC52, and fall back to OSC52 for remote/SSH sessions where no local clipboard utility is reachable. atotto/clipboard was already in the module graph (indirect); promoted to a direct import — no new dependency. Gates: gofmt/vet/build(host+linux+windows)/tui test -race/staticcheck all pass. * tui: collapse long tool output by default with click-to-expand Long, noisy tool output (web-search / MCP / read dumps) flooded the transcript. Now a tool result whose output exceeds the live body cap renders collapsed by default — head + a "▸ N lines — click to expand" footer — and you click the card (while it is live) to expand (▾) / collapse. Mirrors the existing reasoning-row collapse, so collapsed rows also flush to scrollback clean. Scoped to avoid hiding what matters: - diff tools (edit_file / apply_patch / write_file) never collapse — their body must stay reviewable (matches the deeper flush cap intent); - the uncapped detailed transcript view (Ctrl+O, bodyCap==0) never collapses — it still shows full output; - short output (<= cardBodyMaxLines) stays inline as before. Reuses transcriptRow.expanded + the row toggle (toggleReasoningRow generalized to toggleTranscriptRow); the card head is a clickable toggle line. Running tool cards (spinner) are unchanged. Tests: collapse/expand, short-output inline, diff tools never collapse, toggle, and the head exposes a click toggle; existing diff + detailed-view tests updated. Gates: gofmt/vet/build(host+linux+windows)/tui -race/staticcheck all clean. * cli: fall back to a usable saved provider instead of re-running onboarding If the active provider lacked a credential (e.g. it relies on an env var that isn't set, or an OAuth login that isn't stored), the TUI forced first-run onboarding on every launch — even when other saved providers were fully configured. Each onboarding then appended yet another provider. Now, when the active provider isn't usable, fall back to the first usable saved provider (prefer a remote keyed one over a local endpoint) for the session, and skip onboarding. Onboarding only runs when no configured provider is usable — a genuinely fresh setup. Saved logins persist; switch the active provider with `zero provider use <name>`. Tests: firstUsableProvider prefers remote keyed, falls back to local, accepts a keyless local proxy, and reports none-usable. Gates: gofmt/vet/build (host+linux+windows)/staticcheck clean; the only failing cli tests (doctor/exec, network-dependent) are pre-existing on origin/main. * tui: hold the scroll position while output streams in The chat viewport scroll offset is measured from the bottom, so when the transcript grew (a streaming turn) the window followed the new bottom and yanked the user off whatever they had scrolled up to read. syncChatScroll now pins the viewport: while the user is scrolled up, it bumps the offset by however many lines the body grew, so the absolute view holds where they are reading; at the bottom (offset 0) it follows the tail as before. Hooked into the single Update funnel; only the scrolled-up path renders the body, so the common (at-bottom) case stays cheap. Tests: pins by the growth delta while scrolled up; resets/follows at the bottom. Gates: gofmt/vet/build(host+linux+windows)/tui -race/staticcheck clean. * agent: search the web proactively instead of answering "I don't know" When a web search/fetch tool is available and the user asks about something external the model is unsure of or that may be current (a product, library, company, price, version, recent event), the system prompt now tells it to search and read results before answering, rather than replying that it doesn't know without checking. Addresses the model saying "I don't know about that" until the user explicitly asked it to search. Tests: prompt includes the new guidance ("search the web and read"). Existing prompt assertions use Contains, so the addition is non-breaking. * tui: show clean labels for MCP tool cards (mcp_exa_web_search_exa → web search) * agent: sharpen web-search guidance (disambiguate, refine, scale) Strengthen the search rule so the model searches before answering about an external entity/product/library/model/version/release it doesn't recognize, doesn't assume the most common meaning of an ambiguous name (disambiguates by the conversation's domain), and refines + re-searches when results look off-topic — rather than confidently answering the wrong interpretation (e.g. reading "Fable 5" as a video game instead of an AI model). Also: don't web-search timeless facts or workspace-answerable questions (use the file tools), keep queries short, and scale the number of searches to the question. Tests: prompt includes the new guidance ("search the web before answering", "do not recognize"). Existing assertions use Contains, so non-breaking. * test: gofmt setup_fallback_test.go (comment alignment) * Address review feedback on web_search + chat UX Resolve CodeRabbit review comments on this PR: - firstUsableProvider: skip a profile whose CatalogID no longer resolves *and* that has no explicit BaseURL (no endpoint → unusable). A stale CatalogID with a BaseURL still works as a custom endpoint and is kept. - providerProfileIsLocal: classify by parsed URL hostname (exact localhost / 127.0.0.1 / ::1) instead of substring matching, so hosts like "notlocalhost.com" are no longer misread as local. - lenientScore: reject non-finite scores (ParseFloat accepts "NaN"/"Inf") so the documented score filter actually holds and the renderer never emits a non-finite value. - syncChatScroll: shift the pinned from-bottom offset by the signed line delta so the view also holds when the body shrinks (card collapse, transcript clear), clamped at zero; previously only growth was handled. - copyTranscriptSelectionCmd: report "Copy failed" when both the native clipboard and the OSC52 fallback fail, instead of claiming success; keep the selection so the user can retry. - Drop vestigial ZERO_WEBSEARCH_BACKEND test env: the native backend was removed (MCP path), so the var is read by no product code and the references misrepresented it as a config knob. Adds tests for the local-host parsing, the catalog/BaseURL skip, and the non-finite score rejection. |
||
|
|
2bcb85bfb3 |
Polish TUI title bar (#219)
* Polish TUI title bar * Show PR diff stats in TUI title bar Add a small PR status service that detects the active GitHub PR or GitLab merge request via gh/glab, computes diff stats against the PR base with git diff --shortstat, and publishes updates to the TUI. Render the resulting +N -N #PR segment in the title bar next to the branch, linked to the PR URL, while keeping the composer footer unchanged. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check * Make submitted prompt bubble terminal-safe Replace half-block top/bottom rows in submitted user prompts with background-filled padding rows. macOS Terminal renders the half-block glyphs as visible horizontal artifacts, while filled-space padding keeps the prompt bubble height without relying on ambiguous block glyph behavior. Update selectable transcript rendering to use the same padding helper and remove the now-unused half-block theme style. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: git diff --check * Address PR status review feedback |
||
|
|
e1253c826c |
Polish assistant response transcript (#204)
* Polish assistant response transcript * Keep copy feedback above composer |
||
|
|
6a9f44a716 |
Polish TUI transcript and reasoning display (#200)
* Polish TUI transcript and reasoning display * Address provider and transcript review fixes * Gate inline think tag parsing * Cover disabled think tag parsing config |
||
|
|
250bf24c9d |
feat: add MCP manager UX (#188)
* feat: add MCP manager UX * fix: harden MCP manager UX * test: stabilize agent eval timeout on Windows * fix: preserve MCP server metadata * fix: clarify empty MCP view * feat: make MCP command a manager view * fix: make MCP manager usable in TUI * fix: make MCP manager selectable in TUI * fix: simplify MCP manager popup * feat: add searchable MCP marketplace * fix: address MCP review issues * fix: address MCP follow-up review * feat: add MCP setup wizard * feat: detect MCP setup requests in chat * fix: address MCP manager review feedback * fix: avoid hijacking MCP chat prompts * fix: persist MCP view cache fallback |
||
|
|
761b797863 |
Render assistant Markdown in TUI (#187)
* Render assistant Markdown in TUI Add a lightweight assistant Markdown renderer for streaming and completed transcript rows, including inline bold, code/emphasis marker cleanup, pipe tables, adaptive row separators for wrapped tables, and plain-text output for selection metadata. Tests cover streaming markdown, completed rows, selection metadata, table wrapping, compact tables, inline markers, and width bounds with neutral fixtures. * Refine TUI Markdown table rendering Add outer borders for rendered assistant tables, add row separators for larger comparison tables even on wide terminals, and normalize common HTML line breaks inside table cells. Tests cover bordered tables, wide multi-row tables, compact tables, and HTML break normalization. * Fix Markdown inline parser edge cases Preserve literal stars, prevent code spans from inheriting bold state, and avoid inserting spaces before punctuation split by inline marker boundaries. Add regression checks for plain/selectable ANSI-free text and final-row done metadata on rendered Markdown rows. * Preserve coding literals in TUI Markdown Tighten inline delimiter parsing so code spans, Python dunder names, globs, and arithmetic stars stay literal while normal bold/emphasis still renders. Add regressions for the reviewed code-span, dunder, glob, and operator examples. |
||
|
|
fcfdb873d0 |
Polish main TUI surface (#168)
* Polish main TUI surface Simplify the empty chat surface, replace starter prompt chips with a cleaner brand state, move model and permission state into the composer frame, quiet the footer status, and improve composer word-editing keybindings. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tui Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: git diff --check * Polish autocomplete command palette * Polish TUI file mention picker * Address TUI review feedback * Keep autocomplete anchored above composer * Center autocomplete over chat viewport * Layer autocomplete overlays over transcript * Prefill argument commands from palette * Show argument hints for command completions * Polish TUI provider setup * Polish TUI picker and composer metadata * Add two-step mouse selection in TUI * Polish TUI mouse selection flows * Make provider wizard navigation test hermetic * Fix TUI review edge cases |