commit - 6c01aa8ef14a038f0203c91d985154e6a7c0de2e
commit + 7e10c7fa11b86603d677fd679e4c803a1632a037
blob - d14ac8deda24ca7dec52894d25ddaf79726f713e
blob + ca31895e34e61ec5a0c28053bf5db4bd23eeb7ed
--- docs/design/m7-curses.md
+++ docs/design/m7-curses.md
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.
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
## 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
(`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
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
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
- 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;
`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
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 && \
"$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
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"
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"
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
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"
$([ "$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
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
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
{ 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,
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
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
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
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
.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
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
/* 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 */
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)
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. */
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;
}
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;
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);
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;
}