Skip to content

feat(branding): the dark app chrome follows the brand colour - #824

Merged
ViaJables merged 36 commits into
ui-insight:mainfrom
arhyneRWU:feat/panel-chrome-follows-brand
Sep 8, 2026
Merged

feat(branding): the dark app chrome follows the brand colour#824
ViaJables merged 36 commits into
ui-insight:mainfrom
arhyneRWU:feat/panel-chrome-follows-brand

Conversation

@arhyneRWU

Copy link
Copy Markdown
Contributor

Stacked on #821. This branch contains that PR's commit as well; review only e472d211, or merge #821 first and this becomes a single-commit diff. Both touch color.ts, BrandingContext.tsx and index.css, which is why they aren't independent.

Summary

--color-panel-dark is a fixed #191919 in @theme that nothing derives from anything. So a deployment branded to navy gets navy accents sitting on neutral-black chrome — the workspace tab strip (RightPanel.tsx:31), the document-panel header (LeftPanel.tsx:242, commented "Black header bar"), the knowledge panels, the left rail. The brand stops at the accent colour and the shell stays default.

It's now derived from the brand's hue and saturation with the lightness pinned.

Why pinning lightness is the whole design

Tinting neutrals toward a brand colour is normally a bad idea: it makes contrast a runtime property of a value you don't control, and ThemePanel offers no contrast warning at all — an admin can pick #1a1a2e today and nothing objects.

Pinning the lightness sidesteps that. Only hue and saturation vary, so the chrome/text contrast is bounded by construction:

brand derived chrome vs white text
— (current) #191919 17.58:1
#163A64 navy #0f1d2e 16.99:1
#eab308 gold #2e260f 15.0:1
#808080 grey #1f1f1f neutral, saturation 0.0000
#ff0000 red #2e0f0f saturation capped at 0.508

Saturation is capped so a fully saturated brand doesn't read muddy; a greyscale brand comes back genuinely neutral rather than faintly tinted.

Unbranded deployments are unchanged

When highlight_color is still the shipped default, the variable is removed rather than set, so the :root neutral wins and the chrome is pixel-identical.

removeProperty rather than an early skip, deliberately: applyTheme runs once for the cached theme and again for the server's, so an admin resetting a brand back to the default has to clear the value this same call previously wrote. There's a test for that path, and it's what keeps the guard tests from being vacuous — it asserts navy is set first, then cleared.

This mirrors _THEME_DEFAULT_COLOR in backend/app/services/email_service.py:104-107 ("if highlight_color still equals this, the admin hasn't picked a brand color, so leave the email gold alone"), cited in the comment so it reads as an existing convention rather than a special case.

Changes

getPanelDark() in color.ts, one setProperty/removeProperty branch in BrandingContext, the @theme entry moved to the var(runtime, fallback) form, and 13 hardcoded #191919 backgrounds migrated onto the token so the chrome can't end up half-branded.

