commit - 1ea23e34d9f698ee5ed967301a83ed578127e7a4
commit + 91b632582edaf81718fbffc9193f46706b137bd2
blob - 40d97bbcfbdff451c686c1743d770ecd648b8a54
blob + cb2cb4504dfaeeff1442b53c94c8e823f6989863
--- regress/Makefile
+++ regress/Makefile
SUBDIR= agentcfg anthropic buf conf deploy diff editor generation http imsgev journal json log markdown \
- model_window openai output print sandbox skills sse term tools turn web xmalloc
+ model_window openai output print sandbox skills sse term tools turn ui_config \
+ web xmalloc
.include <bsd.subdir.mk>
blob - ab6275d337f12bc5508e5c79157d134b29749fbd
blob + 57c2435bf5f099f573b300ffbe187a7635b10368
--- regress/README
+++ regress/README
and checked-send failures fail closed);
generation (canonical event ordering,
bounds, and exactly one terminal);
+ ui_config (typed ordering, bounded staging,
+ and atomic publication);
imsg length/type
validation in worker.c/coord.c; hub-and-spoke
authority map in proto.h; turn
src/fugu/priv.c, coord.c sandbox (spawn/handshake/teardown),
turn (lead and subagent turn loops)
src/fugu/journal.c journal, turn
+src/fugu-tty/ui_config_rx.c ui_config; turn (full-palette PTY path)
src/common/imsgev.c imsgev (whole-payload views, checked
normal/teardown sends, event failures);
sandbox (event runtime)
blob - 735c7ebf5464536931e048b8cef4887693489da4
blob + 1a4b63676dc080f78a50128e54b7bce0dca9a89e
--- regress/turn/run.sh
+++ regress/turn/run.sh
fail "curses: ptydrive helper not built"
fi
+# --- 28a: the full skill palette crosses the imsg payload boundary ---
+# 256 maximum-length names do not fit beside ui_config in one imsg. The
+# typed config stream must nevertheless deliver the complete palette before
+# accepting input: Tab expands the unique prefix, and Enter runs that skill.
+if [ -x "$obj/ptydrive" ]; then
+ rm -rf "$HOME/.fugu"
+ rm -f "$dir/reqout"
+ skilldir=$HOME/.fugu/skills
+ mkdir -p "$skilldir"
+ bulk_tail=$(awk 'BEGIN { for (i = 0; i < 55; i++) printf "x" }')
+ i=0
+ while [ "$i" -lt 255 ]; do
+ num=$(printf '%03d' "$i")
+ name=bulk${num}_${bulk_tail}
+ mkdir -p "$skilldir/$name"
+ printf -- '---\ndisable-model-invocation: true\n---\nBULK-%s\n' \
+ "$num" > "$skilldir/$name/SKILL.md"
+ i=$((i + 1))
+ done
+ target_tail=$(awk 'BEGIN { for (i = 0; i < 40; i++) printf "x" }')
+ target=typed_ui_config_target_${target_tail}
+ marker=TYPED-UI-CONFIG-SENTINEL-BODY
+ mkdir -p "$skilldir/$target"
+ printf -- '---\ndisable-model-invocation: true\n---\n%s\n' "$marker" \
+ > "$skilldir/$target/SKILL.md"
+ start_stub "OK"
+ write_conf "$stubport"
+ printf '/typed_ui_config_\t\r\000\000/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
+ rc=$?
+ seen=0
+ grep -q "$marker" "$dir/reqout" 2>/dev/null && seen=1
+ if [ "$rc" -eq 0 ] && [ "$seen" -eq 1 ]; then
+ pass "typed UI config: full skill palette completes and Tab runs its sentinel"
+ else
+ echo "# typed UI config: ptydrive rc=$rc, sentinel_seen=$seen"
+ fail "typed UI config: full skill palette completes and Tab runs its sentinel"
+ fi
+ stop_stub
+else
+ fail "typed UI config: ptydrive helper not built"
+fi
+
# --- 29: the external-editor compose room (behavior.md 2.1) ----------
# `v` in normal mode suspends the screen and runs $VISUAL in the
# fugu-editor room; a zero exit replaces the input with the file's
blob - /dev/null
blob + a2114fd327240c4762111431b7d5bb7ae96d4e4e (mode 644)
--- /dev/null
+++ regress/ui_config/Makefile
+PROG= ui_config_test
+SRCS= ui_config_test.c ui_config_rx.c xmalloc.c log.c
+NOMAN= yes
+
+CFLAGS+= -I${.CURDIR}/../../src/fugu
+CFLAGS+= -I${.CURDIR}/../../src/fugu-tty
+.PATH: ${.CURDIR}/../../src/fugu-tty
+
+REGRESS_TARGETS= run-ui-config
+
+run-ui-config: ui_config_test
+ ./ui_config_test
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + 679cfc582966b2a7c1f4c4b94c48c556bb54f1ea (mode 644)
--- /dev/null
+++ regress/ui_config/ui_config_test.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "model_window.h"
+#include "proto.h"
+#include "ui_config_rx.h"
+
+#include "regress.h"
+
+static struct ui_config
+valid_config(void)
+{
+ struct ui_config cfg;
+
+ memset(&cfg, 0, sizeof(cfg));
+ cfg.ascii = 1;
+ cfg.notify = 0;
+ cfg.window = 200000;
+ strlcpy(cfg.model, "claude-test", sizeof(cfg.model));
+ return (cfg);
+}
+
+static void
+test_malformed_settings(void)
+{
+ struct ui_config_rx *rx;
+ struct ui_config_snapshot *snap = NULL;
+ struct ui_config cfg;
+
+ cfg = valid_config();
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg) - 1, &snap) == UI_CONFIG_RX_ERROR);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+
+ cfg = valid_config();
+ cfg.ascii = 2;
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+
+ cfg = valid_config();
+ cfg.notify = -1;
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+
+ cfg = valid_config();
+ cfg.window = -1;
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+
+ cfg = valid_config();
+ cfg.window = FUGU_MODEL_WINDOW_MAX + 1;
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+
+ cfg = valid_config();
+ memset(cfg.model, 'm', sizeof(cfg.model));
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+}
+
+static int
+feed_one_skill(const void *name, size_t len)
+{
+ struct ui_config_rx *rx;
+ struct ui_config_snapshot *snap = NULL;
+ struct ui_config cfg;
+ int status;
+
+ cfg = valid_config();
+ rx = ui_config_rx_new();
+ if (ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) != UI_CONFIG_RX_MORE)
+ status = UI_CONFIG_RX_ERROR;
+ else
+ status = ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ name, len, &snap);
+ ui_config_rx_free(rx);
+ return (status);
+}
+
+static void
+test_malformed_skills(void)
+{
+ struct ui_config_rx *rx;
+ struct ui_config_snapshot *snap = NULL;
+ struct ui_config cfg;
+ char longname[FUGU_SKILL_NAME_MAX];
+ const char embedded[] = { 'a', '\0', 'b' };
+
+ memset(longname, 'a', sizeof(longname));
+ CHECK(feed_one_skill(NULL, 0) == UI_CONFIG_RX_ERROR);
+ CHECK(feed_one_skill(NULL, 1) == UI_CONFIG_RX_ERROR);
+ CHECK(feed_one_skill(longname, sizeof(longname)) == UI_CONFIG_RX_ERROR);
+ CHECK(feed_one_skill(embedded, sizeof(embedded)) == UI_CONFIG_RX_ERROR);
+ CHECK(feed_one_skill("not/valid", 9) == UI_CONFIG_RX_ERROR);
+ CHECK(feed_one_skill("okay-Name_9", 11) == UI_CONFIG_RX_MORE);
+
+ cfg = valid_config();
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_MORE);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ "same", 4, &snap) == UI_CONFIG_RX_MORE);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ "same", 4, &snap) == UI_CONFIG_RX_ERROR);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ NULL, 0, &snap) == UI_CONFIG_RX_ERROR);
+ CHECK(snap == NULL);
+ ui_config_rx_free(rx);
+
+ cfg = valid_config();
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_MORE);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ embedded, sizeof(embedded), &snap) == UI_CONFIG_RX_ERROR);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ NULL, 0, &snap) == UI_CONFIG_RX_ERROR);
+ CHECK(snap == NULL);
+ ui_config_rx_free(rx);
+}
+
+static void
+test_skill_limit(void)
+{
+ struct ui_config_rx *rx;
+ struct ui_config_snapshot *snap = NULL;
+ struct ui_config cfg;
+ char name[16];
+ int i, status = UI_CONFIG_RX_MORE;
+
+ cfg = valid_config();
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_MORE);
+ for (i = 0; i < FUGU_SKILLS_MAX; i++) {
+ snprintf(name, sizeof(name), "skill%03d", i);
+ status = ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ name, strlen(name), &snap);
+ if (status != UI_CONFIG_RX_MORE)
+ break;
+ }
+ CHECK(i == FUGU_SKILLS_MAX && status == UI_CONFIG_RX_MORE);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ NULL, 0, &snap) == UI_CONFIG_RX_COMPLETE);
+ CHECK(snap != NULL && ui_config_snapshot_skill_count(snap) ==
+ FUGU_SKILLS_MAX);
+ CHECK(strcmp(ui_config_snapshot_skill(snap, 0), "skill000") == 0);
+ CHECK(strcmp(ui_config_snapshot_skill(snap,
+ FUGU_SKILLS_MAX - 1), "skill255") == 0);
+ ui_config_snapshot_free(snap);
+ ui_config_rx_free(rx);
+
+ snap = NULL;
+ cfg = valid_config();
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_MORE);
+ for (i = 0; i < FUGU_SKILLS_MAX; i++) {
+ snprintf(name, sizeof(name), "skill%03d", i);
+ status = ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ name, strlen(name), &snap);
+ if (status != UI_CONFIG_RX_MORE)
+ break;
+ }
+ CHECK(i == FUGU_SKILLS_MAX && status == UI_CONFIG_RX_MORE);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ "overflow", 8, &snap) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+}
+
+static void
+test_inclusive_boundaries(void)
+{
+ struct ui_config_rx *rx;
+ struct ui_config_snapshot *snap = NULL;
+ struct ui_config cfg;
+ char name[FUGU_SKILL_NAME_MAX - 1];
+
+ cfg = valid_config();
+ cfg.ascii = 0;
+ cfg.notify = 1;
+ cfg.window = FUGU_MODEL_WINDOW_MAX;
+ memset(cfg.model, 'm', sizeof(cfg.model));
+ cfg.model[sizeof(cfg.model) - 1] = '\0';
+ memset(name, 's', sizeof(name));
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_MORE);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ name, sizeof(name), &snap) == UI_CONFIG_RX_MORE);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ NULL, 0, &snap) == UI_CONFIG_RX_COMPLETE);
+ CHECK(ui_config_snapshot_settings(snap)->window ==
+ FUGU_MODEL_WINDOW_MAX);
+ CHECK(strlen(ui_config_snapshot_skill(snap, 0)) ==
+ FUGU_SKILL_NAME_MAX - 1);
+ ui_config_snapshot_free(snap);
+ ui_config_rx_free(rx);
+}
+
+static struct ui_config_snapshot *
+complete_snapshot(struct ui_config_rx *rx, const char *model,
+ const char *skill)
+{
+ struct ui_config_snapshot *snap = NULL;
+ struct ui_config cfg;
+
+ cfg = valid_config();
+ strlcpy(cfg.model, model, sizeof(cfg.model));
+ if (ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) != UI_CONFIG_RX_MORE)
+ return (NULL);
+ if (skill != NULL && ui_config_rx_feed(rx,
+ FUGU_IMSG_UI_CONFIG_SKILL, skill, strlen(skill),
+ &snap) != UI_CONFIG_RX_MORE)
+ return (NULL);
+ if (ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ NULL, 0, &snap) != UI_CONFIG_RX_COMPLETE)
+ return (NULL);
+ return (snap);
+}
+
+static void
+test_sequence_and_atomicity(void)
+{
+ struct ui_config_rx *rx;
+ struct ui_config_snapshot *old, *next = NULL;
+ struct ui_config cfg;
+ char byte = 'x';
+
+ cfg = valid_config();
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_TEXT,
+ &byte, 1, &next) == UI_CONFIG_RX_OTHER);
+ ui_config_rx_free(rx);
+
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ "early", 5, &next) == UI_CONFIG_RX_ERROR);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &next) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ NULL, 0, &next) == UI_CONFIG_RX_ERROR);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &next) == UI_CONFIG_RX_ERROR);
+ ui_config_rx_free(rx);
+
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &next) == UI_CONFIG_RX_MORE);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &next) == UI_CONFIG_RX_ERROR);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_STATUS,
+ &byte, 1, &next) == UI_CONFIG_RX_ERROR);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ &byte, 1, &next) == UI_CONFIG_RX_ERROR);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ NULL, 0, &next) == UI_CONFIG_RX_ERROR);
+ CHECK(next == NULL);
+ ui_config_rx_free(rx);
+
+ /* A later, incomplete stream cannot mutate an already published view. */
+ rx = ui_config_rx_new();
+ old = complete_snapshot(rx, "old-model", "old_skill");
+ CHECK(old != NULL);
+ cfg = valid_config();
+ strlcpy(cfg.model, "unpublished-model", sizeof(cfg.model));
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &next) == UI_CONFIG_RX_MORE);
+ CHECK(next == NULL);
+ CHECK(strcmp(ui_config_snapshot_settings(old)->model,
+ "old-model") == 0);
+ CHECK(ui_config_snapshot_skill_count(old) == 1);
+ CHECK(strcmp(ui_config_snapshot_skill(old, 0), "old_skill") == 0);
+ ui_config_rx_free(rx);
+ CHECK(strcmp(ui_config_snapshot_settings(old)->model,
+ "old-model") == 0);
+ ui_config_snapshot_free(old);
+
+ /* END publishes a complete replacement and permits later reconfigure. */
+ rx = ui_config_rx_new();
+ old = complete_snapshot(rx, "first-model", NULL);
+ next = complete_snapshot(rx, "second-model", "second_skill");
+ CHECK(old != NULL && next != NULL);
+ CHECK(strcmp(ui_config_snapshot_settings(old)->model,
+ "first-model") == 0);
+ CHECK(strcmp(ui_config_snapshot_settings(next)->model,
+ "second-model") == 0);
+ CHECK(strcmp(ui_config_snapshot_skill(next, 0),
+ "second_skill") == 0);
+ CHECK(ui_config_snapshot_skill(next, 1) == NULL);
+ ui_config_snapshot_free(old);
+ ui_config_snapshot_free(next);
+ ui_config_rx_free(rx);
+}
+
+int
+main(void)
+{
+ struct ui_config_rx *rx;
+ struct ui_config_snapshot *snap = NULL;
+ struct ui_config cfg;
+
+ cfg = valid_config();
+
+ rx = ui_config_rx_new();
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_BEGIN,
+ &cfg, sizeof(cfg), &snap) == UI_CONFIG_RX_MORE);
+ CHECK(snap == NULL);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_SKILL,
+ "greet", 5, &snap) == UI_CONFIG_RX_MORE);
+ CHECK(snap == NULL);
+ CHECK(ui_config_rx_feed(rx, FUGU_IMSG_UI_CONFIG_END,
+ NULL, 0, &snap) == UI_CONFIG_RX_COMPLETE);
+ CHECK(snap != NULL);
+ CHECK(ui_config_snapshot_settings(snap)->ascii == 1);
+ CHECK(ui_config_snapshot_settings(snap)->notify == 0);
+ CHECK(ui_config_snapshot_settings(snap)->window == 200000);
+ CHECK(strcmp(ui_config_snapshot_settings(snap)->model,
+ "claude-test") == 0);
+ CHECK(ui_config_snapshot_skill_count(snap) == 1);
+ CHECK(strcmp(ui_config_snapshot_skill(snap, 0), "greet") == 0);
+
+ ui_config_snapshot_free(snap);
+ ui_config_rx_free(rx);
+ test_malformed_settings();
+ test_malformed_skills();
+ test_skill_limit();
+ test_inclusive_boundaries();
+ test_sequence_and_atomicity();
+ REGRESS_END();
+}
blob - 23e224028b672b78a69570779b65f39049b5ca2e
blob + b995cdd31bd24787f89b407a6beeb22d9b533c9f
--- src/fugu/coord.c
+++ src/fugu/coord.c
event_loopexit(NULL);
}
-/* The interface settings, then the skill names as NUL-separated
- * completion candidates; re-sent whenever a setting changes (2.1). */
+/*
+ * One typed configuration transaction: fixed settings, bounded Skill
+ * names, then the terminal frame that makes the snapshot visible. Keeping
+ * these checked sends contiguous prevents a render event from being
+ * accepted against a half-published palette.
+ */
static void
send_ui_config(struct coord *c)
{
struct ui_config cfg;
- struct buf cb;
+ size_t len;
int i;
if (!c->ui)
cfg.window = effective_window(c);
if (c->conf->model != NULL)
strlcpy(cfg.model, c->conf->model, sizeof(cfg.model));
- buf_init(&cb);
- buf_add(&cb, &cfg, sizeof(cfg));
- if (c->skills != NULL)
+ if (c->skills != NULL) {
+ if (c->skills->n < 0 || c->skills->n > FUGU_SKILLS_MAX)
+ fatalx("invalid UI Skill count");
for (i = 0; i < c->skills->n; i++) {
- buf_addstr(&cb, c->skills->skills[i].name);
- buf_addc(&cb, '\0');
+ len = strlen(c->skills->skills[i].name);
+ if (len == 0 || len >= FUGU_SKILL_NAME_MAX)
+ fatalx("invalid UI Skill name");
}
- ui_send(c, FUGU_IMSG_UI_CONFIG, cb.data, cb.len);
- buf_free(&cb);
+ }
+ ui_send(c, FUGU_IMSG_UI_CONFIG_BEGIN, &cfg, sizeof(cfg));
+ if (c->skills != NULL)
+ for (i = 0; i < c->skills->n; i++)
+ ui_send(c, FUGU_IMSG_UI_CONFIG_SKILL,
+ c->skills->skills[i].name,
+ strlen(c->skills->skills[i].name));
+ ui_send(c, FUGU_IMSG_UI_CONFIG_END, NULL, 0);
}
static void
{
const char *rest = line + 1, *sp = strchr(rest, ' ');
const struct skill *sk;
- char name[SKILL_NAME_MAX];
+ char name[FUGU_SKILL_NAME_MAX];
size_t nlen = sp != NULL ?
(size_t)(sp - rest) : strlen(rest);
blob - 750ea902fc8e0d16176f26e7f41b4c95e6aefdd8
blob + 34516048ceae9b0faa99bce0196bb2d6681151d6
--- src/fugu/proto.h
+++ src/fugu/proto.h
*/
#define FUGU_EDIT_MAX (64 * 1024)
+/* Canonical Skill-domain limits shared by loader, tool, and UI stream. */
+#define FUGU_SKILL_NAME_MAX 64 /* including terminating NUL */
+#define FUGU_SKILLS_MAX 256
+
/* A model id on the wire (MODEL, UI_SELECT_MODEL), bounded on both
* sides: provider listings are hostile input (I13). */
#define FUGU_MODEL_ID_MAX 128
* that parses hostile terminal bytes and model markdown holds no
* credentials and no journals (I7, I11).
*/
- FUGU_IMSG_UI_CONFIG, /* struct ui_config: ascii/notify/window/model */
+ FUGU_IMSG_UI_CONFIG_BEGIN, /* struct ui_config: fixed settings */
+ FUGU_IMSG_UI_CONFIG_SKILL, /* one raw bounded Skill name */
+ FUGU_IMSG_UI_CONFIG_END, /* atomically publish staged settings/Skills */
FUGU_IMSG_UI_TEXT, /* assistant text delta (streamed, markdown) */
FUGU_IMSG_UI_TOOL, /* a tool-call line */
FUGU_IMSG_UI_DIFF, /* a presentation-only diff hunk (2.1) */
blob - fcf7a0a158819aa4e14897a5e75bed21befbeab6
blob + 11595ca8b0c821dfb763b78c394e054408221053
--- src/fugu/skills.c
+++ src/fugu/skills.c
#include "skills.h"
#define SKILL_FILE_MAX (256 * 1024)
-#define SKILLS_MAX 256
-
/* Read a regular file under the size cap; NULL if absent/oversize. */
static char *
slurp(const char *path, size_t *lenp)
{
size_t i;
- if (s[0] == '\0' || strlen(s) >= SKILL_NAME_MAX)
+ if (s[0] == '\0' || strlen(s) >= FUGU_SKILL_NAME_MAX)
return (0);
for (i = 0; s[i] != '\0'; i++)
if (!isalnum((unsigned char)s[i]) && s[i] != '-' && s[i] != '_')
if ((d = opendir(dir)) == NULL)
return (ss); /* no skills directory */
- ss->skills = xreallocarray(NULL, SKILLS_MAX, sizeof(*ss->skills));
- while ((de = readdir(d)) != NULL && ss->n < SKILLS_MAX) {
+ ss->skills = xreallocarray(NULL, FUGU_SKILLS_MAX,
+ sizeof(*ss->skills));
+ while ((de = readdir(d)) != NULL && ss->n < FUGU_SKILLS_MAX) {
char path[1024];
char *data;
size_t len;
blob - 68321bbc86101fb825efa0d3d75ce587b913ed68
blob + eeebe2832165b8c69a49bb65589449d50c7628b8
--- src/fugu/skills.h
+++ src/fugu/skills.h
#include <sys/types.h>
+#include "proto.h"
+
/*
* User-installed skills (behavior.md section 4): prompt templates
* under ~/.fugu/skills/<name>/SKILL.md, loaded once at startup. A
* tool (section 5), whose result is the same body.
*/
-#define SKILL_NAME_MAX 64
-
struct skill {
- char name[SKILL_NAME_MAX];
+ char name[FUGU_SKILL_NAME_MAX];
char *description; /* may be NULL */
char *body; /* the template; NUL-terminated */
size_t bodylen;
blob - 28a0b075625bd74a6f7adf174b92ef36812be65f
blob + c85c3f89ffa51c2863e5d736f1543051bb983324
--- src/fugu-tty/Makefile
+++ src/fugu-tty/Makefile
PROG= fugu-tty
-SRCS= main.c ui.c markdown.c editor.c term_input.c utf8.c \
+SRCS= main.c ui.c ui_config_rx.c markdown.c editor.c term_input.c utf8.c \
worker.c imsgev.c buf.c log.c xmalloc.c
BINDIR= ${LIBEXECDIR}/fugu
MAN=
blob - 5606c58654257e575fb61aaa0b53c773f7495e87
blob + eece9db7071abfafc31067ad2192b24c8a84511a
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
#include "markdown.h"
#include "editor.h"
#include "term_input.h"
+#include "ui_config_rx.h"
#include "ui.h"
/* transcript item kinds */
int ascii, notify;
int64_t window;
char model[64];
+ struct ui_config_rx *config_rx;
/* status */
struct ui_status status;
}
static void
-set_config(struct ui *u, const u_char *p, size_t len)
+publish_config(struct ui *u, const struct ui_config_snapshot *snapshot)
{
- struct ui_config cfg;
- size_t off, i;
+ const struct ui_config *cfg;
+ char **comps, **old;
+ size_t ncomps, oldn, i;
const char *builtin[] = { "clear", "compact", "context",
"exit", "model", "quit" };
- if (p == NULL || len < sizeof(cfg))
- return;
- memcpy(&cfg, p, sizeof(cfg));
- cfg.model[sizeof(cfg.model) - 1] = '\0';
- u->ascii = cfg.ascii;
- u->notify = cfg.notify;
- u->window = cfg.window;
- strlcpy(u->model, cfg.model, sizeof(u->model));
+ /* Build the whole replacement before touching the live UI snapshot. */
+ ncomps = sizeof(builtin) / sizeof(builtin[0]) +
+ ui_config_snapshot_skill_count(snapshot);
+ comps = xcalloc(ncomps, sizeof(*comps));
+ for (i = 0; i < sizeof(builtin) / sizeof(builtin[0]); i++)
+ comps[i] = xstrdup(builtin[i]);
+ for (i = sizeof(builtin) / sizeof(builtin[0]); i < ncomps; i++)
+ comps[i] = xstrdup(ui_config_snapshot_skill(snapshot,
+ i - sizeof(builtin) / sizeof(builtin[0])));
- /* completion candidates: the built-ins, then the skill names that
- * follow the struct as NUL-separated bytes */
- for (i = 0; i < u->ncomps; i++)
- free(u->comps[i]);
- u->ncomps = 0;
- for (i = 0; i < sizeof(builtin) / sizeof(builtin[0]); i++) {
- u->comps = xreallocarray(u->comps, u->ncomps + 1,
- sizeof(*u->comps));
- u->comps[u->ncomps++] = xstrdup(builtin[i]);
- }
- off = sizeof(cfg);
- while (off < len) {
- size_t e = off;
+ old = u->comps;
+ oldn = u->ncomps;
+ ed_set_completions(&u->ed, comps, ncomps);
+ u->comps = comps;
+ u->ncomps = ncomps;
+ for (i = 0; i < oldn; i++)
+ free(old[i]);
+ free(old);
- while (e < len && p[e] != '\0')
- e++;
- if (e > off) {
- u->comps = xreallocarray(u->comps, u->ncomps + 1,
- sizeof(*u->comps));
- u->comps[u->ncomps] = xmalloc(e - off + 1);
- memcpy(u->comps[u->ncomps], p + off, e - off);
- u->comps[u->ncomps][e - off] = '\0';
- u->ncomps++;
- }
- off = e + 1;
- }
- ed_set_completions(&u->ed, u->comps, u->ncomps);
+ cfg = ui_config_snapshot_settings(snapshot);
+ u->ascii = cfg->ascii;
+ u->notify = cfg->notify;
+ u->window = cfg->window;
+ strlcpy(u->model, cfg->model, sizeof(u->model));
u->ready = 1; /* input may now be processed */
drain_input(u); /* keys typed before config land now */
u->dirty = 1;
ui_render(struct worker *w, struct imsg *imsg)
{
struct ui *u = w->ctx;
+ struct ui_config_snapshot *config = NULL;
struct imsgev_payload payload;
const u_char *p;
size_t len;
+ int status;
if (u == NULL)
return (-1);
fatal("get UI payload");
p = payload.data;
len = payload.len;
+ status = ui_config_rx_feed(u->config_rx, imsg_get_type(imsg), p, len,
+ &config);
+ if (status == UI_CONFIG_RX_ERROR)
+ return (-1);
+ if (status == UI_CONFIG_RX_MORE)
+ return (0);
+ if (status == UI_CONFIG_RX_COMPLETE) {
+ publish_config(u, config);
+ ui_config_snapshot_free(config);
+ draw(u);
+ return (0);
+ }
switch (imsg_get_type(imsg)) {
- case FUGU_IMSG_UI_CONFIG:
- set_config(u, p, len);
- break;
case FUGU_IMSG_UI_TEXT:
if (p != NULL)
append_delta(u, (const char *)p, len);
getmaxyx(stdscr, u->rows, u->cols);
u->scroll = -1;
u->status.state = UI_IDLE;
+ u->config_rx = ui_config_rx_new();
ed_init(&u->ed);
ti_init(&u->ti);
buf_init(&u->compose);
blob - /dev/null
blob + ecfeccb48792a5ba394cb7ee3b5b43308333d43e (mode 644)
--- /dev/null
+++ src/fugu-tty/ui_config_rx.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "xmalloc.h"
+#include "model_window.h"
+#include "proto.h"
+#include "ui_config_rx.h"
+
+struct ui_config_snapshot {
+ struct ui_config settings;
+ char **skills;
+ size_t nskills;
+};
+
+struct ui_config_rx {
+ struct ui_config_snapshot *pending;
+ int failed;
+};
+
+struct ui_config_rx *
+ui_config_rx_new(void)
+{
+ return (xcalloc(1, sizeof(struct ui_config_rx)));
+}
+
+void
+ui_config_snapshot_free(struct ui_config_snapshot *snapshot)
+{
+ size_t i;
+
+ if (snapshot == NULL)
+ return;
+ for (i = 0; i < snapshot->nskills; i++)
+ free(snapshot->skills[i]);
+ free(snapshot->skills);
+ free(snapshot);
+}
+
+void
+ui_config_rx_free(struct ui_config_rx *rx)
+{
+ if (rx == NULL)
+ return;
+ ui_config_snapshot_free(rx->pending);
+ free(rx);
+}
+
+static enum ui_config_rx_status
+ui_config_rx_error(struct ui_config_rx *rx)
+{
+ ui_config_snapshot_free(rx->pending);
+ rx->pending = NULL;
+ rx->failed = 1;
+ return (UI_CONFIG_RX_ERROR);
+}
+
+enum ui_config_rx_status
+ui_config_rx_feed(struct ui_config_rx *rx, uint32_t type, const void *data,
+ size_t len, struct ui_config_snapshot **result)
+{
+ struct ui_config_snapshot *snapshot;
+ struct ui_config settings;
+ const unsigned char *name;
+ size_t i;
+
+ if (rx == NULL)
+ return (UI_CONFIG_RX_ERROR);
+ if (result == NULL)
+ return (ui_config_rx_error(rx));
+ *result = NULL;
+ if (rx->failed)
+ return (UI_CONFIG_RX_ERROR);
+ if (type == FUGU_IMSG_UI_CONFIG_BEGIN) {
+ if (rx->pending != NULL || data == NULL ||
+ len != sizeof(struct ui_config))
+ return (ui_config_rx_error(rx));
+ memcpy(&settings, data, sizeof(settings));
+ if ((settings.ascii != 0 && settings.ascii != 1) ||
+ (settings.notify != 0 && settings.notify != 1) ||
+ settings.window < 0 ||
+ settings.window > FUGU_MODEL_WINDOW_MAX ||
+ memchr(settings.model, '\0', sizeof(settings.model)) == NULL)
+ return (ui_config_rx_error(rx));
+ rx->pending = xcalloc(1, sizeof(*rx->pending));
+ rx->pending->settings = settings;
+ return (UI_CONFIG_RX_MORE);
+ }
+ if (type == FUGU_IMSG_UI_CONFIG_SKILL) {
+ if (rx->pending == NULL || data == NULL || len == 0 ||
+ len >= FUGU_SKILL_NAME_MAX ||
+ rx->pending->nskills >= FUGU_SKILLS_MAX)
+ return (ui_config_rx_error(rx));
+ snapshot = rx->pending;
+ name = data;
+ for (i = 0; i < len; i++)
+ if (!isalnum(name[i]) && name[i] != '-' && name[i] != '_')
+ return (ui_config_rx_error(rx));
+ for (i = 0; i < snapshot->nskills; i++)
+ if (strlen(snapshot->skills[i]) == len &&
+ memcmp(snapshot->skills[i], name, len) == 0)
+ return (ui_config_rx_error(rx));
+ snapshot->skills = xreallocarray(snapshot->skills,
+ snapshot->nskills + 1, sizeof(*snapshot->skills));
+ snapshot->skills[snapshot->nskills++] =
+ xstrndup(data, len);
+ return (UI_CONFIG_RX_MORE);
+ }
+ if (type == FUGU_IMSG_UI_CONFIG_END) {
+ if (rx->pending == NULL || data != NULL || len != 0)
+ return (ui_config_rx_error(rx));
+ *result = rx->pending;
+ rx->pending = NULL;
+ return (UI_CONFIG_RX_COMPLETE);
+ }
+ if (rx->pending != NULL)
+ return (ui_config_rx_error(rx));
+ return (UI_CONFIG_RX_OTHER);
+}
+
+const struct ui_config *
+ui_config_snapshot_settings(const struct ui_config_snapshot *snapshot)
+{
+ return (&snapshot->settings);
+}
+
+size_t
+ui_config_snapshot_skill_count(const struct ui_config_snapshot *snapshot)
+{
+ return (snapshot->nskills);
+}
+
+const char *
+ui_config_snapshot_skill(const struct ui_config_snapshot *snapshot,
+ size_t index)
+{
+ if (index >= snapshot->nskills)
+ return (NULL);
+ return (snapshot->skills[index]);
+}
blob - /dev/null
blob + 59a520cfa1faae76c7a16fc10c9789de02eb3df1 (mode 644)
--- /dev/null
+++ src/fugu-tty/ui_config_rx.h
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef UI_CONFIG_RX_H
+#define UI_CONFIG_RX_H
+
+#include <sys/types.h>
+
+#include <stdint.h>
+
+#include "proto.h"
+
+struct ui_config_rx;
+struct ui_config_snapshot;
+
+enum ui_config_rx_status {
+ UI_CONFIG_RX_ERROR = -1,
+ UI_CONFIG_RX_OTHER,
+ UI_CONFIG_RX_MORE,
+ UI_CONFIG_RX_COMPLETE
+};
+
+/*
+ * Feed every UI render frame through this Interface. Unrelated frames are
+ * returned as OTHER only while idle; any interleaving inside BEGIN..END is a
+ * protocol error. ERROR poisons the receiver permanently and discards its
+ * staging. COMPLETE transfers an immutable snapshot to the caller. Freeing
+ * a receiver with an incomplete stream discards only its staging.
+ */
+struct ui_config_rx *ui_config_rx_new(void);
+void ui_config_rx_free(struct ui_config_rx *);
+enum ui_config_rx_status ui_config_rx_feed(struct ui_config_rx *, uint32_t,
+ const void *, size_t, struct ui_config_snapshot **);
+
+const struct ui_config *ui_config_snapshot_settings(
+ const struct ui_config_snapshot *);
+size_t ui_config_snapshot_skill_count(
+ const struct ui_config_snapshot *);
+const char *ui_config_snapshot_skill(
+ const struct ui_config_snapshot *, size_t);
+void ui_config_snapshot_free(struct ui_config_snapshot *);
+
+#endif /* UI_CONFIG_RX_H */