commit 7e10c7fa11b86603d677fd679e4c803a1632a037 from: Isaac Meerleo date: Tue Jul 21 17:49:38 2026 UTC Improve terminal UI and remove startup warnings Reflect a model-picker selection locally so the first repaint does not wait for the coordinator's authoritative configuration snapshot. commit - 6c01aa8ef14a038f0203c91d985154e6a7c0de2e commit + 7e10c7fa11b86603d677fd679e4c803a1632a037 blob - d14ac8deda24ca7dec52894d25ddaf79726f713e blob + ca31895e34e61ec5a0c28053bf5db4bd23eeb7ed --- docs/design/m7-curses.md +++ docs/design/m7-curses.md @@ -2,7 +2,7 @@ Milestone M7 builds the full-screen terminal front end (behavior.md §2.1): scrolling transcript, modal vi-like input editor, status bar, -markdown and diff rendering, mouse-wheel scrollback, bracketed paste, +markdown and diff rendering, selectable terminal text, bracketed paste, the external editor, the conversation drawer, and the model picker. It is by far the largest milestone, so it is split into testable pieces that land green one at a time. @@ -128,10 +128,9 @@ draws the pure welcome layout beneath drawer/model-pic reuse the empty input row and are centered over stable status-field regions; they add no geometry. Every route that can replace editor content (typing, paste, completion, history, Queue edit, and external compose) passes through -one observer, so dismissal is one-way and local to fugu-tty. The experimental -Claude subscription warning stays on stderr in line/print mode but is not -inserted as a curses transcript item; the durable manuals, `fugu -n`, and -profile failure diagnostics retain the risk disclosure. +one observer, so dismissal is one-way and local to fugu-tty. Experimental +subscription profiles do not emit a startup warning in any mode; the durable +manuals, `fugu -n`, and profile failure diagnostics retain the risk disclosure. `Ctrl-L` records a reflow-stable display boundary and forces a physical clear. The tail view starts after that boundary, while scrolling up can still reach earlier lines. It sends no coordinator message and does not alter conversation @@ -188,8 +187,9 @@ provider active for the session and cold-starts the pr ## Status bar and the context meter The status bar shows idle/busy/summarizing/retry (retry naming the -attempt and pending delay), the input mode, and — once a Lead usage report -commits — `Context current/max N%` against the effective window (`context_limit`, +attempt and pending delay), the input mode, the active model, and — once a +Lead usage report commits — `Context current/max N%` against the effective +window (`context_limit`, else provider-reported, else the compiled table, else unknown). `current` combines the main conversation's cache and session tokens without ephemeral subagent usage. Counts of at least 1,000 are rounded to the nearest `k`. At or @@ -203,7 +203,7 @@ Built and green (both builds): the render/edit/diff/in (`markdown.c`, `editor.c`, `diff.c`, `term_input.c`, each unit-tested), the coordinator↔tty render/input protocol and terminal-mode driver, and the curses front end (`ui.c`) — layout, markdown/diff/tool/error -rendering, scrollback (including xterm basic/SGR mouse-wheel reports), +rendering, scrollback, native terminal text selection, status bar with the context gauge, bell, bracketed paste, resize, modal editing, completion, history, and the message queue. A pty driver exercises the whole path end to end. blob - 95e57a866d9ee863fd031631e45cc6b7811a5124 blob + 392f8261b354794a562b0e3d7f90fb19820ec2a2 --- docs/release-live-checks.md +++ docs/release-live-checks.md @@ -61,16 +61,16 @@ env FUGU_LIVE_EXPECT_PROVIDER=codex \ make live-check ``` -Either subscription opt-in checks the experimental startup warning, exact -profile identifier, and redacted token, and extracts inline tokens only inside -a private leak-scanning subprocess. For a Codex audit, that subprocess also -decodes each JWT payload into a private temporary file, extracts the one -bounded literal `chatgpt_account_id`, and adds it to the fixed-pattern scan. It -refuses to report success when no account id can be derived. The token and, -when applicable, account id are checked against terminal artifacts, the -isolated Journal, and the delimited syslog slice without entering argv, the -environment, or command output. Temporary evidence is removed when the target -exits. +Either subscription opt-in checks that startup omits the experimental warning, +that the dry run contains the exact profile identifier and redacted token, and +extracts inline tokens only inside a private leak-scanning subprocess. For a +Codex audit, that subprocess also decodes each JWT payload into a private +temporary file, extracts the one bounded literal `chatgpt_account_id`, and adds +it to the fixed-pattern scan. It refuses to report success when no account id +can be derived. The token and, when applicable, account id are checked against +terminal artifacts, the isolated Journal, and the delimited syslog slice +without entering argv, the environment, or command output. Temporary evidence +is removed when the target exits. The Codex Tool check also isolates the Journal created by that invocation and requires a committed `responses-continuity` Provider-state envelope for the blob - cf2924fc7f77e706f29f4f9754b28659901a1bc3 blob + 9348167eadf07acfc2c3b0dae086684318883e6d --- handoff/behavior.md +++ handoff/behavior.md @@ -122,14 +122,15 @@ Transcript behavior: line. Presentation only — it never enters the provider projection or the journal's replayable context (I14). - Errors are visually distinct from assistant output. -- Scrollback: page keys and the mouse wheel always; while scrolled away +- Scrollback: page keys always; while scrolled away from the tail, the status bar says so and how to return; new output does not yank the view while the Owner is reading history unless they were at the tail. Status bar: an idle/busy indicator (distinct states for a normal turn, a summarization, and an automatic retry — the retry state shows -attempt count and pending delay, §8), the current input mode, and — +attempt count and pending delay, §8), the current input mode, the active +model, and — once known — `Context current/max N%` against the effective window (§3, `/context`). `current` is the latest committed Lead Generation's active context: Anthropic and Claude add uncached input, output, cache-read, and @@ -149,8 +150,8 @@ Input editing is modal, vi-like, starting in insert mo - Either mode: `Enter` submits; `Tab`/`Shift-Tab` cycle slash-command completions when the line starts with `/` (built-ins first, then - skills alphabetically); `PgUp`/`PgDn` and the mouse wheel scroll the - transcript; `Up`/`Down` recall prompt history (this session); `Ctrl-L` + skills alphabetically); `PgUp`/`PgDn` scroll the transcript; + `Up`/`Down` recall prompt history (this session); `Ctrl-L` clears the transcript viewport while retaining earlier lines in scrollback and without resetting conversation context, current input, history, or queued messages; @@ -164,11 +165,10 @@ Input editing is modal, vi-like, starting in insert mo `v` composes in the external editor (below); `m` opens the model picker without editing the prompt. -On taking the terminal the curses front end enables xterm basic mouse -tracking with SGR coordinates and routes wheel reports to transcript -scrollback; click and release reports are consumed without editing input. -It disables mouse reporting on every survivable teardown and while an -external editor owns the terminal. `Up`/`Down` remain prompt-history keys. +On taking the terminal the curses front end explicitly disables xterm mouse +tracking so the terminal emulator retains ordinary drag selection and copy. +Transcript scrolling remains available through `PgUp`/`PgDn` and normal-mode +`J`/`K`. `Up`/`Down` remain prompt-history keys. Paste is atomic: on taking the terminal the curses front end enables xterm bracketed-paste mode (and disables it on every teardown path blob - b367e04fbe82932ba0247ab8813a1aeeb563967e blob + 373d92c0db5340b7cddd0c43bfeb52e343512679 --- regress/print/run.sh +++ regress/print/run.sh @@ -374,9 +374,8 @@ write_claude_conf run_claude_fugu -p "use the Claude subscription tool" -o ndjson ok "claude-subscription: complete tool round exits 0" \ $([ "$rc" -eq 0 ]; echo $?) -grep -q 'experimental Claude subscription profile claude-code-2.1.185' \ - "$dir/errout" -ok "claude-subscription: print mode retains the startup warning" $? +! grep -q 'experimental Claude subscription profile' "$dir/errout" +ok "claude-subscription: print mode omits startup warning" $? "$obj/jsoncheck" ndjson "$dir/out" ok "claude-subscription: canonical tool events retain NDJSON shape" $? grep -q '^2$' "$dir/reqcount" 2>/dev/null && \ @@ -466,9 +465,8 @@ n=$(grep -c '"type":"usage","input_tokens":100,"output "$dir/out") ok "codex subscription: cached input remains inside the input total" \ $([ "$n" -eq 2 ]; echo $?) -grep -q 'experimental Codex subscription profile chatgpt-codex-2026-07-17' \ - "$dir/errout" -ok "codex subscription: print mode retains the startup warning" $? +! grep -q 'experimental Codex subscription profile' "$dir/errout" +ok "codex subscription: print mode omits startup warning" $? journal=$(ls "$sessions"/*.ndjson 2>/dev/null | head -1) sid=$(basename "$journal" .ndjson) test -n "$sid" blob - 4d2a2af3a02467f06c7b1e904286a8bfa3abb2ca blob + 1033fdbbd5d1b5cd1324d258770ff1b1875c85e7 --- regress/turn/run.sh +++ regress/turn/run.sh @@ -1950,11 +1950,11 @@ if [ -x "$obj/ptydrive" ]; then rc=$? ok "curses: clean exit on /quit" $([ "$rc" -eq 0 ]; echo $?) esc=$(printf '\033') - grep -a -Fq "${esc}[?1000h" "$dir/cursesout" && - grep -a -Fq "${esc}[?1006h" "$dir/cursesout" && + ! grep -a -Fq "${esc}[?1000h" "$dir/cursesout" && + ! grep -a -Fq "${esc}[?1006h" "$dir/cursesout" && grep -a -Fq "${esc}[?1006l" "$dir/cursesout" && grep -a -Fq "${esc}[?1000l" "$dir/cursesout" - ok "curses: mouse reporting is enabled and restored" $? + ok "curses: native text selection remains available" $? grep -q "OWNER-BLOCK-MARKER" "$dir/reqout" 2>/dev/null ok "curses: the typed prompt reached the provider" $? grep -a -Fq "OWNER-BLOCK-MARKER" "$dir/cursesout" @@ -1964,6 +1964,8 @@ if [ -x "$obj/ptydrive" ]; then grep -a -Fq "git ssh://anonymous@got.fugu.farm" \ "$dir/welcome-stage.0" ok "curses: empty transcript shows the welcome before input" $? + grep -a -Fq "Model claude-sonnet-4-6" "$dir/welcome-stage.0" + ok "curses: status shows the configured model" $? grep -a -Fq "agent state" "$dir/welcome-stage.0" && grep -a -Fq "input mode" "$dir/welcome-stage.0" && grep -a -Fq "context used / limit" "$dir/welcome-stage.0" @@ -1984,8 +1986,9 @@ if [ -x "$obj/ptydrive" ]; then ok "curses: model picker overlays the undismissed welcome" $? grep -a -Fq "Fugu - Japanese for pufferfish" "$dir/welcome-stage.5" && grep -a -Fq "context used / limit" "$dir/welcome-stage.5" && + grep -a -Fq "Model stub-model-a" "$dir/welcome-stage.5" && ! grep -a -Fq "model: stub-model-a" "$dir/welcome-stage.5" - ok "curses: selecting from m restores welcome without scrollback" $? + ok "curses: selection updates the model and restores welcome" $? picker_stage='' for stage in 6 7; do if grep -a -Fq "stub-model-b" "$dir/welcome-stage.$stage"; then @@ -2021,10 +2024,9 @@ else fail "curses: ptydrive helper not built" fi -# --- 28w: subscription warnings stay outside the curses transcript --- -# The accepted risk disclosure remains in line/print mode, -n, manuals, and -# profile failures. Both an active and an available named Claude route must -# nevertheless leave the empty curses transcript available to the welcome. +# --- 28w: subscription profiles preserve the curses welcome --- +# Both an active and an available named Claude route must leave the empty +# curses transcript available to the welcome. if [ -x "$obj/ptydrive" ]; then for profile in default named; do rm -rf "$HOME/.fugu" @@ -2058,13 +2060,13 @@ EOF $([ "$rc" -eq 0 ]; echo $?) grep -a -Fq "experimental Claude subscription profile" \ "$dir/profileout" - ok "curses: $profile subscription warning is not transcript" \ + ok "curses: $profile subscription warning is absent" \ $([ $? -ne 0 ]; echo $?) grep -a -Fq "Fugu - Japanese for pufferfish" "$dir/profileout" ok "curses: $profile subscription profile retains welcome" $? done else - fail "curses profile warning: ptydrive helper not built" + fail "curses subscription profile: ptydrive helper not built" fi # --- 28a: the full skill palette crosses the imsg payload boundary --- blob - 679cfc582966b2a7c1f4c4b94c48c556bb54f1ea blob + c6d865d3ff58664727ca91cbb511165a9f16c526 --- regress/ui_config/ui_config_test.c +++ regress/ui_config/ui_config_test.c @@ -225,6 +225,8 @@ test_inclusive_boundaries(void) NULL, 0, &snap) == UI_CONFIG_RX_COMPLETE); CHECK(ui_config_snapshot_settings(snap)->window == FUGU_MODEL_WINDOW_MAX); + CHECK(strlen(ui_config_snapshot_settings(snap)->model) == + FUGU_MODEL_ID_MAX); CHECK(strlen(ui_config_snapshot_skill(snap, 0)) == FUGU_SKILL_NAME_MAX - 1); ui_config_snapshot_free(snap); blob - 700421220e2a3822ce1396669b4f4dce9a7609e1 blob + 5b0bba7b4cc7adf49a584a8b783841f9413e29a7 --- scripts/live-check +++ scripts/live-check @@ -680,9 +680,9 @@ credentialed() grep -q 'live subscription tool ok' \ "$artifacts/subscription-tool.out" ok "subscription profile completes after its Tool result" $? - grep -Fq "$subscription_marker" \ + ! grep -Fq "$subscription_marker" \ "$artifacts/subscription-tool.err" - ok "subscription startup warns that the profile is experimental" $? + ok "subscription startup omits the experimental warning" $? if [ "$expect_provider" = codex ]; then find "$journals" -type f -name '*.ndjson' -print \ 2>/dev/null | sort >"$subscription_after" blob - c121936fd24e3eb1237528dc5cba5d5fded4e091 blob + c226da780e4deb54a67e7e58b2d7b835acc86e60 --- src/fugu/coord.c +++ src/fugu/coord.c @@ -6881,19 +6881,6 @@ static const struct { { ROLE_EDIT, "fugu-editor", 1 }, /* the editor needs the tty */ }; -static int -provider_type_configured(const struct fugu_conf *cf, int type) -{ - const struct fugu_provider *fp; - - if (cf->provider_type == type) - return (1); - TAILQ_FOREACH(fp, &cf->providers, entry) - if (fp->type == type) - return (1); - return (0); -} - int coord_run(struct fugu_conf *cf, const char *prompt, size_t promptlen, size_t dispatchlen, const char *resume_id, const char *argv0, @@ -7088,24 +7075,6 @@ coord_run(struct fugu_conf *cf, const char *prompt, si turn_jobs.artifacts_commit = turn_artifacts_commit; turn_jobs.artifacts_abort = turn_artifacts_abort; c.turn = turn_txn_new(c.journal, &seed, &turn_jobs); - if (provider_type_configured(cf, PROVIDER_CLAUDE) && !c.ui) { - static const char warning[] = - "experimental Claude subscription profile " - CLAUDE_SUB_PROFILE_ID " (snapshot " CLAUDE_SUB_PROFILE_DATE - "): unofficial; it may stop working " - "or trigger account enforcement"; - - fprintf(stderr, "fugu: warning: %s\n", warning); - } - if (provider_type_configured(cf, PROVIDER_CODEX) && !c.ui) { - const struct codex_sub_profile *profile = codex_sub_profile(); - - fprintf(stderr, "fugu: warning: experimental Codex subscription " - "profile %s (snapshot %s): unofficial; it may stop working " - "or trigger account enforcement\n", profile->id, - profile->snapshot_date); - } - if (prompt != NULL) { signal_set(&c.sigint_ev, SIGINT, cancel_signal, &c); if (signal_add(&c.sigint_ev, NULL) == -1) blob - 7ba1ba3fb769cb0011587f58c6cb41eef96caec0 blob + 2041e5d547ead5c1b66dee703aac759d58e5889b --- src/fugu/fugu.1 +++ src/fugu/fugu.1 @@ -149,10 +149,8 @@ the invocation; clearing input, resizing, or does not restore it. The welcome is presentation only and is never sent to a Provider or written to the Journal. -An experimental subscription profile continues to print its startup warning -on standard error in line and print modes. -The warning is omitted from the curses transcript; explicit risk disclosure -remains in this manual, the +Experimental subscription profiles do not print a startup warning. +Explicit risk disclosure remains in this manual, the .Fl n dry-run output, and profile-specific failure diagnostics. Assistant markdown headings, lists, fenced code, emphasis, and inline code @@ -162,7 +160,8 @@ input row; every wrapped row fills the current transcr Monochrome terminals use bold reverse video. Tool calls, errors, and edit diffs are visually distinct. The status bar identifies idle, active-turn, summarization, and retry states, -and shows +the current input mode, and the active model. +It shows .Dq Context current/max N% when known. The current count combines cache and session tokens from the main @@ -188,8 +187,6 @@ Cycle built-in slash-command and skill completions whe Recall prompt history. .It Ic PageUp , PageDown Scroll the transcript. -.It Ic Mouse wheel -Scroll the transcript one line per report without changing prompt history. .It Ic Ctrl-L Clear the transcript viewport while retaining earlier lines in scrollback. Conversation context, current input, prompt history, and queued messages are @@ -203,6 +200,15 @@ Enter normal mode. .El . .Pp +Mouse tracking is disabled so the terminal emulator's ordinary drag selection +and copy remain available. +Use +.Ic PageUp , PageDown +or normal-mode +.Ic J , K +to scroll the transcript. +. +.Pp Normal mode supports the usual .Ic h , l , 0 , $ , ^ , w , b , e motions; blob - f95164804fde4d69524c372dd3204fca381c4637 blob + b1c940dda1cd419975d768612420de70c9b2a933 --- src/fugu/proto.h +++ src/fugu/proto.h @@ -400,7 +400,7 @@ struct ui_config { int ascii; /* transliterate to ASCII */ int notify; /* bell on completion */ int64_t window; /* effective context window, 0 unknown */ - char model[64]; /* active model id */ + char model[FUGU_MODEL_ID_MAX + 1]; /* active model id */ }; /* Status-bar state (behavior.md 2.1). */ blob - c3a532af8c47fa980b25808e997af51c1e3d9e80 blob + 855c795675d38edeb497206165058d744fdbe679 --- src/fugu-tty/ui.c +++ src/fugu-tty/ui.c @@ -85,7 +85,7 @@ struct ui { /* configuration from the coordinator */ int ascii, notify; int64_t window; - char model[64]; + char model[FUGU_MODEL_ID_MAX + 1]; struct ui_config_rx *config_rx; /* status */ @@ -154,12 +154,12 @@ struct status_fields { struct welcome_field field[WELCOME_CAPTIONS_MAX]; }; -/* Terminal modes owned by fugu-tty. Disable in reverse order so every - * survivable teardown and external-editor handoff restores the terminal. */ +/* Terminal modes owned by fugu-tty. Mouse tracking is deliberately kept + * off so the terminal emulator retains ordinary drag selection. */ static const char terminal_modes_on[] = - "\033[?2004h\033[?1000h\033[?1006h"; + "\033[?1006l\033[?1000l\033[?2004h"; static const char terminal_modes_off[] = - "\033[?1006l\033[?1000l\033[?2004l"; + "\033[?2004l"; static void terminal_modes(int enable) @@ -584,16 +584,20 @@ status_text(const struct ui *u, char *state, size_t st static void draw_status(struct ui *u, int row, int expanded, struct status_fields *fields) { - char state[64], mode[16], left[128], right[160], context[96]; - char q[32]; + char state[64], mode[16], left[160], right[160], context[96]; + char model[FUGU_MODEL_ID_MAX + sizeof("Model ")], q[32]; size_t len; - int attr, col, context_value, modelen, n, rightcol, statelen, warn; + int attr, col, context_value, limit, modelen, n, rightcol; + int right_visible, statelen, warn; memset(fields, 0, sizeof(*fields)); if (status_text(u, state, sizeof(state), mode, sizeof(mode)) == -1) return; statelen = (int)strlen(state); modelen = (int)strlen(mode); + n = snprintf(model, sizeof(model), "Model %s", u->model); + if (u->model[0] == '\0' || n < 0 || (size_t)n >= sizeof(model)) + model[0] = '\0'; right[0] = '\0'; context_value = 0; /* The welcome explains the live field before the first Generation. */ @@ -633,6 +637,12 @@ draw_status(struct ui *u, int row, int expanded, struc fields->field[WELCOME_INPUT_MODE].width = modelen < 10 ? modelen : 10; } + col = 28; + if (model[0] != '\0' && col < rightcol - 2) { + move(row, col); + (void)draw_utf8_cells(model, strlen(model), + rightcol - col - 2); + } mvaddnstr(row, rightcol, right, (int)strlen(right)); if (context_value) { fields->field[WELCOME_CONTEXT_LIMIT].col = rightcol; @@ -655,8 +665,16 @@ draw_status(struct ui *u, int row, int expanded, struc } if (u->cols > 1) mvaddnstr(row, 1, left, u->cols - 1); - if (right[0] != '\0' && rightcol > (int)strlen(left) + 2) + right_visible = right[0] != '\0' && + rightcol > (int)strlen(left) + 2; + if (right_visible) mvaddnstr(row, rightcol, right, u->cols - rightcol); + col = (int)strlen(left) + 3; + limit = right_visible ? rightcol - 2 : u->cols - 1; + if (model[0] != '\0' && col < limit) { + move(row, col); + (void)draw_utf8_cells(model, strlen(model), limit - col); + } len = strlen(state); fields->field[WELCOME_AGENT_STATE].col = 1; @@ -668,8 +686,7 @@ draw_status(struct ui *u, int row, int expanded, struc fields->field[WELCOME_INPUT_MODE].width = modelen < u->cols - col ? modelen : u->cols - col; } - if (context_value && right[0] != '\0' && - rightcol > (int)strlen(left) + 2) { + if (context_value && right_visible) { fields->field[WELCOME_CONTEXT_LIMIT].col = rightcol; fields->field[WELCOME_CONTEXT_LIMIT].width = (int)strlen(right); @@ -1252,6 +1269,9 @@ pick_key(struct ui *u, enum ed_key k, uint32_t cp) imsgev_send(&u->w->iev, FUGU_IMSG_UI_SELECT_MODEL, 0, b.data, b.len); buf_free(&b); + /* The coordinator's config snapshot confirms this selection. */ + strlcpy(u->model, m->id, sizeof(u->model)); + u->window = m->context_window; pick_close(u); break; }