Seven live literals are deliberately left:

  • Both certification-badge gradients (CertifiedBanner.tsx:9, ActivityRail.tsx:439) — linear-gradient(135deg, #191919, #2d2d2d). The second stop is out of scope, so tinting only the first gives a navy→neutral-grey hue break, and migrating one badge but not its twin would desynchronise them. Doing these properly needs a second derived stop.
  • Four text colours that merely share the hex (FileList.tsx:110/129, FolderRow.tsx:161, FileRow.tsx:218) — tokenising a glyph colour to a chrome background variable would be wrong.
  • One 8-digit alpha form (ChatInput.tsx:149, #19191913) needing color-mix() rather than a swap.

One judgement call worth a reviewer's eye: ChatMessage.tsx:287 is a content surface rather than shell chrome. It's included because that bubble already carries borderLeft: 7px solid var(--highlight-color) — it's brand-aware already, so tinting its fill is coherent. Say the word and it comes out.

Test Plan

  • Shared checks pass (make ci) — frontend 756 passed / 102 files (746 at branch start; +10 is 6 colour-utility tests and 4 BrandingContext tests). tsc -b exit 0. eslint . 0 errors, same 20 pre-existing react-hooks/exhaustive-deps warnings, count unchanged. No backend file touched. No node/npm on my machine, so the frontend legs ran in a pinned, network-isolated container against the committed node_modules — no npm install, no lockfile change.
  • Release check — not applicable.
  • Manually tested — not run. No browser here. The contrast figures are computed and asserted, but whether navy chrome looks better than neutral black is a taste question a screenshot answers and I can't. If you think branded chrome is the wrong call for this product, that's a reasonable position and this PR is the wrong idea rather than a wrong implementation.
  • Updated CHANGELOG.md
  • Updated deploy/release docs — not applicable.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2

ViaJables and others added 15 commits September 1, 2026 11:18
Documents, uploads and links showed as pills under the chat header while an
attached knowledge base showed as a full-width bar above the composer, and a
folder — which scopes the chat and is sent on every message — showed nothing
anywhere. All three answer the same question about a conversation, so they
share one row.

The pill row was already the deliberate merge of chat uploads and file-browser
selection; the KB bar was never brought along. This finishes that.

- Type is icon + text tag + tint, never tint alone: --highlight-color is
  deploy-customisable and colour alone fails a colourblind reader. The KB and
  Folder tags preserve what the bar's "Knowledge Base: " prefix said.
- Scope chips (KBs, capped at 3; folders) render first and always; the
  unbounded document tail collapses behind "+N more" past six, so a heavy
  library selection cannot push the knowledge base out of view.
- FileBrowser reports selected folder titles alongside uuids, mirroring the
  document path, so a folder chip can name itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKDBCBvedFaffXibXnFM2g
…feedback)

Six changes from a sit-down with a DGA working through the course:

1. Module 0 opens with the big picture — what AI is, why it's in research
   administration, what the course delivers — before any terminology, and
   teaches structured vs. unstructured data as the course's central idea
   with no under-the-hood mechanics.
2. Jargon is defined in RA terms: JSON as "the fill-in-the-form format you
   read, never write", Token via attachment-size limits, and Module 1's
   pipeline lesson drops chunking/embedding/ChromaDB for "it reads the text
   and builds an index, like the back of a book".
3. Every module now carries a worked example — eleven new walkthroughs
   grounded in the modules' own sample documents.
4. Time estimates show everywhere they were missing: journey-map total,
   locked cards, and the open module's header ("your place is saved").
5. An explicit pop-out button opens the course in its own browser window
   for a second monitor, and the five cryptic mode icons get descriptive
   tooltips with the pin group visually separated.
6. The Module 1 lab now says the Run button stays greyed out until a
   document is ticked in the file browser — the exact stuck point.

Point 7 of the feedback (wrong duplicate-name message on import) is a
backend bug, fixed separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tbi5K9tPm3DNKC5VJyXyQ
An Explore import told a user she "already had a workflow with the same
name" while her personal library showed no such row — and she was right
to disbelieve it. The uniqueness scope counts more than the personal
library: a teammate's team-shared workflow, her own workflow filed under
the Team tab, and a workflow whose library bookmark was removed while
the object (and its name) lives on. The flat "already exists in your
library" message claimed all of these were sitting in front of her.

The 409 now resolves the conflicting workflow and names the case — team
library (hers or a teammate's), or existing-but-unlisted — and quotes
the stored name's exact capitalization, since the match is
case-insensitive and "budget analyzer" is invisible to someone scanning
for "Budget Analyzer".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tbi5K9tPm3DNKC5VJyXyQ
The recipe called `build`/`update` without `--repo`, so the tool discovered
the repository from the working directory. Under `git worktree` that is the
worktree, not the checkout holding the graph, so a run from one built a
second index there containing only the files that worktree had touched --
and then answered every query from it reporting `"status": "ok"`.

Measured on a working copy: four worktrees carried indexes of 7, 27, 26 and
6 files against a real 967. A partial index returns 0 for anything it never
parsed and a true 0 is indistinguishable, so `callers_of` on a live symbol
reports no callers, which reads as "safe to change".

The first `git worktree list` entry is always the main working tree, so one
graph now serves every worktree. ORIG_HEAD is read from that same tree,
since that is the tree being indexed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBMRynZ8iqpC7JKvwdBw9m
The page noted that semantic search "needs an extra" but not what happens
without it. It does not fail: `search` returns `"status": "ok"` with
`"search_mode": "fts"` -- keyword matching under a semantic name. A query
whose terms appear literally still looks right, so the degradation is
invisible until a conceptual query quietly returns nothing, which reads as
"there is nothing there".

The install line now takes `[embeddings]` and the setup adds the separate
`embed` step, since building the graph does not populate vectors. Three
`search_mode` values are documented -- `semantic`, `hybrid` (also correct)
and the degraded `fts`.

Also documents two traps that belong to the tool rather than the recipe:
`detect-changes` reads its diff from the same `--repo` path it reads the
graph from, so from a worktree it reports on main; and a symbol the graph
never indexed is answered exactly like one with no callers, which only
2.3.8+ annotate with a `confidence` field.

The network note now separates the local model -- downloaded once from
Hugging Face, then run on-device, sending no repository content anywhere --
from the optional remote backends, which stay off for the reason already
given.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBMRynZ8iqpC7JKvwdBw9m
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBMRynZ8iqpC7JKvwdBw9m
`token_safety_margin` gains a rung between the tiktoken-is-exact check and
`DEFAULT_TOKEN_SAFETY_MARGIN`. When a provider has counted the request, the
margin becomes the measured ratio of its count to our local one, rather than
the 1.5 that covers the worst content anyone measured.

Deliberately a ratio and not a total. The margin is applied per component,
`BudgetPlan.total_input_tokens` is a computed property summing those parts, and
the planner recounts on mutated text inside its trim loop. A recorded total
stops being true the moment anything is trimmed; a ratio stays approximately
valid.

Clamped at 1.0: a provider counting below tiktoken may only reduce
over-inflation, never reclaim window. Rejected outright on a model-name
mismatch, a zero baseline, a non-positive count, or a non-finite ratio, each of
which falls through to the existing ladder — so an absent or broken measurement
is the 1.5 guess, by construction rather than by a second code path
remembering. The "estimated, not exact" warning stays quiet only when the
measurement actually supplied the margin.

Inert until a caller passes one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
`_sized_for` re-derived the current model's safety margin from its name and
config in order to restate a request in a candidate's units. That is correct
only while every count comes from the same estimate-plus-default path.

A provider-counted request carries a much tighter margin, and
`token_safety_margin` cannot know that happened — it still answers 1.5. Dividing
an already-tight number by a factor never applied understates the request by the
whole difference, at the exact point the router is deciding whether a candidate
can hold it. That is the ui-insight#648 defect — an estimate that reads low, so the
request hard-fails — relocated to the routing boundary, which this function's
own docstring says it exists to prevent.

Callers may now state the margin their number was measured with.
`choose_document_model` and `suggest_document_model` pass it through; the
candidate's margin stays derived, because the candidate has not been counted.
Omitted, every caller keeps today's arithmetic to the token.

A supplied margin below 1.0, non-finite, or not a number at all is refused in
favour of the honest derivation, matching how `context_budget._configured_margin`
refuses the same thing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
New `native_token_count`: fetches a provider's own count for a prepared
request, or reports plainly that it could not.

Two properties, and the second is the load-bearing one. A returned figure is the
provider's own, unmodified. Anything going wrong returns nothing at all — the
caller drops its safety margin on the strength of a usable result, so a count
that comes back zero, short, or from a provider that counted half the request is
worse than no count, converting a conservative over-estimate into a confident
under-estimate.

So: gated before any I/O on a protocol that can actually count, since
`OpenAIModel.count_tokens` raises and every OpenAI-compatible model would
otherwise decrypt a key and build a client per chat turn to learn that. Wrapped
in a five-second timeout that is deliberately not the request's own settings —
`build_thinking_model_settings` sets 120s and the shared client retries a 429 six
times honouring Retry-After up to 60s, which would put a two-minute stall in
front of every message. Blanket `except` by necessity: Anthropic raises
ModelHTTPError, Google's genai errors are unwrapped, and a misconfigured
protocol reaches OpenAIModel; there is no complete and stable union of those.
CancelledError passes through, being the caller going away rather than a failure.

Google's count omits the system instruction — pydantic-ai attaches
`system_instruction` only for providers other than `google-gla`, and an
api-key GoogleProvider is `google-gla`. `covers_system_prompt` says so rather
than letting a caller silently drop a multi-kilobyte grounding preamble from its
budget.

Nothing imports this yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
Wires the native count into `chat_stream`. Before the request is sized, the
provider is asked what the assembled prompt actually costs; the same payload is
counted locally; the ratio becomes this turn's safety margin in place of the 1.5
guess.

The payload counted is the whole user prompt, documents included. `_build_chat_prompt`
is the existing assembly extracted verbatim so the pre-flight counts the request
chat is about to make rather than a reconstruction of it — in document chat the
documents are the payload, and they are also the digit-dense content the 1.5 was
sized for. Sampling the question and history alone would measure prose and apply
the answer to a budget table.

The baseline covers exactly the components the provider counted, which differ by
provider: Anthropic's count includes the system prompt, Google's does not for
api-key providers. Counting it on one side and not the other would depress the
ratio for a reason unrelated to tokenizer divergence, and a depressed ratio
clamps to a margin of 1.0 over a tiktoken figure that itself under-counts these
models — the hard-fail direction.

Routing is given the margin the number was measured with, rather than
re-deriving it. That value survives a model switch on purpose: it describes how
`requested_input_tokens` was measured, and that does not change when the model
does.

A count for one model is not passed to the planner for another. `_native_margin`
already rejects on a name mismatch, but relying on that silently is how the next
person learns it the hard way.

Anything unusable — ineligible protocol, timeout, 429, malformed response — leaves
the stream exactly as it is today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
The sign-in, password-reset and invite-acceptance pages hardcoded Vandalizer's
gold instead of reading the configured brand colour. A white-labelled install
therefore greeted new users with its own logo above a button in someone else's
brand — on the first screen anyone sees. Landing.tsx was migrated for exactly
this reason; these surfaces were missed.

Doing it safely needed a token that did not exist. `getAccessibleOnLight`
darkens the brand colour until it passes contrast on white; nothing did the
same for the near-black auth and footer surfaces. Without it, migrating a
deployment branded to anything dark would trade a legible gold for an
unreadable version of its own colour: RWU navy #163A64 measures 1.72:1 on
#0a0a0a, against 4.79:1 for the lightened variant, hue preserved.

Seventeen buttons paired the gold background with hardcoded `text-black`. Those
move to the runtime-computed contrast colour in the same edit — changing the
background alone is how black text ends up on navy.

Deliberately left: the footer's "Powered by Vandalizer" credit, GPL link and
NSF acknowledgement (the obligation is the words; only colour classes changed),
two SSO buttons whose black text sits on permanent white, and one
`hover:text-[#d49e00]` on a dark surface — `--highlight-hover` is a *darkened*
brand colour and would read worse there than what it replaces, so a correct fix
needs an on-dark hover variant that does not exist yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
`--color-panel-dark` was a fixed #191919 that nothing derived from anything, so
a deployment branded to navy got navy accents on neutral-black chrome — the
workspace tab strip, the document-panel header, the knowledge panels, the left
rail.

Derived now from the brand's hue and saturation with the lightness pinned. That
pin is what makes it safe rather than reckless: contrast against the white
chrome text stays predictable whatever an admin picks, where tinting neutrals
freely makes contrast a runtime property of an arbitrary input. Navy #163A64
gives #0f1d2e at 16.99:1 on white, against #191919's 17.58:1. Saturation is
capped so a saturated brand does not read muddy; a greyscale brand comes back
neutral rather than faintly tinted.

Thirteen hardcoded #191919 backgrounds move onto the token, so the chrome cannot
end up half-branded — including LeftPanel's "Black header bar", which is the
surface that prompted this.

Unbranded deployments are unchanged. When highlight_color is still the shipped
default the variable is *removed* rather than set, so the :root neutral wins.
removeProperty and not an early skip: applyTheme runs for the cached theme and
again for the server's, so an admin resetting a brand back to the default has to
clear the value this same call previously wrote. Mirrors the _THEME_DEFAULT_COLOR
guard in email_service.py, which protects branded email for the same reason.

Left alone: both certification-badge gradients (the second stop is out of scope,
and tinting one end gives a hue break), four text colours sharing the hex, and
one 8-digit alpha form needing color-mix() rather than a swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HucxUJ7gaA6iKveF8aruW2
…on catch it

Extractions and workflows failed against a vLLM model registered under its
HuggingFace repo name — "Native structured output is not supported by this
model." on every run — while chat worked and the same weights under a bare
name were fine.

JSON-schema output is a property of the server: vLLM enforces `response_format`
via guided decoding for anything it serves. The profile answered for the model
family's own hosted API instead — `Qwen/Qwen3-32B` resolved through OpenRouter's
family map to a profile leaving `supports_json_schema_output` at its False
default, and pydantic-ai refused the request. VLLMProvider now declares the
capability for every model it serves, keeping each family's own schema
transformer.

The per-model "supports structured output" toggle now does something. It was
written by the model editor and read by nothing on the extraction path, so
switching it off — the obvious escape hatch here — changed nothing.

The reason none of this was visible: the admin Test button ran one free-text
completion, so it went green for the entire outage. It now makes a second,
schema-constrained round trip using the same output-mode decision the
extraction engine makes (shared as one function, so the diagnostic cannot pass
on a configuration a real run fails on), and reports a model that chats but
cannot do that as failing. "Connected" and "usable" are different claims.

Two supporting fixes from the same incident:

* An unsupported output mode is classified with its two real remedies instead
  of falling through to "read the raw error".
* The Endpoint step reports the URL actually dialed, not the one typed — the
  vLLM and Ollama providers append `/v1` to a stored endpoint and the OpenAI
  one does not, so changing the protocol dropdown silently changed the URL.

Separately, found while building a model to test that: the external-OpenAI
branch passed `openai_client=` to a constructor taking only provider/profile/
settings, so any model added through the "OpenAI" or "Custom" setup preset
raised TypeError before a request left the server. No test built a model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ3BAa7MWo8CySegCumLEC
The migration moved the auth inputs onto the raw brand token, but these
inputs also set focus:outline-none -- which suppresses the global
:focus-visible outline -- so the ring is the entire focus indicator. On a
dark brand (#163A64 over #0a0a0a) it computes to roughly 1.3:1: a
keyboard user on a white-labelled deployment cannot see where they are.
Gold was visible, so this was a regression introduced by the branding
change, and only on the deployments the change exists to serve.

14 focus rings and 3 spinner borders move to the contrast-corrected
--highlight-on-dark. The raw token stays correct on the app's light
surfaces; it is wrong only against near-black.

AuthLayout's ambient glow was still bg-yellow-600/10 -- a blurred wash
across 40% of the viewport behind every auth page. The audit grepped for
#f1b300 and named Tailwind yellows did not match, so a navy sign-in page
kept a large yellow halo. It now follows the brand.

The added test asserts the on-dark token is present AND the raw one
absent, since the raw token is right elsewhere and wrong here -- pinning
the distinction, not the presence of a class. Verified by reverting the
component: both new cases go red, and green again with the fix.

Not changed: ProtectedRoute's spinner has the same raw-brand border, but
its container declares no background, so whether it renders on dark is
unclear. Left for a follow-up rather than changed on a guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtRYNNEdrq6SPyPt8PjH1v
ViaJables and others added 14 commits September 4, 2026 12:52
The module coalesced the failure log but not the failure work. A
deployment whose egress to the count endpoint is slow, or whose org is
rate-limited there, paid the full 5s timeout on every chat turn ahead of
the first token -- permanently, and after the first occurrence it said so
only at DEBUG. There was no metric and no way out short of a restart.

Three consecutive failures now pause a model for five minutes. The check
sits after the protocol gate, so an ineligible model never enters the
circuit, and before any model construction, key decryption or request, so
an open circuit costs nothing. One request is let through when the
cooldown elapses, and any success clears the record, so an outage cannot
disable counting until the next deploy. State is per model name: a
failing Anthropic endpoint does not stop Gemini being counted. The clock
is monotonic, so a clock adjustment cannot strand a model in cooldown.

Skipping is strictly better than blocking here: the estimate path it
falls back to is the one that shipped before this feature, so a paused
model gets the same budget it would have got anyway, without the wait.

Two corrections to the changelog, both things it asserted that the code
does not do:

- "fires only for models an admin explicitly configured" is not true of
  Gemini. detect_api_protocol routes any name containing "gemini" to
  google on its own, so a deployment already running one gets the
  pre-flight on upgrade without changing anything. Issue ui-insight#816 said this
  correctly and the entry lost it.
- "one unconditional round trip" is no longer true, which is the point of
  this commit.

The existing failure-logging tests needed updating rather than working
around: _CIRCUIT is per-process state exactly like _UNAVAILABLE_LOGGED,
so it joins that autouse reset -- without it, whether a test sees an open
circuit depends on collection order. The repeated-failure test now runs
50 turns rather than 4, pinning the bound instead of an arithmetic
coincidence.

Verified by disabling only the gate while keeping the state: the provider
is called on all 10 turns and the test fails on the count. Stashing the
whole module instead only proves a symbol is missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtRYNNEdrq6SPyPt8PjH1v
…ktree

Three things on top of the worktree fix, which is right.

`awk '/^worktree /{print $2}'` splits on whitespace, so a checkout at
"/Users/me/My Repos/vandalizer" resolved to "/Users/me/My". That is not a
cosmetic truncation: the graph.db test then fails, the recipe takes the
build branch, and `build --repo /Users/me/My` points the tool at a
directory outside the repository. `sed -n '1s/^worktree //p'` takes the
rest of the line whatever it contains.

The Setup block's `code-review-graph embed` carried no --repo, so it had
exactly the bug this PR fixes: run from a worktree it discovers the
worktree, creates a second graph there -- the directory the paragraph
below tells you to hunt down and delete -- and the shared graph never
gets vectors, so search keeps answering in fts mode with nothing saying
why. The PR's own defect survived its own fix.

One graph shared by every worktree means concurrent refreshes now contend
for one SQLite file, where per-worktree databases could not. This repo is
routinely worked from several worktrees at once, so that is the normal
case rather than an edge one. The recipe takes an flock where one exists;
macOS ships none, so the docs say to refresh one worktree at a time
there. `set --` builds the argument list once so the locked and unlocked
branches cannot drift.

The docs also now say what pinning --repo costs: the graph reflects the
main checkout's HEAD, so a symbol added on a worktree branch is absent --
and an absent node is indistinguishable from a real "no callers" answer,
which is the failure this tool is most likely to mislead a reviewer with.

Verified: the sed keeps "/Users/me/My Repos/vandalizer" whole where the
awk truncates it; the path survives as a single argument; make parses the
recipe; review-graph is still a prerequisite of nothing and CI still
never runs it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtRYNNEdrq6SPyPt8PjH1v
…ot the workspace

/certification is a redirect that opens the panel in whatever mode the
origin window last persisted, so the 1080x860 pop-out showed the entire
workspace with a floating panel. The pop-out button now asks for
fullscreen via ?panel=fullscreen and the redirect honours it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
… own workflow bookmark

LibraryItem.find_one({item_id}) matched any bookmark of that object,
including a teammate's team-library row, so a workflow the user had
removed from their library could still be reported as listed in it.
Filter by kind and by who added it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
…tes up front

mkdir -p runs before the tool's first build, so the tool may never write
its own .gitignore into the directory; without this the flock lock file
shows up as untracked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
…tually breaks

Only steps that route through the extraction engine use NativeOutput;
LLM prompt steps answer in free text and keep working.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
ViaJables and others added 7 commits September 8, 2026 12:29
…r Unreleased, not v4.12.0

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
getPanelDark on an empty or short highlight_color produced a non-colour,
and var(--panel-dark, #191919) does not fall back for an invalid value,
so the whole app shell painted transparent. Treat anything that is not
#rrggbb like the default. Also files the CHANGELOG entry under Changed:
the chrome following the brand is new behaviour, not a bug fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qk88vvuDRJz5Nft2pRNpeY
@ViaJables
ViaJables merged commit e53ca93 into ui-insight:main Sep 8, 2026
11 checks passed
@ViaJables

Copy link
Copy Markdown
Collaborator

Merged after review. Two things committed on the branch before merge, and one follow-up:

  • Committed: applyTheme now treats anything that is not a #rrggbb colour like the default and clears --panel-dark. getPanelDark('') produced a non-colour, and var(--panel-dark, #191919) does not fall back for an invalid value, so a malformed stored theme painted the whole shell transparent. Test added.
  • Committed: CHANGELOG entry moved from ### Fixed to ### Changed — the chrome following the brand is new behaviour, not a bug fix. Also re-applied the WorkflowEditorPanel swap at main's new location of the "Add task" button.
  • Follow-up: the on-dark accent from fix(branding): a white-labelled sign-in page shouldn't show our gold button #821 is computed against #0a0a0a, not the tinted chrome, so with a navy brand, navy accents on navy-tinted chrome get slightly less distinguishable than on #191919. A follow-up should compute an on-chrome accent.

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.

2 participants