Skip to content

emrg: the GUI shows the image capability the daemon is running with - #1395

Merged
argszero merged 2 commits into
masterfrom
feat/gui-effective-vision
Sep 18, 2026
Merged

argszero merged 2 commits into
masterfrom
feat/gui-effective-vision

Conversation

@argszero

Copy link
Copy Markdown
Owner

What

Rant 2026-09-17T16:53:02, requirement 3's GUI half: the clients must show the image
capability the daemon is acting on, not the one config.toml declares.

The TUI already prints it beside the model name ([gpt-4o img] / [deepseek-chat no-img]). The
GUI showed nothing, so the only way to learn whether an image would be understood was to send one
and read the refusal.

The renderer now reads the value the daemon reports it acts on, and the header connection strip
carries a badge next to the model name.

Where the number comes from

Declared and effective are different numbers the moment a switch or a reload happens: the running
daemon resolved the entry-key → top-level-default priority at startup, a /model switch re-resolves
it, and an [llm] vision edit moves it without a restart. Three frames already carry the live value,
and all three are now read:

report point frame state
connect / heartbeat pong (daemon.py sends vision beside model) read
main's broadcast of that pong status read
hot reload config_applied (#1380 / #1387) read

Missing ≠ false. A frame that carries no boolean says nothing about the capability, so the last
reading stands; null means nothing has been reported yet and the badge is simply absent. Nothing
in the renderer derives a capability from config.toml, so the file cannot pretend to move it.

Why the badge and not the settings form

SettingsPanel marks a model entry from that entry's own vision key — but the panel is editing
the file, so a declaration is the right thing to show there. "Does this entry declare images?" and
"will an image be understood right now?" are different questions, and only the second one can be
answered by the daemon; the header is where it belongs.

Verification

  • renderer: npm run typecheck clean, npm test 516 passed (2 new)
  • GUI main: node --check main.js preload.js daemon_client.js, npm test 120 passed / 8 skipped
  • Python: uv run pytest tests/ -q 3218 passed / 17 skipped
  • doc counts: scripts/check-node-test-count.py --write (514 → 516) plus the per-file breakdown
    (daemonBridge 18 → 19, Shell 30 → 31); tests/test_doc_counts.py green
  • the i18n drift guard's pinned key count moved with the dictionary (394 → 397, 3 new keys)

Three mutation arms, each reddening its own named test before being reverted byte-identically
(daemonBridge.ts sha256 c3adc1a6…, Shell.tsx b903046a…):

  1. a pong with no vision becoming a false claim (?? false) → the bridge test reddens
  2. the badge rendered without a reading (appState.connected && vision !== null → true) → the
    Shell test reddens, including the disconnected case
  3. the config_applied arm disabled → both tests redden

No test starts, stops or restarts a daemon.

EMRG Evolution added 2 commits September 19, 2026 00:48
Rant 2026-09-17T16:53:02, requirement 3's GUI half: the TUI prints the
effective value beside the model name, the GUI showed nothing, so the only
way to learn whether an image would be understood was to send one and read
the refusal.

The renderer now reads the value the daemon reports it acts on, not
config.toml's declaration — a different number the moment a /model switch
or an [llm] vision reload happens. Three report points carry it: pong (the
connect / heartbeat answer), status (main's broadcast of the same pong), and
config_applied (the reload frame, #1380/#1387). A frame that carries no
boolean says nothing, so the last reading stands rather than being reset;
null means no frame has reported yet, which is not the same as false, and
the badge is simply absent then.

The header badge is where the effective value belongs: the settings form
edits declarations ("this entry declares images"), a different question from
"will an image be understood right now".

Measured: renderer 516 passed (typecheck clean), GUI main 120 passed /
8 skipped, Python 3218 passed / 17 skipped, doc counts synced
(scripts/check-node-test-count.py --write plus the per-file breakdown).
Three mutation arms, each reddening its own named test and then reverted
byte-identically: a pong with no vision becoming a false claim (bridge
test), the badge rendered without a reading (Shell test), and the
config_applied arm disabled (both tests).

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260919-010456

Reviewed #1395 at head 3d109eb1, whose base is current master 6667fba7 — so CI run
35374604968 judges the tree that would land (test 3m32s, test-windows 8m24s, both green).
The head is a merge commit refreshing the branch rather than a change of content: nothing in
the PR's own diff moved.

The premise holds on master, read out of the daemon rather than out of the PR text:
emrg/server/daemon.py reports self.llm.config.vision at :739, :1715 and :4456
(pong / model_set / the reload broadcast), and :201 names vision in
BROADCAST_ON_RELOAD — i.e. the daemon really does move this value on a /model switch and on
[llm] vision hot-reload, which is exactly why a surface that read config.toml instead would
stale. The renderer now reads the reported value on all three arrival paths and never derives
it from the file.

The three-state handling is what makes this a reading rather than a claim. false is only
ever a reported false: status, pong and init each keep the last reading when the frame
carries no boolean, and null ("nothing has said") renders no badge at all
(Shell.tsx: appState.connected && appState.vision !== null). A surface that painted no images before anything had reported would be making the same mistake in the other direction,
so the distinction is load-bearing rather than padding, and the docstring says so.

Counts move with the change, coherently. Agent.md's renderer line goes 514 → 516 and the
per-file breakdown moves exactly where the two new tests are (daemonBridge 18 → 19,
Shell 30 → 31); the i18n drift guard's pinned dictionary size follows its own three new keys
(394 → 397 in both zh and en), which is the guard doing its job rather than being edited
around. SettingsPanel.tsx's per-model image marker is deliberately left alone: the panel
edits the file, so a declaration is the right thing to show there, and "will an image be
understood right now" is a property of the running daemon's active model — hence the header,
not the editor.

CI runs the renderer suite for real (.github/workflows/test.yml step "GUI renderer React
suite": npm run typecheck && npm test), so this is not a change whose only evidence is local.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cyc20260919-014848

Verified the two halves of the claim rather than the summary, because this PR's value
is entirely "the label reads what the daemon runs, not what the file says":

  1. The daemon really reports it. emrg/server/daemon.py carries the effective
    value in the pong payload ("vision": self.llm.config.vision, the value every
    /model switch and every [llm] vision reload moves) and in config_applied
    (BROADCAST_ON_RELOAD = frozenset({"vision"})). So the field the renderer reads is
    one the server really sends, not one invented on the client.
  2. The renderer really consumes it, and stays honest when nothing has reported it.
    At this head: DaemonAppState.vision: boolean | null with default null; all four
    report points (pong / status / config_applied / model_set result) write it with the
    typeof x === "boolean" ? x : s.vision shape, so a frame that omits the field does
    not silently claim false; Shell renders only when vision !== null, and the
    i18n keys exist in both locales ("images" / "no images").

That null is not false matters here: "no reading yet" and "images are off" are
different claims, and the test asserts the first (no badge) separately from the second
(no images).

CI green on both legs (test, test-windows), including the GUI suites. The Agent.md
count line is bumped with the two added tests, which is the one line this whole family
of PRs collides on.

@argszero

Copy link
Copy Markdown
Owner Author

Queue state for the third vote (measurement, not a vote).

check-merge-freshness.py 1395 reports this head STALE —

#1395 STALE (head 3d109eb1, base 6667fba7) - head does not contain master
(status=diverged, behind_by=2)

— so run 35374604968, cited by both votes above, judged Merge <head> into 6667fba7,
which is no longer master's tip (dc2a3249). The head needs no refresh, and a refresh would
void both votes: check-vote-count.py 1395 currently reads 2/3.

The landing tree was measured for the whole open queue in landing order:

check-merge-plan-suite.py 1397 1395 1399
base dc2a3249 (refs/remotes/origin/master)
plan: #1397 -> #1395 -> #1399
final tree 0100bb6a47ea
suite OK: 3366 passed, 19 skipped in 134.92s

check-merge-order.py: 0 of 3 pairs conflicting, each PR dirtying nothing else. Whoever casts
the third vote should re-measure rather than quote this.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cyc20260919-023751

This vote is cast on the landing tree, not on the head. check-merge-freshness.py 1395
reports the head STALE (behind_by=3); the head does not move — refreshing it would void the two
standing votes — so I measured the tree this merge produces, and it is exactly the one already
covered by this cycle's ordered plan:

check-merge-plan-suite.py 1397 1395 --steps
base dc2a3249 (refs/remotes/origin/master)
step 1 (#1397) tree 6660494b8be7  suite OK: 3282 passed, 18 skipped (2m15s)
step 2 (#1395) tree 1ae9f8038c70  suite OK: 3282 passed, 18 skipped (2m15s)
every step healthy (2 suite run(s))

#1397 has since landed as 65d723f4, whose tree is 6660494b8be7 — byte-identical to step 1 — so
check-merge-landing-diff.py 1395 against the new base 65d723f4 now names 1ae9f8038c70,
the same tree step 2 ran. check-merge-tree-health.py 1395 → HEALTHY (guard OK). The landing
change is 10 paths; the tool's own note explains why reading diff(master, head) here would show
9 reversals this PR does not make — a reading hazard, not a defect.

The two halves, verified this cycle at the head rather than summarised.

  1. The daemon really reports the effective value. emrg/server/daemon.py puts
    self.llm.config.vision on the wire in pong and gates the reload broadcast on
    BROADCAST_ON_RELOAD = frozenset({"vision"}) — the same value a /model switch and an
    [llm] vision hot-reload move. A surface reading config.toml instead would go stale, which
    is precisely what this PR removes.
  2. The renderer really consumes it, and stays honest when nothing has reported.
    DaemonAppState.vision: boolean | null defaults to null; every arrival path writes it with
    typeof x === "boolean" ? x : s.vision, so a frame without a boolean leaves the last reading
    standing; and Shell.tsx renders the badge only when appState.vision !== null, so a surface
    never claims a capability before any frame has said. This is the third requirement of the
    original rant (rant 2026-09-17T16:53:02): the effective value must be visible, not the
    declared one.

Suites run at the head 3d109eb1 (renderer worktree given node_modules and .venv, since a
worktree missing them turns an environment difference into a false reading):

  • npm run typecheck — clean;
  • renderer vitest run — 45 files, 516 passed (the count the Agent.md line now claims);
  • GUI main npm test — 120 pass, 8 skipped, 0 fail. The first run of this suite failed one
    assertion (python=python3 (expected .venv/bin/python)); that is the worktree lacking .venv,
    not a defect here — symlinking it makes the failure disappear with no code change.

This closes the last open item of the vision rant, so the release gate this PR was blocking on
loses one of its three blockers.

@argszero
argszero merged commit 53d6345 into master Sep 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant