The catalog shipped with the mobile-app and freeform galleries only, so
four of the six categories opened empty — a filter chip that leads
nowhere reads as a broken panel, not an empty one.
Adds eight entries covering the remaining categories (two web pages, two
dashboards, two component systems, two modify instructions), each with
its bilingual body, a generated preview, and a 15-locale title key. The
catalog-size constant moves 1255 -> 1263 accordingly.
Starter prompts gain previews too: they were the first thing a new user
sees and were the only category rendering as bare text.
The native raster test now decodes four of the new previews instead of a
single hard-coded one, so a re-generated asset that regresses decoding
is caught for more than one image.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
The harness could only drive fresh generation, so the modify-category
prompts had no way to produce before/after thumbnails: their whole point
is what changes about an EXISTING document.
`OPENPENCIL_SMOKE_MODIFY_INPUT=<baseline.op>` runs a real document
through the same `build_modify_plan` -> `run_modify_turn` -> scoped host
apply path the desktop uses, so a thumbnail reflects the shipping code
rather than a harness-only shortcut.
The baseline is never overwritten — the output must go to a distinct
`OPENPENCIL_SMOKE_OUT` — and both files are SHA-256 addressed in the
summary, so a thumbnail set can prove every modify prompt started from
the identical input.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
A node whose stock search came back empty was parked in a terminal
failure state with no way back, so a design shipped with placeholder
slots even when a second attempt would have found art. Split the retry
policy out of `image_enrich_cli` into its own `retry` module and give
the session an explicit `retry_search_failures` entry point that
re-admits those nodes for a bounded, caller-managed retry.
Only Search/Auto nodes are re-admitted: an explicit Generate target that
failed is never silently converted into a stock search, since that would
substitute different art than the design asked for. `image_request_mode`
makes that distinction a property of the node rather than something each
call site re-derives.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
`plan_is_sidebar_dashboard` accepted a sidebar signal from ANY subtask,
so a landing page whose plan happens to carry a nav/menu section was
built on the two-column dashboard scaffold — a sidebar rail down the
left of a page that should be a full-width hero stack.
Require the signal to come from the FIRST subtask (a real sidebar is the
leading section, not an incidental one), and let a plan's landing-page
anatomy veto an ambiguous signal via `plan_has_landing_anatomy`. An
explicit landing-page request now vetoes every dashboard signal, while
an explicit dashboard or admin-console request still wins ahead of the
structural check, so the unambiguous cases are decided by what the user
asked for rather than by section keywords.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
A prompt that names its canvas size ("1200x800", "390 宽") had no path
to the root frame: planning always applied the desktop/mobile defaults,
and cleanup was free to grow the root past whatever was asked for.
Parse the request once (`request_dimensions`), apply it during plan
normalization, state it in the compact prompt so the model builds to the
same number, and carry a `preserve_requested_root_height` flag into
cleanup through an explicit `CleanupPolicy`. The policy defaults to the
historical behavior — only the fresh-root orchestrator path opts in — so
append and modify runs are untouched.
Cleanup needs the RESOLVED height to honor that flag without collapsing
real content, so `geometry_validation` grows `resolved_node_height`,
measuring the laid-out subtree against the node's own top edge rather
than trusting the declared value.
Planning corpus follows: the desktop sizes are labelled "Desktop
default", so an explicit request reads as an override rather than a
contradiction.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
The line grammar anchors every pattern on `\)$`, so an operation
written as `img=G(...),` was rejected as unparsable. A trailing `;` was
already stripped; a `,` was not — and the comma is the costlier miss,
because a model reaching for a list separator writes it on EVERY line.
All lines fail, the transaction rolls back, and nothing lands.
Measured 2026-07-31: five `G(...)` image fills rejected for one trailing
comma each. `Cannot parse operation` echoes the line without saying what
is wrong, so the model mis-diagnosed it as an ARGUMENT separator
problem, then started deleting and rebuilding subtrees it had already
committed — chasing node ids that were never stale, since ids are string
identities and a delete renumbers nothing. The design ended up worse
than before the failing batch. One rejected line is a retry; a rejected
batch is a demolition.
Only the line's own tail is trimmed, so a comma inside an argument body
is untouched: every real operation ends on `)`.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
`effective_path_env` always let the login shell's PATH lead. That is
right for a Dock/Finder launch, where the process inherits launchd's
stock PATH and the login shell is the only place the user's toolchain
exists — but wrong whenever the process PATH was customised, because
then it *is* the user's live intent and we were silently overriding
which binary gets resolved.
Measured: with two `codex` installs (an old npm global under homebrew
and a current one under nvm) and a `.zshrc` ordering homebrew first, the
app resolved the old binary while the user's terminal resolved the new
one. It reported a stale model catalog and, worse, rewrote the shared
`~/.codex/models_cache.json` with its own outdated list, so even the
cache fallback went backwards.
Decide the merge direction on a fact rather than a guess: if every entry
of the process PATH is a stock system directory it carries no intent and
the login shell leads; otherwise the process PATH leads. Login-only
entries are still appended either way, so nothing that used to be
reachable stops being reachable.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
`model_profile` declares `deepseek-v4-pro { thinking_disabled: true }`,
but the wire layer decided whether to actually send
`thinking:{"type":"disabled"}` from a model-name allowlist that only
covered MiniMax and GLM. DeepSeek matched neither, so the declaration
was silently dropped and every agent-loop turn leaked reasoning until
`max_tokens` ran out — which truncates a `batch_design` mid-JSON while
leaving the short read-only tool calls intact, so the transcript shows a
run of green tool calls and then simply stops.
The same list lived in three places (single-shot body, agent loop,
headless harness) and had already drifted: the harness matched GLM with
`starts_with` where production used `contains`, so a vendor-prefixed id
benchmarked with thinking on and shipped with it off. Collapse all three
onto `op_orchestrator::accepts_thinking_body_field`, next to the profile
table that states the intent, and add a guard test asserting every model
whose profile asks for thinking off can express that on the wire.
DeepSeek's field shape and its `effort=high` default are documented at
https://api-docs.deepseek.com/guides/thinking_mode/ — it is the same
`{"thinking":{"type":...}}` MiniMax and GLM take. Sending it
unconditionally is still wrong: a builtin provider may point at an
endpoint that rejects unknown body fields, so the table stays the
boundary and a new family is one line in one place.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
The cache parser filters on `visibility`, but the app-server parser
ignored the protocol's twin field `hidden`, so the two paths that feed
the same picker disagreed on what counts as a listable model. Today the
server withholds internal entries (`codex-auto-review`) from
`model/list` on its own, so this changes nothing against the current
build — it keeps a server that starts sending them from leaking an
unusable model into the picker.
Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x