Repository navigation
feat(tui): linear no-Bubble-Tea frontend for --plain and TERM=dumb (#4604 Track 1) - #4610
Merged
Merged
Conversation
…ridge via GetChatBridge (#4381) SetChatBridge (writer) holds activeChatBridgeMu, but the 9 reader sites in mcp.go/config.go historically read activeChatBridge under globalMu.RLock() — a different lock, so no happens-before edge existed between write and read. A workspace-switch/shutdown teardown goroutine calling SetChatBridge while the frontend MCP panel invokes ListMCPServers / ReconnectMCPServer / ForceReauthMCPServer / SetMCPServerEnabled / AddMCPServer / RemoveMCPServer / loadSessionScopedConfig / ApplyImpersonation is a data race under -race. Route every reader through GetChatBridge() (activeChatBridgeMu.RLock — the writer's lock), preserving the snapshot-once semantics of #458/#563. No ABBA risk: activeChatBridgeMu is only taken inside Set/Get and never wraps globalMu. Adds TestActiveChatBridgeRaceRepro: writer churn vs 4 reader goroutines; fails under -race on unfixed code, passes with the fix. Co-Authored-By: ggcode <noreply@ggcode.dev>
#4382) Adds TestInstanceHooksClearToEmptySilentlyLost — a characterization test documenting the verified bug: clearing instance-scope hooks (non-empty -> empty) is silently lost at write time (diffHooks zero-suppression, no reset fallback for hooks in instanceResetManagedKeys/NestedKeys, deepMergeYAMLMaps keeps unmentioned keys) and resurrects on reload (mergeHookConfig only replaces when len(instance) > 0). Assertions marked BUG must be inverted when the bug is fixed. See #4382 for the full evidence chain. Co-Authored-By: ggcode <noreply@ggcode.dev>
…ntract ForgetKey(key) -> GarbageCollect() removes the live memory file without archiving or writing a .tombstone, so ReadMemoryAsOf(key, now) time-travels back to stale .history content (found=true, "v1 content", tombstoneExists=false). Co-Authored-By: ggcode <noreply@ggcode.dev> Co-Authored-By: ggcode <noreply@ggcode.dev>
The 9-word English marker sniff before pattern matching silently dropped reactive FixHint injection for marker-free error shapes (signal: killed, no such file or directory, exit status 137, context deadline exceeded, unexpected EOF) that the learning path happily generates patterns for. MatchPattern is now authoritative and compiled via the shared regexCache (the gate path bypassed it with per-call regexp.Compile). - flipped the old gate-pinning test to the new semantics - zz_issue4605_test.go: 5 marker-free error shapes must match; success output stays quiet
…ailures (#4607) Co-Authored-By: ggcode <noreply@ggcode.dev>
…4604 Track 1) --plain previously only set NO_COLOR while still running the full Bubble Tea program (raw mode, frame refresh) - on dumb terminals, screen sessions, and limited SSH that is exactly the garbled output, repeated warning lines, and junk characters #4604 reports. New RunLinearREPL frontend (cmd/ggcode/plain.go): - line-buffered stdin loop, linear streaming print, no raw mode, no frames; rides the SAME interactive runtime core (agent, registry, sessions, config hot-swap) built before the fork point - routes at root.go before any TUI-shaped service (A2A, lanchat, Bubble Tea) starts; --plain or TERM=dumb/blank auto-select it - slash subset: /status /model /provider /endpoint /help /exit via the shared ActivateCurrentSelection + ApplyProviderToAgent funnel, persisted with cfg.Save() - approvals default Accept; GGCODE_PLAIN_APPROVE=deny rejects - ask_user unregistered: agent ends its turn and the next typed line is the answer (multi-turn is the native pause point) Verified: TERM=dumb smoke run (banner/notices//help//status real resolution//exit), same-value /model hot-switch persists; package tests pass (isDumbTerminal table, approval env matrix, exit-family loop semantics); full cmd/ggcode package PASS. Co-Authored-By: ggcode <noreply@ggcode.dev>
The HOME-directory warning was a full-screen Bubble Tea dialog (tui.ConfirmHomeDir) invoked in runE BEFORE the plain/TUI fork, so --plain and TERM=dumb sessions still got a raw-mode dialog garbled on dumb terminals. Add ConfirmHomeDirPlain - same i18n title/message, a plain y/N prompt on stdin, no tea program - and dispatch to it from the linear path. Co-Authored-By: ggcode <noreply@ggcode.dev>
Eight-case table via stdin pipe injection: y/yes/Y/padded continue, n/arbitrary/empty exit, EOF falls back to continue (mirrors the TUI dialog's failure path). Co-Authored-By: ggcode <noreply@ggcode.dev>
/provider --list [--all] list endpoints grouped by vendor with up to
10 models each (+N more hint); default shows
only keyed+modelled endpoints, --all includes
keyless/modelless with annotations; active
endpoint marked with a leading *
/provider --add-endpoint add an endpoint (name/type/url/model/context-
window/max-output/apikey) to the ACTIVE vendor,
protocol whitelist enforced, duplicate refused
/endpoint --show-models all models on the active endpoint (static
config merged with live discovery)
/endpoint --refresh-models force model refetch bypassing the TTL cache
/endpoint --add-model append a model to the ACTIVE endpoint
(dedup, optional --apikey update) - unlike
/provider --add-endpoint it never creates one
/endpoint --set-apikey overwrite the active endpoint's API key
provider: DiscoverModels factored into discoverModels(force); exported
ForceDiscoverModels + invalidateCachedDiscoveredModels so refresh drops
the cache entry instead of serving the 6h TTL copy.
Verified: live smoke on real config (grouped listing with caps, --all
annotations, full model list, unknown-option guard); 5 new table tests
(filter classes, cap+hint, active marker, validation guards before any
mutation); cmd/ggcode + internal/provider packages PASS.
Co-Authored-By: ggcode <noreply@ggcode.dev>
Live-cache eviction semantics the linear --refresh-models depends on: force refetches (2 network hits), and the fresh list is then served from cache with no further traffic. Co-Authored-By: ggcode <noreply@ggcode.dev>
The previous dispatch called tui.ConfirmHomeDir unconditionally and only then overwrote the result with the plain variant, so --plain sessions saw the full-screen TUI dialog AND a second y/N prompt after choosing continue. Select the variant before invoking either one. Verified in a real HOME launch (TERM=dumb): exactly one y/N confirm with the shared i18n copy, then the linear banner - no tea dialog. Co-Authored-By: ggcode <noreply@ggcode.dev>
The linear frontend printed a fixed mix of English strings and (for the HOME confirm) whatever the TUI i18n table held - mixed-language output by construction. - plain_i18n.go: self-contained en/zh-CN catalogs (60 keys) covering every user-visible string; plainTr lookup with English then raw-key fallback so output never blanks - /lang <en|zh-CN>: hot-switches the session language and persists to cfg.Language - the same GLOBAL field the TUI /lang writes, so all modes and future sessions inherit it (no per-session language store exists; consistent with TUI semantics) - startup derives plainLang from cfg.Language (empty = en) Verified: /lang zh-CN switches help/banners live; restart shows the Chinese banner (persisted); switched back to en. 4 new tests (catalog key parity, fallback chain, POSIX-locale derivation, nil-safe cfg accessor); cmd/ggcode package PASS. Co-Authored-By: ggcode <noreply@ggcode.dev>
Unsupported language and missing args refuse without mutating plainLang; valid targets switch en<->zh-CN in-memory. Companion to f5389ef. Co-Authored-By: ggcode <noreply@ggcode.dev>
New docs/guide/plain-mode.md covering the linear frontend: entry (--plain / TERM=dumb auto-select), the full slash-command table including the provider/endpoint management subcommands and /lang, --add-endpoint vs --add-model semantics, behavior differences (approval default, ask_user off, line-oriented HOME confirm, global language). --plain was previously absent from the CLI reference flag table despite existing. Co-Authored-By: ggcode <noreply@ggcode.dev>
) On a bypass/autopilot default the linear REPL inherited two cruise- mode injections that fight its line-by-line model: the goal-collection meta-instruction at turn start, and the end-of-turn strategist prompt that re-enters the loop after every plain answer - a text turn never ended the run. Agent.SuppressAutopilotInjection() short-circuits both injection sites (goal collection + strategist gate) while leaving the permission surface untouched - the mode stays bypass, plainApproval stays the approval decision point. No TUI/pipe/daemon behavior change: they never set the flag. Verified: plain Q&A turn ('1+1') answers and returns to the prompt in one pass, no GOAL line, no strategist banner; agent (38.6s) and cmd (22.4s) packages PASS. Co-Authored-By: ggcode <noreply@ggcode.dev>
Baseline (cruise injects exactly once via the asked latch), suppressed (zero injections, latch stays false so later re-checks see clean state), and the mode-intact guarantee (opt-out does not downgrade the permission surface; idempotent; nil-safe). Companion to b8b7d74. Co-Authored-By: ggcode <noreply@ggcode.dev>
Co-Authored-By: ggcode <noreply@ggcode.dev>
ToolCallDone prints the call line without a newline; the matching ToolResult (paired by tool-call ID) appends ' (ok)' or ' (error)'. Unmatched results are dropped (no printed line to attach to); calls whose result never arrives get their line closed by Done so output does not run on. agent-unavailable message joins the i18n catalogs. Verified live: '[tool] glob (ok)' / multi-tool turns; cmd/ggcode package PASS. Co-Authored-By: ggcode <noreply@ggcode.dev>
# Conflicts: # internal/agent/ratchet_reactive.go # internal/config/zz_hooks_clear_repro_test.go
plainStreamPrinter extracted from runLinearTurn so the tool-call status contract is unit-testable: (ok)/(error) suffixes pair by call ID, ghost results drop, Done closes one tail newline. The tests caught a real leak: Done drained only ONE pending call (break in the loop), so a second unterminated call's stale ID could pair with a later turn's result. Done now clears the whole map while printing exactly one tail newline. cmd/ggcode PASS. Co-Authored-By: ggcode <noreply@ggcode.dev>
Co-Authored-By: ggcode <noreply@ggcode.dev>
…4604) Real-world event order broke the (ok)/(error) suffixes: provider streams emit StreamEventDone per iteration - and on OpenAI-compatible providers each carries Usage - so treating any Done as the turn end cleared the pending map right before the ToolResult arrived between iterations. The suffix never printed. StreamEventDone is now inert in the printer; plainCloseTurn runs after RunStream returns: one tail-closing newline (max) + separator. Regression test pins the interleaved order (Done between call and result, with Usage - exactly what zai/glm emits). Verified live: single-tool turns print '[tool] glob (ok)' stably; multi-tool turns print (ok)/(error) suffixes. cmd/ggcode PASS. Co-Authored-By: ggcode <noreply@ggcode.dev>
Plain sessions were memory-only: SetPersistHandler is wired by the TUI/daemon only, so every plain conversation was lost on exit. --persist-session attaches the same per-message JSONL append (via wirePlainPersist: NewSession snapshot + AppendMessageToDisk, best- effort - failures log, never block the REPL). Write-only by design: the JSONL is deliberately NOT indexed (no store.Save), so auto-resume and the picker never pick it up and the next plain start stays fresh; explicit --resume <id> can still read the file. Verified live: flag on creates '<ts>-<id>.jsonl' with the turn, prints the session id; flag off creates nothing; a follow-up plain start does not auto-load it. cmd/ggcode PASS. Co-Authored-By: ggcode <noreply@ggcode.dev>
Co-Authored-By: ggcode <noreply@ggcode.dev>
Co-Authored-By: ggcode <noreply@ggcode.dev>
ggaidev
approved these changes
Oct 30, 2026
ggaidev
left a comment
Collaborator
There was a problem hiding this comment.
Verified plain frontend track: linear REPL, slash commands, i18n, autopilot injection opt-out, tool status pairing (RunStream boundary fix), --persist-session write-only persistence. Live-tested each on bin build; cmd/agent packages PASS.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Track 1 落地:完全无 Bubble Tea 的线性前端
方案(与维护者讨论定稿)
不是把 tea 降级渲染,而是新建第三种前端:行缓冲 stdin + 线性流式打印,零 raw mode、零帧刷新、零 alternate screen。复用 root.go 交互构建段(agent/registry/session/config 热切换全在),在 A2A/lanchat/tea 启动前分流。
按设计实现的四点
ggcode>提示符——多轮对话即原生停顿点分流
--plain或 TERM=dumb/空 → RunLinearREPL(root.go L907 前分叉,TUI 专属服务零启动)验证
Track 2(每秒重复警告 redraw bug)另案处理,仍欢迎报告者补充环境信息。
closes #4604
Co-Authored-By: ggcode noreply@ggcode.dev