Commit Diff


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;
 	}