commit - 4bf2f54fab51f79f5a7f751e2d342775a53fd316
commit + e61bc0a1de536144d337a2ad8ed4bfc0163d0d5c
blob - 4763e2d1c582f79c0fc3fdf32bea3a3e753638cd
blob + b276003cb91cfc767e9147af854ff9031fdd8fab
--- 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, scrollback, 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.
+markdown and diff rendering, mouse-wheel scrollback, 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.
## Where the UI runs, and why
- `T_CONFIG` — ascii, notify, mode label, effective window, model id.
- `T_USER` — echo a submitted user line into the transcript.
- `T_TEXT` — an assistant text delta, appended to the live block.
-- `T_TOOL` — a tool call line (name + one-line summary).
+- `T_TOOL` — an indented tool call line: name plus a bounded, control-safe
+ allowlist of useful arguments (paths, search patterns, line windows, and
+ similar action identifiers; never bulk content, HTTP headers/bodies, edit
+ replacements, or subagent prompts).
- `T_DIFF` — a formatted diff hunk (presentation only).
- `T_ERROR` — an error line, visually distinct.
- `T_STATUS` — status-bar state: idle/busy/summarizing/retry (with
- attempt and delay), plus the usage counters and percent-of-window.
+ attempt and delay), plus the committed Lead context count.
- `T_DONE` — turn (or summarization) complete; fugu-tty bells if
`notify`.
- `T_CLEARED` — drop the transcript (after `/clear`).
## 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 usage report
-exists — input/output and cache token counts plus percent-of-window
-against the effective window (`context_limit`, else provider-reported,
-else the compiled table, else unknown). At or above 80% the field is a
-warning attribute and the first crossing emits a one-line `/compact`
-notice. On completion the bell fires unless `notify no`. Line and print
-modes never bell.
+attempt and pending delay), the input mode, 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
+above 80% the field is a warning attribute and the first crossing emits a
+one-line `/compact` notice. On completion the bell fires unless `notify no`.
+Line and print modes never bell.
## Status
(`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, status bar with the context gauge, bell,
+rendering, scrollback (including xterm basic/SGR mouse-wheel reports),
+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 - 97cd41f060170cfc5dcf29672ebc19cc1040ae5e
blob + 809b36c6cdf7bd16c4c795a8cf38403c4b5dfd31
--- handoff/behavior.md
+++ handoff/behavior.md
current assistant block.
- Assistant text renders markdown: headings, fenced code blocks,
bullet lists, inline code, bold/italic — via curses attributes.
- Unmatched markers render literally. User text renders plain.
-- Tool calls appear as they run, visually distinct from prose (the
- reference used a `tool:`-prefixed line per call; equivalent clarity
- is required, exact dress free).
+ Unmatched markers render literally. Ordinary assistant text is light
+ grey when color is available; markdown-specific colors take precedence.
+ User text renders plain.
+- Tool calls appear as they run, indented and visually distinct from prose.
+ Each `tool:`-prefixed line includes a bounded, control-safe allowlist of
+ useful arguments: for example, a read's path and line window or a grep's
+ pattern and path. Bulk or secret-prone values such as file content, edit
+ replacements, HTTP headers/bodies, and subagent prompts are omitted.
- A successful `edit` or `multi_edit` (one before/after diff across
all applied pairs), or a `write` replacing an existing file,
additionally renders as a unified-diff hunk — hunk header, a few
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 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.
+- Scrollback: page keys and the mouse wheel 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 —
-once known — the running token usage for the session (input/output
-and, when the provider reports them, cache-read/cache-write counts).
-Once a usage report exists the bar also shows percent-of-window,
-computed from the last report's input tokens against the effective
-window (§3, /context); at or above 80% the field renders in a warning
-attribute, and the first crossing emits a one-line notice naming
-`/compact`.
+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
+cache-created tokens; OpenAI adds prompt and completion tokens because its
+cached-token count is already a subset of prompt tokens. Ephemeral subagent
+usage is excluded from this display, while remaining part of machine-readable
+whole-Turn usage. Counts of at least 1,000 are rounded to the nearest thousand
+and shown with `k`. At or above 80% the field renders in a warning attribute,
+and the first crossing emits a one-line notice naming `/compact`.
When a turn or summarization completes, the curses interface writes
the terminal bell (BEL) so multiplexers and unfocused terminals can
- Either mode: `Enter` submits; `Tab`/`Shift-Tab` cycle slash-command
completions when the line starts with `/` (built-ins first, then
- skills alphabetically); `PgUp`/`PgDn` scroll the transcript;
- `Up`/`Down` recall prompt history (this session); `Ctrl-L` repaints;
+ skills alphabetically); `PgUp`/`PgDn` and the mouse wheel scroll the
+ transcript; `Up`/`Down` recall prompt history (this session); `Ctrl-L` repaints;
`Ctrl-C` clears a non-empty line, quits on an empty one; `Ctrl-D`
quits on an empty line; `Esc` enters normal mode.
- Normal mode: `h l 0 $ ^ w b e` motions; `i a A I O` (and `o` is
`J`/`K` scroll the transcript one line (allowed even mid-turn);
`v` composes in the external editor (below).
+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.
+
Paste is atomic: on taking the terminal the curses front end enables
xterm bracketed-paste mode (and disables it on every teardown path
the process survives — pledge-violation death leaves the terminal
The conversation has one canonical in-memory shape; each route translates at
its boundary. Direct routes provide model-listing calls to the picker; a
subscription route contributes its configured model and does not issue a
-listing call. Usage (input/output tokens, plus cache-read/cache-write when
-reported) is surfaced to the UI. Provider errors reach the Owner without
-revealing credentials. A turn that errors mid-stream obeys §7.
+listing call. Lead usage becomes the UI's provider-aware context count;
+subagent usage remains only in whole-Turn accounting. Provider errors reach
+the Owner without revealing credentials. A turn that errors mid-stream obeys
+§7.
Prompt caching: on the direct anthropic protocol every request — lead and
subagent — marks cache breakpoints: `cache_control` ephemeral on the
blob - 6dac0e8d6065a92ad1ecb31eb628cc8e20be4180
blob + 30d7e413e30c776a622a0944b25a44a210a0eb2d
--- handoff/verification.md
+++ handoff/verification.md
included; the openai builder emits no cache fields.
- **Context meter:** the effective-window resolution order
exercised (`context_limit`, provider-reported, compiled table,
- unknown); `/context` leaves the journal byte-identical; the 80%
+ unknown); `/context` leaves the journal byte-identical; the status count
+ combines Lead cache and session tokens without subagent usage; the 80%
gauge threshold and its single first-crossing notice.
- **Notify:** bell emitted on turn completion with `notify yes`,
absent with `notify no`, never in line or print mode.
blob - fad71189bc0121f1a51aae272ac685ff851074a8
blob + ef74eca62ff8762a9ec7a136a3c140102c93357b
--- regress/turn/run.sh
+++ regress/turn/run.sh
# the tool's output was fed back to the provider in the 2nd request
grep -q "tool-ran-ok" "$dir/reqout"
ok "tool loop: the tool result reached the next request" $?
+grep -Fq 'fugu: shell command="if ( exec 4>&3 ) 2>/dev/null; then n=1; else n=0; fi; echo FD3=$n; echo tool-ran-ok"' \
+ "$dir/errout"
+ok "tool loop: progress shows the bounded command arguments" $?
# the coordinator control fd did not leak into the tool subprocess (I10):
# the computed FD3=0 marks it closed; FD3=1 would mean a leak
grep -q "FD3=0" "$dir/reqout"
# the request, the journal commits it) and /quit must exit cleanly.
if [ -x "$obj/ptydrive" ]; then
rm -rf "$HOME/.fugu"
- start_stub "OK"
+ start_stub "CONTEXT"
write_conf "$stubport"
printf 'hello\r/quit\r' | TERM=vt100 HOME=$HOME \
FUGU_CONF=$dir/fugu.conf FUGU_LIBEXEC=$dir/libexec/fugu \
FUGU_CA_FILE=$dir/cert.pem FUGU_RETRY_BASE_MS=50 \
- "$obj/ptydrive" "$fugu" >/dev/null 2>&1
+ "$obj/ptydrive" "$fugu" >"$dir/cursesout" 2>&1
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}[?1006l" "$dir/cursesout" &&
+ grep -a -Fq "${esc}[?1000l" "$dir/cursesout"
+ ok "curses: mouse reporting is enabled and restored" $?
grep -q "hello" "$dir/reqout" 2>/dev/null
ok "curses: the typed prompt reached the provider" $?
+ grep -a -Fq "Context 46k/200k 23%" "$dir/cursesout"
+ ok "curses: status shows Lead cache plus session context" $?
n=$(grep -c '"outcome":"ok"' "$sessions"/*.ndjson 2>/dev/null || echo 0)
ok "curses: the turn committed to the journal" \
$([ "$n" -ge 1 ]; echo $?)
blob - 9155028c505545b78aa5190d9a4026fe25079c59
blob + 280083f566a5ef0fb0dfe2f33789f7df045e9868
--- regress/turn/stub.c
+++ regress/turn/stub.c
* Hermetic Anthropic Messages stub for the turn suite: over TLS, it
* checks the request carried the x-api-key header, then serves a
* canned chunked SSE stream chosen per accepted connection (in argv
- * order). Scenarios: OK (a two-delta text reply), ERROR (an SSE
+ * order). Scenarios: OK (a two-delta text reply), CONTEXT (a reply with
+ * distinct uncached/cache-read/cache-created/output usage), ERROR (an SSE
* error event), HTTP500 (a non-2xx before any stream), HTTP401 (an
* Anthropic-style JSON error body), RETRY503/RETRY429 (retryable
* statuses, 429 carrying Retry-After: 2), EMPTY (a well-formed
"event: message_stop\n"
"data: {\"type\":\"message_stop\"}\n\n";
+/* Distinct counters make the context sum observable: 11k+23k+5k+7k=46k. */
+static const char sse_context[] =
+"event: message_start\n"
+"data: {\"type\":\"message_start\",\"message\":{\"usage\":"
+ "{\"input_tokens\":11000,\"output_tokens\":0,"
+ "\"cache_read_input_tokens\":23000,"
+ "\"cache_creation_input_tokens\":5000}}}\n\n"
+"event: content_block_start\n"
+"data: {\"type\":\"content_block_start\",\"index\":0,"
+ "\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n"
+"event: content_block_delta\n"
+"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":"
+ "{\"type\":\"text_delta\",\"text\":\"Context reply.\"}}\n\n"
+"event: content_block_stop\n"
+"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n"
+"event: message_delta\n"
+"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":"
+ "\"end_turn\"},\"usage\":{\"output_tokens\":7000}}\n\n"
+"event: message_stop\n"
+"data: {\"type\":\"message_stop\"}\n\n";
+
static const char sse_error[] =
"event: error\n"
"data: {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\","
strlen(body_ok));
write_chunk(conn, sse_ok, strlen(sse_ok));
(void)tls_write_all(conn, "0\r\n\r\n", 5);
+ } else if (strcmp(argv[i], "CONTEXT") == 0) {
+ (void)tls_write_all(conn, body_ok,
+ strlen(body_ok));
+ write_chunk(conn, sse_context,
+ strlen(sse_context));
+ (void)tls_write_all(conn, "0\r\n\r\n", 5);
} else if (strcmp(argv[i], "ERROR") == 0) {
(void)tls_write_all(conn, body_ok,
strlen(body_ok));
blob - ad716e87ff862050b2394139462bf2d884011c2b
blob + 56b3dbadc3c2724c5f9183464e10d95916ebe673
--- src/fugu/coord.c
+++ src/fugu/coord.c
#include "sysprompt.h"
#include "skills.h"
#include "tooldefs.h"
+#include "tool_display.h"
#include "agentcfg.h"
#include "turn_mechanics.h"
#include "turn_txn.h"
/* accepted Generation views used by the existing presentation/tool seams */
struct buf text;
- struct a_usage usage;
+ struct a_usage lead_usage; /* latest Lead Generation only */
+ int have_context; /* published main-session context */
+ int64_t context_tokens;
struct tool_call tcalls[MAX_TOOL_CALLS];
int ntcalls;
char stop_reason[FUGU_STOP_REASON_MAX];
}
static void
-emit_tool(struct coord *c, const char *name)
+emit_tool(struct coord *c, const struct tool_call *tc)
{
+ struct buf display;
+
+ buf_init(&display);
+ tool_display_format(&display, tc->name, tc->input.data, tc->input.len);
if (c->ui)
- ui_send(c, FUGU_IMSG_UI_TOOL, name, strlen(name));
- else
- fprintf(stderr, "fugu: %s\n", name); /* terse (S1) */
+ ui_send(c, FUGU_IMSG_UI_TOOL, display.data, display.len);
+ else {
+ fputs("fugu: ", stderr);
+ if (display.len > 0)
+ fwrite(display.data, 1, display.len, stderr);
+ fputc('\n', stderr);
+ }
+ buf_free(&display);
}
/* A diff can carry very long lines (the line count is capped, not the
/* System feedback (stdout / UI_NOTE). */
#define emit_note(c, ...) emit_msg((c), FUGU_IMSG_UI_NOTE, stdout, __VA_ARGS__)
-/* Push a status-bar update to the front end (behavior.md 2.1). */
-static void
-send_status(struct coord *c, int state)
-{
- struct ui_status s;
-
- if (!c->ui)
- return;
- memset(&s, 0, sizeof(s));
- s.state = state;
- s.have_usage = (c->usage.input_tokens > 0 || c->usage.output_tokens > 0);
- s.usage = c->usage;
- ui_send(c, FUGU_IMSG_UI_STATUS, &s, sizeof(s));
-}
-
static int64_t effective_window(struct coord *); /* defined below */
static int
return (0);
}
+/*
+ * Provider usage does not have one cross-provider cache convention.
+ * Anthropic (including the Claude subscription route) reports uncached,
+ * cache-read, and cache-created input as disjoint counters. OpenAI's
+ * prompt_tokens already includes cached_tokens, which is only a detail of
+ * that total. Output is part of the context after the Generation settles.
+ */
+static int64_t
+lead_context_tokens(const struct coord *c)
+{
+ const struct a_usage *u = &c->lead_usage;
+ int64_t total;
+
+ total = u->input_tokens + u->output_tokens;
+ if (c->conf->provider_type != PROVIDER_OPENAI)
+ total += u->cache_read + u->cache_write;
+ return (total);
+}
+
+static void
+context_publish(struct coord *c)
+{
+ const struct a_usage *u = &c->lead_usage;
+
+ c->have_context = (u->input_tokens > 0 || u->output_tokens > 0 ||
+ u->cache_read > 0 || u->cache_write > 0);
+ c->context_tokens = c->have_context ? lead_context_tokens(c) : 0;
+}
+
+static void
+context_invalidate(struct coord *c)
+{
+ c->have_context = 0;
+ c->context_tokens = 0;
+}
+
static int
-usage_account(struct coord *c, const struct a_usage *u)
+lead_usage_account(struct coord *c, const struct a_usage *u)
{
- struct a_usage session = c->usage;
+ struct a_usage lead = c->lead_usage;
struct a_usage turn = c->turn_usage;
- if (usage_add(&session, u) == -1 || usage_add(&turn, u) == -1)
+ if (usage_add(&lead, u) == -1 || usage_add(&turn, u) == -1)
return (-1);
- c->usage = session;
+ c->lead_usage = lead;
c->turn_usage = turn;
return (0);
}
+/* Subagent usage remains part of whole-Turn accounting, never Lead context. */
+static int
+subagent_usage_account(struct coord *c, const struct a_usage *u)
+{
+ struct a_usage turn = c->turn_usage;
+
+ if (usage_add(&turn, u) == -1)
+ return (-1);
+ c->turn_usage = turn;
+ return (0);
+}
+
+static void
+status_fill(struct coord *c, struct ui_status *s, int state)
+{
+ memset(s, 0, sizeof(*s));
+ s->state = state;
+ s->have_context = c->have_context;
+ s->context_tokens = c->context_tokens;
+}
+
+/* Push a status-bar update to the front end (behavior.md 2.1). */
+static void
+send_status(struct coord *c, int state)
+{
+ struct ui_status s;
+
+ if (!c->ui)
+ return;
+ status_fill(c, &s, state);
+ ui_send(c, FUGU_IMSG_UI_STATUS, &s, sizeof(s));
+}
+
/*
* One typed configuration transaction: fixed settings, bounded Skill
* names, then the terminal frame that makes the snapshot visible. Keeping
au.cache_read = ev.usage.cache_read;
au.cache_write = ev.usage.cache_write;
if (!c->turn_data_exceeded && !c->turn_usage_exceeded) {
- if (usage_account(c, &au) == -1) {
+ if (lead_usage_account(c, &au) == -1) {
if (c->turn_generation_io != NULL)
c->turn_usage_exceeded = 1;
else
ui_send(c, FUGU_IMSG_UI_NOTE, ev.payload.data,
ev.payload.len);
- memset(&s, 0, sizeof(s));
- s.state = UI_RETRY;
- s.have_usage = 1;
- s.usage = c->usage;
+ status_fill(c, &s, UI_RETRY);
ui_send(c, FUGU_IMSG_UI_STATUS, &s, sizeof(s));
} else
fprintf(stderr, "fugu: %.*s\n", (int)ev.payload.len,
free(c->conf->model);
c->conf->model = xstrdup(id);
turn_txn_cache_reset(c->turn); /* the cache starts cold (8) */
+ context_invalidate(c); /* tokenization/provider changed */
send_ui_config(c); /* the status bar shows it */
emit_note(c, "model: %s", id);
break;
if (turn_tool_results_add_usage(c->turn_tool_results,
&gu) == -1)
c->turn_tool_results_rejected = 1;
- observer_rc = usage_account(c, &u);
+ observer_rc = subagent_usage_account(c, &u);
if (observer_rc == -1)
c->turn_usage_exceeded = 1;
else
reset_tcalls(c);
buf_reset(&c->text);
- memset(&c->usage, 0, sizeof(c->usage));
+ memset(&c->lead_usage, 0, sizeof(c->lead_usage));
c->stop_reason[0] = '\0';
c->err[0] = '\0';
c->generation_terminal = 0;
memset(result, 0, sizeof(*result));
reset_tcalls(c);
buf_reset(&c->text);
- memset(&c->usage, 0, sizeof(c->usage));
+ memset(&c->lead_usage, 0, sizeof(c->lead_usage));
c->stop_reason[0] = '\0';
c->err[0] = '\0';
c->generation = generation_new(data_available);
call = &batch->calls[i];
c->turn_tool_calls++;
machine_tool_call(c, tc);
- emit_tool(c, tc->name); /* terse (S1) */
+ emit_tool(c, tc);
if (strcmp(tc->name, "agent") == 0) {
if (!tc->result_ready)
agent_call_error(tc, "agent: no result");
msg_add_text(um, prompt, promptlen);
memset(&c->turn_usage, 0, sizeof(c->turn_usage));
- memset(&c->usage, 0, sizeof(c->usage));
+ memset(&c->lead_usage, 0, sizeof(c->lead_usage));
c->turn_tool_calls = 0;
c->turn_data = promptlen;
c->turn_data_exceeded = 0;
"turn abandoned by session switch"));
if (gate != COMMIT_GATE_COMMITTED)
return (cancel_turn(c));
+ context_publish(c);
emit_answer(c);
machine_finish(c, 0);
return (0);
"turn abandoned by session switch"));
if (gate != COMMIT_GATE_COMMITTED)
return (cancel_turn(c));
+ context_publish(c);
emit_error(c, "fugu: reached the %d-step tool limit for one turn",
TURN_ITER_MAX);
machine_error(c, "tool-loop iteration limit reached");
turn_txn_clear(c->turn, contextp);
buf_reset(&c->compaction); /* a reset voids the summary */
rebuild_system(c); /* drop the summary either way */
+ context_invalidate(c);
if (c->ui)
ui_send(c, FUGU_IMSG_UI_CLEARED, NULL, 0);
emit_note(c, "context cleared");
buf_reset(&c->compaction);
buf_add(&c->compaction, c->text.data, c->text.len);
rebuild_system(c);
+ context_invalidate(c);
if (c->ui)
ui_send(c, FUGU_IMSG_UI_CLEARED, NULL, 0);
emit_note(c, "conversation compacted (~%lld tokens of notes)",
blob - 2811160f11ee550272b36ff18b2e241e843b186d
blob + c842be5413d99c5bbb441fd8cb9ef666c916c8ec
--- src/fugu/fugu.1
+++ src/fugu/fugu.1
are rendered with terminal attributes; user messages remain plain text.
Tool calls, errors, and edit diffs are visually distinct.
The status bar identifies idle, active-turn, summarization, and retry states,
-and shows token usage and effective-context-window use when known.
+and shows
+.Dq Context current/max N%
+when known.
+The current count combines cache and session tokens from the main
+conversation's latest committed generation; ephemeral subagent usage is
+excluded.
+Counts of at least 1,000 are rounded to the nearest thousand and carry a
+.Dq k
+suffix.
At 80 percent of the effective window it warns once and names
.Cm /compact .
.
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
Repaint the display.
.It Ic Ctrl-C
blob - 33b066010d4001607e945821266929cb6d00ae58
blob + 6c69d306111f7d24e254e16cf6e009bf954d370b
--- src/fugu/proto.h
+++ src/fugu/proto.h
int state; /* enum ui_state */
int attempt; /* retry attempt number (UI_RETRY) */
int delay_ms; /* pending retry delay (UI_RETRY) */
- int have_usage; /* usage fields are meaningful */
- struct a_usage usage; /* running token counts */
+ int have_context; /* context_tokens is meaningful */
+ int64_t context_tokens; /* Lead context only; excludes subagents */
};
/* A tool_use block opening; the id and name follow as "id\0name". */
blob - 05f771be2db08fe8748ce411937e60884c6fc586
blob + a01c9a8bd283e2f1a2f45eca0eebf5effc9fb437
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
#define P_TOOL 5
#define P_ERR 6
#define P_DIM 7
+#define P_TEXT 8
/* the conversation drawer (behavior.md 2.1): reference width, and the
* narrowest terminal it will occupy without starving the transcript */
static struct ui *UI; /* for the atexit terminal restore */
+/* Terminal modes owned by fugu-tty. Disable in reverse order so every
+ * survivable teardown and external-editor handoff restores the terminal. */
+static const char terminal_modes_on[] =
+ "\033[?2004h\033[?1000h\033[?1006h";
+static const char terminal_modes_off[] =
+ "\033[?1006l\033[?1000l\033[?2004l";
+
+static void
+terminal_modes(int enable)
+{
+ if (enable)
+ (void)write(STDOUT_FILENO, terminal_modes_on,
+ sizeof(terminal_modes_on) - 1);
+ else
+ (void)write(STDOUT_FILENO, terminal_modes_off,
+ sizeof(terminal_modes_off) - 1);
+}
+
/* ---- small helpers -------------------------------------------------- */
static int
a |= u->color ? COLOR_PAIR(P_CODE) : A_DIM;
if (md & MD_BULLET)
a |= A_BOLD;
+ if (u->color && !(md & (MD_HEADING | MD_CODE | MD_CODEBLOCK)))
+ a |= COLOR_PAIR(P_TEXT);
return (a);
}
/* Wrap plain UTF-8 text to width columns, one attr, into new lines. */
static void
render_plain(struct ui *u, const char *text, size_t len, int width, int attr,
- const char *prefix)
+ const char *prefix, const char *continuation)
{
struct dline *l;
size_t i = 0;
int col;
size_t last_break, seg_start;
+ const char *lead;
if (width < 1)
width = 1;
do {
l = line_new(u);
col = 0;
- if (prefix != NULL && i == 0) {
+ lead = i == 0 ? prefix : continuation;
+ if (lead != NULL) {
int pw = 0;
size_t k;
- for (k = 0; prefix[k] != '\0'; k++)
+ for (k = 0; lead[k] != '\0'; k++)
pw++;
- line_add(l, attr, prefix, strlen(prefix), pw);
+ line_add(l, attr, lead, strlen(lead), pw);
col = pw;
}
seg_start = i;
attr = u->color ? COLOR_PAIR(P_TOOL) :
A_BOLD | A_UNDERLINE;
}
- render_plain(u, text + i, j - i, width, attr, NULL);
+ render_plain(u, text + i, j - i, width, attr, NULL, NULL);
i = (j < len) ? j + 1 : j;
}
}
switch (it->kind) {
case IT_USER:
render_plain(u, t, n, w,
- A_BOLD, "> ");
+ A_BOLD, "> ", NULL);
break;
case IT_ASSIST:
render_assist(u, it, w);
break;
case IT_TOOL:
render_plain(u, t, n, w,
- u->color ? COLOR_PAIR(P_TOOL) : A_DIM, "tool: ");
+ u->color ? COLOR_PAIR(P_TOOL) : A_DIM,
+ " tool: ", " ");
break;
case IT_DIFF:
render_diff(u, t, n, w);
break;
case IT_NOTE:
render_plain(u, t, n, w,
- u->color ? COLOR_PAIR(P_DIM) : A_DIM, NULL);
+ u->color ? COLOR_PAIR(P_DIM) : A_DIM, NULL, NULL);
break;
case IT_ERROR:
render_plain(u, t, n, w,
- u->color ? COLOR_PAIR(P_ERR) : A_BOLD, "! ");
+ u->color ? COLOR_PAIR(P_ERR) : A_BOLD, "! ", NULL);
break;
}
}
}
static void
-fmt_usage(const struct ui *u, char *out, size_t sz)
+fmt_token_count(int64_t tokens, char *out, size_t sz)
{
- const struct a_usage *g = &u->status.usage;
- int64_t pct = -1;
+ int64_t rounded;
- if (u->window > 0 && g->input_tokens > 0)
- pct = g->input_tokens * 100 / u->window;
- if (g->cache_read > 0 || g->cache_write > 0)
- snprintf(out, sz, "in %lld out %lld cache %lld/%lld",
- (long long)g->input_tokens, (long long)g->output_tokens,
- (long long)g->cache_read, (long long)g->cache_write);
- else
- snprintf(out, sz, "in %lld out %lld",
- (long long)g->input_tokens, (long long)g->output_tokens);
- if (pct >= 0) {
- char pbuf[32];
-
- snprintf(pbuf, sizeof(pbuf), " %lld%%", (long long)pct);
- strlcat(out, pbuf, sz);
+ if (tokens < 1000) {
+ snprintf(out, sz, "%lld", (long long)tokens);
+ return;
}
+ rounded = tokens / 1000 + (tokens % 1000 >= 500);
+ snprintf(out, sz, "%lldk", (long long)rounded);
}
static void
+fmt_context(const struct ui *u, char *out, size_t sz)
+{
+ char current[32], maximum[32];
+
+ fmt_token_count(u->status.context_tokens, current, sizeof(current));
+ if (u->window > 0) {
+ int64_t q, tail, pct;
+
+ fmt_token_count(u->window, maximum, sizeof(maximum));
+ q = u->status.context_tokens / u->window;
+ tail = (u->status.context_tokens % u->window) * 100 /
+ u->window;
+ pct = q > (INT64_MAX - tail) / 100 ? INT64_MAX :
+ q * 100 + tail;
+ snprintf(out, sz, "Context %s/%s %lld%%", current, maximum,
+ (long long)pct);
+ } else
+ snprintf(out, sz, "Context %s/?", current);
+}
+
+static int
+context_warn(const struct ui *u)
+{
+ int64_t threshold;
+
+ if (!u->status.have_context || u->window <= 0)
+ return (0);
+ threshold = (u->window * 80 + 99) / 100;
+ return (u->status.context_tokens >= threshold);
+}
+
+static void
draw_status(struct ui *u, int row)
{
- char left[128], right[160], usage[96];
- int64_t pct = -1;
- int warn = 0;
+ char left[128], right[160], context[96];
+ int warn;
switch (u->status.state) {
case UI_BUSY:
}
right[0] = '\0';
- if (u->status.have_usage) {
- fmt_usage(u, usage, sizeof(usage));
- strlcpy(right, usage, sizeof(right));
- if (u->window > 0 && u->status.usage.input_tokens > 0)
- pct = u->status.usage.input_tokens * 100 / u->window;
+ if (u->status.have_context) {
+ fmt_context(u, context, sizeof(context));
+ strlcpy(right, context, sizeof(right));
}
if (u->notice[0] != '\0')
strlcpy(right, u->notice, sizeof(right));
- warn = (pct >= 80);
+ warn = context_warn(u);
move(row, 0);
attrset(warn ? (u->color ? COLOR_PAIR(P_ERR) : A_BOLD) : A_REVERSE);
u->esc_pending = 0;
}
event_del(&u->ev_in);
- (void)write(STDOUT_FILENO, "\033[?2004l", 8); /* paste off */
+ terminal_modes(0);
endwin();
imsgev_send_chunks(&u->w->iev, FUGU_IMSG_UI_COMPOSE, 0, text, len);
imsgev_send(&u->w->iev, FUGU_IMSG_UI_COMPOSE_GO, 0,
return;
}
u->composing = 0;
- (void)write(STDOUT_FILENO, "\033[?2004h", 8); /* paste back on */
+ terminal_modes(1);
refresh(); /* return from endwin to program mode */
clearok(stdscr, TRUE);
if (p != NULL && len == sizeof(u->status)) {
memcpy(&u->status, p, sizeof(u->status));
u->busy = (u->status.state != UI_IDLE);
- if (u->window > 0 && u->status.usage.input_tokens > 0 &&
- u->status.usage.input_tokens * 100 / u->window >= 80
- && !u->warned80) {
+ if (context_warn(u) && !u->warned80) {
add_item(u, IT_NOTE, "context is over 80% of "
"the window -- consider /compact", 54);
u->warned80 = 1;
{
if (UI == NULL)
return;
- (void)write(STDOUT_FILENO, "\033[?2004l", 8); /* bracketed paste off */
+ terminal_modes(0);
endwin();
UI = NULL;
}
init_pair(P_TOOL, COLOR_CYAN, -1);
init_pair(P_ERR, COLOR_RED, -1);
init_pair(P_DIM, COLOR_BLUE, -1);
+ init_pair(P_TEXT, COLOR_WHITE, -1);
}
- (void)write(STDOUT_FILENO, "\033[?2004h", 8); /* bracketed paste on */
+ terminal_modes(1);
u = xcalloc(1, sizeof(*u));
u->w = w;