The three worst-covered files were genuinely untested, not edge cases:
os.dart 27%→~85% (inject the process runner so openURL/reveal don't spawn a
real browser), clide_code_block 39%→~90% (expose the byte→char span mapper
as a top-level fn + render tests), deeplink 29%→~75% (the confirm-opens and
not-activated paths). Buys buffer above the 95% floor so a feature batch
doesn't immediately trip the gate. 95.01% → 95.22%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The drawing-card feature batch (icon/image/compare/graph/d2 cards, --stdin,
tool resolution) added widget + wiring code that dipped total coverage under
the 95% floor — surfaced by `make release` (push-check skips coverage). Cover
the gaps: ProblemsController.refresh, every SVG shape-type bbox + the style
vocabulary, _spawnD2 via a real /bin/cat, the icon-show bus path + error
branches, and quad/arc/close marker paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
compareTemplateHandler lowers an `images` array to an SVG of side-by-side
<image> cells, each with the per-object data-label/description caption and
data-lightbox; paths resolve to absolute up front (injected, honest error
on a miss). _paintImage now aspect-fits (contain, centered) so cells of
differing-shape images don't distort. Flutter-free handler + painter pixel
tests. Card image-loading + registration next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rendered diagram leads; the d2 source carries through the draw bus and
folds into a collapsed ClideCollapserCard beneath the card (D-78 display-
only). en+nl catalogs. Also switches the d2 test to a null-aware element.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A data-lightbox element gets a tap target (click cursor) over its region;
DrawingCard exposes an onLightbox callback, which the conversation wires to
open the whole drawing in a ClideLightbox (zoomable). Completes the
per-object overlay — captions + lightbox — and T-318's defined scope.
Widget-tested (the tap fires the callback).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DrawingCard stacks a caption layer over the SVG: each data-label /
data-description annotation renders a themed caption just below its
element, mapped to pixel space via a shared svgViewportFit (refactored out
of the painter so the overlay and the paint use identical viewBox-fit
math). Display-only (IgnorePointer). Widget-tested. data-lightbox is
captured in the model; the tap-to-zoom interaction is the last follow-on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Display-only card (D-78): renders an SvgDocument in a framed, aspect-
ratio-sized region with an optional clide-themed label/description caption
beneath. The SVG is content (its own palette); the frame + captions are
chrome (SurfaceTokens). Widget-tested via the kernel fixture. Per-object
data-* overlay and lightbox remain follow-ons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
paintSvg/SvgScenePainter/SvgView take an optional SvgImageResolver
(href → decoded ui.Image); image nodes draw into their dest rect. The
caller owns loading (file/asset/network), so the painter stays pure
rendering. Threaded marker + resolver through a small paint context.
Pixel-probe tested (image draws via the resolver; nothing without one).
The clide-owned SVG renderer is now feature-complete for T-320.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collect <marker> defs (refX/refY, orient=auto, markerUnits) and the path
marker-start/mid/end refs in the builder, then paint them at the path
endpoints rotated to the path tangent (orient=auto). The viewBox→viewport
scale is approximated 1:1 (holds for d2's markers). d2 edges now render
with arrowheads. Covered by dart test (builder) + a flutter-test pixel
probe (the green arrowhead draws). image painting remains the last T-320
follow-on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
paintSvg walks the typed SvgDocument and draws it: shapes → ui.Path
(rect/rrect/ellipse/line/poly/path incl. arcs), text → TextPainter
(anchor + baseline), per-node Affine transforms, group opacity via
saveLayer, and viewBox fit (xMidYMid meet). Exposes SvgScenePainter and
the SvgView widget. image-href (async) and markers/arrowheads are deferred
follow-ons. Pixel-probe tested under flutter test (8 cases, incl.
rendering the real d2 fixture). Completes the clide-owned SVG renderer
core (D-103); not yet wired into a card.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tests for the epic's new code so the 95% floor holds: ClideSettings.i18n
string/interpolated null-safe + localizedCommandTitle; settings-renderer
localization (section/field/help/select/file, project-scope tag, didUpdateWidget);
extension-activation rollback + settings-contribution unregister; the Dutch
gate check; RootShell applying app.locale on boot; and prompt/conversation
tool-body + file-ref coverage. Also harden the menu toggle re-tap (capture the
button point while stable, then tapAt) so it no longer throws getCenter under
the serial coverage load.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The markdown span builders are context-free statics, so they pinned the
bundled clideUiFamily const and ignored the Appearance UI-font setting —
Claude's conversation prose stayed on the default face regardless of the
pick. Thread the resolved UI family through the same ClideMarkdownHooks
carrier already used for mono (T-472): build() resolves it from context and
every prose/link span reads hooks.ui. Adds a regression test asserting prose
and inline code follow the families from the ClideSettingsScope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce ClideSettings, a single widget-facing facade for the app's live
user preferences, namespaced by concern: ClideSettings.fonts.monoOf(context)
/ .fonts.uiOf(context), ClideSettings.theme.of(context),
ClideSettings.i18n.of(context). "Plumb once, use many."
Fonts are carried by a new root-provided ClideSettingsScope (resolved from the
font settings in root_shell, rebuilt on change); theme and i18n delegate to
their existing live providers (ClideTheme / the I18n service) so there's one
source of truth and their many consumers migrate incrementally rather than in
a big-bang. Reads outside a scope fall back to the bundled font defaults, so a
widget renders without a provider (isolated tests).
Migrate ~93 monospace-font call sites across 33 files from the clideMonoFamily
const to ClideSettings.fonts.monoOf(context) — pure refactor, identical family
when no override is set. 11 context-less helper sites (markdown static spans,
a few top-level/static builders) keep the const for now and are tracked in
T-472. Records D-101; updates the ui-design skill's font-family rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Under the Vim preset, `:` opens a transient one-line ex overlay running a
fixed v1 table; ZZ runs :wq directly. Completes the last built child of the
T-403 cross-pane vim layer (T-405 part 2 gt/gT still open).
- ExLineController + parseExCommand grammar + editor-targeted executors
(lib/kernel/src/ex_line.dart); the overlay (lib/widgets/src/ex_line_overlay
.dart) reuses the quick-open chrome, mounts in the root_shell Stack, and
publishes the exline.open scope flag. Unknown commands flash + stay open;
with no active buffer every command no-ops (2026-06-13 decision).
- :q closes the active tab via editor.close on its id — the registry promotes
the next buffer and the split self-collapses on the last (2026-06-12
decision); :w/:wq/:x/ZZ save (+close) the active buffer.
- :e <path> seeds quick-open (new QuickOpenController.open(seed:)); :N adds the
editor.goto-line IPC/CLI verb (reuses _offsetForLine). Goto needs caret sync:
EditorController now handles editor.selection-changed and the editor view
moves the caret on a selection-only change.
- `:` and ZZ are typed intents; the editor matcher and PaneKeyNav now bubble
unhandled typed intents to the app-root Actions, so they fire from any focus.
vim.yaml binds `:`, ZZ (shift+z shift+z), and Esc-dismiss.
Tests: parser/controller/executors, editor.goto-line daemon tests,
selection-changed (controller + view), full overlay widget test. make test
green; analyze + format clean.
Also files T-441 (drop bold from the ticket-id card label) and T-442
(sub-agent renders as 3 cards instead of one bundle) under the T-276 UI epic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TabContribution gains an optional iconColor honoured by the icon rail:
full-strength when active/hovered, dimmed (70%) when idle, so the tint
reads as identity without outshouting the active-state border. The
Claude Activity tab sets claudeAccent (#D97757) — nominative use per the
licenses.yaml trademark note (it marks Claude's own panel).
Filed and closed as a try-it-out (user request); trivially revertible if
the accent doesn't land visually.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ColumnHat was superseded by the hat bar in app.dart and survived only
through a zero-coverage smoke test. Its file also carried the live
hatHeight constant (D-57's 24px hats) consumed by the hat bar and the
menu bar — that moves to widgets/src/chrome_metrics.dart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both node types fell through the inline-span switch to an empty
textContent span: words on either side of a hard break glued
together, and images vanished with no trace. A br now emits a
newline; an img renders a muted italic "[image: alt]" placeholder
(falling back to the src) — no inline network loading in the owned
renderer; live-pane images keep going through clide image show.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The summarized button semantics (label, expanded/collapsed state)
wrapped the entire card with excludeSemantics, so every expanded
child vanished from the a11y tree — a screen-reader user could expand
a run and hear nothing inside it. The exclusion now wraps only the
header (ticker when collapsed, header row when expanded); inner item
cards stay readable, and the redundant background-toggle tappable is
explicitly excluded so the header stays the single AT stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).
Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
- whole-tree `dart format` reformat (tall style)
- `dart fix` for unnecessary_underscores + use_null_aware_elements
No runtime behaviour change; `make test` green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dart format was missed on this file in the T-300 commit; the pre-push
gate's --set-exit-if-changed caught it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Workspace file paths mentioned by Claude now linkify and open in the
editor: bare (lib/app.dart), with a line (lib/app.dart:42), backticked,
or as markdown links. Only paths that exist in the repo linkify — the
resolver gates on existence so prose (version numbers, "e.g.") stays
literal. Clicking maps to the editor.open verb, jumping to the line when
a :line suffix is present (D-6 parity).
ClideMarkdown gains resolveFileRef + onOpenFile hooks; conversation_view
resolves against the open project root + existsSync and dispatches over
IPC. Detection covers running prose, whole-content code spans, and link
hrefs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The image fills the 94% viewer with a transparent letterbox, so taps on
the dimmed canvas beside the image hit the viewer and did nothing — only
the 6% margin dismissed. Add onTapUp that dismisses when the tap lands
outside the actual painted-image rect (computed from the RenderImage +
applyBoxFit, transformed for any zoom/pan). Double-tap reset, scroll
zoom, pan, Esc, and the close button are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AnimatedSwitcher cross-fades exiting + entering glyphs for 200ms; with a
fixed per-status ValueKey, flipping a status back (running -> success ->
running within the fade, e.g. two bound Claude panes) left two children
keyed 'running' in the Stack -> "Duplicate keys found" + a cascade of
follow-on framework errors. Make the indicator stateful and fold a
per-change sequence counter into the key, so each appearance is unique and
a same-status rebuild still doesn't re-animate. Regression test flips
status mid-cross-fade with real animations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Image cards, thumbnails, the lightbox, and `clide image show` rendered
via Image.file, whose FileImage keys Flutter's imageCache by (path,
scale) only — so overwriting a file at the same path handed back the
previously decoded frame (hit live re-exporting a wireframe PNG). Add
ClideFileImage, a FileImage that folds mtime + size into ==/hashCode so an
in-place change is a fresh cache key (miss → re-decode), and route the
five Image.file sites through it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the 49 hand-maintained named consts with one generated
label→codepoint map (phosphor_glyphs.g.dart, 1512 glyphs from the glyph
table via tool/gen_phosphor_glyphs.dart). Feature code now references
glyphs by their exact kebab-case name — PhosphorIcons.byName('folder') —
with no raw codepoints; this also lets a Lua extension name an icon
without crossing the FFI boundary with a codepoint.
byName is total: an unknown name degrades to the `placeholder` box so the
bug is visible (it's a real error), while phosphor_glyphs_test asserts
every byName('...') literal in lib/ resolves — recovering the typo check a
const gave. Migrated the 89 call sites. Adds EmptyIconPainter for an
intentional blank that still reserves the icon box; ClideFilterBox gains
showIcon to keep the slot aligned when blank.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The activity run, edit run, and sub-agent run cards now render through the
shared ClideCollapserCard primitive instead of ClideHolderCard. The
collapsed ticker now leads with the card label, the count sits in a
fixed-width slot, and the status tick is pinned to the right edge.
ClideHolderCard (T-266) is fully superseded — removed along with its test
and golden; the deeper-control-passthrough coverage moved to the
ClideCollapserCard widget test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First slice of the conversation-panel card unification. Adds the
category-3 collapser primitive (lib/widgets/): a list of 1..N inner item
cards, collapsed ticker <-> expanded framed inner canvas, with the agreed
chrome — color drives the border + chevron/label tint, a fixed-width
counter slot, the status icon hard against the right edge, chevron hard
against the left edge, background + caret toggle (D-78 tail-follow). The
aggregate status/count/echoed-title are caller-computed, so the widget
stays free of conversation semantics; inner items keep their own per-item
status.
Also adds clide_card_metrics.dart — shared spacing constants (gap, radius,
header padding, counter slot width) for all three card categories.
Not yet wired into the stream (no user-visible change); migration of the
group/tool cards follows. Widget test + golden included.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clide://open?path=&line= translates (in parseArgv) to editor.open, so an OS
scheme invocation routes through the existing CLI→IPC path into the running
window — single-instance for free, no new code path. Registered the scheme:
linux/clide.desktop MimeType x-scheme-handler/clide (Exec already passes %U) and
macOS Info.plist CFBundleURLTypes. Parser validates the action/path/line.
Linux works end to end (the OS passes the URL as argv). macOS URL DELIVERY (the
AppDelegate openURLs callback → forward into this path) is a follow-up — the
scheme is registered, but the native hook needs a real macOS machine to verify,
so it's not shipped blind. Also drops a stray import in clide_markdown_test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add setSurfaceSize(tester, width) to the widget harness — the reusable way to
test width-sensitive layout at an ultrawide surface (a wide SizedBox under the
default 800px surface is clamped, so the view's physicalSize must be set), the
foundation T-241 asked for.
Ultrawide cases on the surfaces most prone to width-proportional bugs (the T-239
class): ClideMarquee stays static when a line fits a 3440 slot (it was only
tested narrow); the quick-open palette stays width-capped, not stretched edge to
edge. The status bar already covers 600+3440 (T-239).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
http(s) links (typed or autolinked) in the conversation now open via the OS URL
handler (OsBridge.openURL) on click, with a hover underline + pointer; non-http
schemes stay inert. Works across prose, lists, tables, and headings.
Refactor: ClideMarkdown's growing set of inline-interaction callbacks
(onRecordTap, onImageToken, onLinkTap) is bundled into one ClideMarkdownHooks
value threaded as a single param — no more per-callback threading, and the hooks
now reach every context uniformly (links/images previously only worked in some).
The public widget API is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A run of 2+ consecutive edits to the same file now folds into one ClideHolderCard
labelled '# edits' (coalesceEditRuns, run after groupConversation) instead of a
stack of cards; a different file or an interleaving step splits the run. Every
edit stays reachable on expand.
The holder gained an optional aggregate status. New owned primitives: ClideSpinner
(the logo mark, monochrome, 3D Y-axis rotation, reduced-motion-aware) and
ClideStatusIndicator (running→spinner / success→check / error→cross, with an
AnimatedSwitcher seam for a richer transition later — kept self-contained, not
built on ConversationCard's mark). The activity card shares the same indicator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The slash and @ typeaheads are near-duplicate caret-anchored completion
surfaces. Per the amended D-88 they share ClideTypeahead (not ClideMenu): the
host owns text parsing + completion; ClideTypeahead owns the anchored overlay +
suggestion list, driven by a suggestions list. Unlike a menu it does not
capture focus or install a barrier — the text field keeps focus — and an
optional nav controller drives the highlight from the field's key handler.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root-cause of the theme-picker friction: the primitive's focus model is fine
(keyboard nav reaches a ClideMenu through the overlay), but the shared harness()
uses Overlay(canSizeOverlay) + a zero-size MediaQuery, which mispositions an
anchored follower off-screen and defeats autoFlip.
- autoFlip now reads the real view size (View.physicalSize) instead of
MediaQuery.size, so it flips correctly even when MediaQuery is overridden.
- Drop the inner Align in the follower — it pegged the panel to a corner of the
full-screen follower box and broke hit-testing for non-top-left anchors, so
end-aligned menu items weren't mouse-tappable.
- Add anchoredHarness() — a properly-sized Overlay tree for testing popover
content (the remaining migrations will use it).
Tests: keyboard nav through the overlay, an end-aligned item is mouse-tappable,
and autoFlip flips below->above near the bottom edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nine surfaces hand-rolled the same anchored-overlay + row-list + barrier +
keyboard-nav pattern. Extract one owned primitive (no Material):
- ClideAnchoredOverlay (clide_anchored.dart): positioning + lifecycle —
LayerLink/CompositedTransformFollower or centred Positioned, side/align +
auto-flip on viewport bounds, full-screen tap-away barrier, OverlayEntry
bookkeeping, focus capture, Esc-to-close. Driven by a ClideOverlayController.
- ClideMenu + ClideMenuListController (clide_menu.dart): a dropdown-token row
surface (items + separators) with arrow/enter/escape nav, skip-disabled,
active mark, per-item colour/leading glyph, keepOpenOnSelect (live-apply),
and onArrowLeft/Right hooks. The nav controller is reusable by surfaces that
keep bespoke rows (typeaheads, quick-open).
Additive — no call sites changed yet. D-88 records the convention (new `design`
domain): anchored pickers build on these; modal pickers stay on DialogRouter.
Tests: clide_anchored_test (open/close, barrier, Esc, centred, clean dispose)
and clide_menu_test (list-nav skip/wrap, select + onClose, disabled, Esc,
keepOpenOnSelect; pure ClideMenuListController cases).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The status-bar footer marquee drove a raw Ticker whenever its content
overflowed, ignoring MediaQuery.disableAnimations — unlike the turn indicator
(T-273), which stops on the same flag. Two costs: reduced-motion users still
got the scrolling footer, and the perpetual ticker was the historical
pumpAndSettle-hang culprit (its tests cope by only ever pump()-ing).
Unify on the one mechanism: read disableAnimations in didChangeDependencies
(as running_indicator does) and gate the ticker on it via _syncTicker(). Under
reduced motion the marquee never starts the ticker (stops + resets if running)
and renders the child statically (clipped, no looped copy). Toggling the flag
at runtime starts/stops the scroll.
Tests: reduced-motion marquee does not scroll and pumpAndSettle completes (no
hang); flipping the flag off lets an overflowing marquee scroll again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Linkify bare ticket/governance refs (T-281, D-77, Q-5, R-2) in rendered
conversation messages so clicking one opens the record in its context-pane
reader — T- in the tickets reader, D/Q/R in the decisions reader — reusing
the existing `selection` MessageBus addressing (the same path clide ui open
and the panels use; D-6 parity already satisfied by `clide ui open`).
ClideMarkdown now linkifies bare refs in running text (paragraphs, lists,
headings, bold/italic), not just record-shaped markdown links. Matching is
word-boundary anchored so "T-shirt" (no digits) and "PT-281" (mid-word) stay
literal; `code` spans and `pre` blocks render verbatim and never reach the
linkifier, so refs inside code stay plain. The clickable span is shared
between bare refs and record-shaped links so both look and behave alike.
Tests: ClideMarkdown linkifier cases (bare T/D/Q/R tap fires onRecordTap,
T-shirt + inline-code refs stay plain, no-callback stays plain); conversation
view integration (clicking a bare ref publishes the reader-open selection to
the tickets/decisions reader).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sidebar/dock filter fields (the shared ClideFilterBox) had no CLI
peer — a one-way, UI-only affordance that broke D-6 parity. Add the
drive+observe verb `clide ui filter <address> [<text>]`, routed entirely
through the kernel MessageBus pub/sub so a box reacts to a published
message identically whether the trigger was a UI keystroke or the CLI —
keeping extensions first-class (no dispatcher→widget wiring).
- ClideFilterBox gains an `address`; when set it listens on `filter.set`
for its address and republishes its value on `filter.state`. Null
address keeps the box a kernel-free UI widget.
- FilterStateCache (new kernel service) caches the latest `filter.state`
per address — the bus has no retention, so this backs the observe-half.
- ui.filter: with text → publishes `filter.set` (drive); without →
reads the cache (observe). Honest toolError when there is no live UI.
- Address every box: decisions/tickets/files/git/output/problems panes,
the four search boxes, and the pql search/query/markdown inputs.
Addresses are the ids from `clide pane list` (e.g. decisions.panel).
settings.json: allow the `clide` CLI + relevant skills.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inline image cards (T-249) are often too small to read. Clicking one now
opens a full-screen lightbox: zoom (scroll wheel / pinch), pan when zoomed,
double-click to reset to fit, Esc / close button / backdrop click to dismiss.
ClideLightbox is a reusable primitive (lib/widgets/) over Flutter's
InteractiveViewer with clide-owned zoom gestures, shown via the DialogRouter
(dimmed backdrop, single modal at a time, D-78). The card stays display-only;
the click is a navigation gesture, not an inline control.
CLI parity (D-6): `clide image show <path> --fullscreen` opens straight into
the lightbox instead of injecting a card.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Non-modal operation-feedback toasts, bottom-right: a ClideToast card per
severity (success/warning/error/info), auto-dismiss (errors linger), queue
with a visible cap, slide+fade in, manual dismiss, live-region a11y.
ToastService is a MessageBus consumer — components raise a toast by publishing
to the 'toast' channel (publishToast helper), so emitters stay decoupled from
the UI. GitController's push/pull are the first emitters. ToastOverlay mounts
in the app-root Stack.
Also adds comprehensive GitController coverage: importing it for the toast
emitter test first pulled the whole file into the coverage denominator, so the
controller is now tested end to end (status/stage/commit/stash/push/pull).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Search tab's Find mode stacked four ClideFilterBoxes (search,
replace, include, exclude) that all looked identical: every box drew the
magnifying glass and the hint was only a semantics label, never visible
text — so they read as four blank search boxes. Render the hint as
placeholder text while empty, and make the leading icon optional (the
replace + glob fields pass icon: null). General win — every filter box
now shows its placeholder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rail was a fixed Row(center, max) — one button per tab — so adding
the Search tab pushed it 54px past its width and threw a RenderFlex
overflow. Center the icons when they fit and scroll horizontally when
they don't (LayoutBuilder + SingleChildScrollView + a minWidth floor),
so the rail stays correct at any tab count. (T-200)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tests for the quick-open overlay's keymap-intent handlers (nav,
accept, dismiss), the no-match / truncated / walk-failure hints, the
search panel's error + no-results states and toggle re-run, the
controller's failed-grep and exclude paths, and engine glob/regex-group
cases. Restores the coverage floor (95.20%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The find-in-files UI on top of the search.grep engine. A
FindInFilesController drives search.grep, accumulates streamed
search.match events (scoped to the active searchId, stale ids
ignored) grouped by file, and opens a match in the editor at its line.
The SearchPanelView contributes a sidebar tab: a debounced query box,
regex + case toggles, include/exclude glob fields, and a grouped
results list with the matched span highlighted.
findInFiles.open (Ctrl/Cmd+Shift+F) reveals and activates the search
tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A file picker overlay over the whole workspace, distinct from the
command palette. QuickOpenController holds the file list + a
subsequence fuzzy filter; the overlay loads the list via files.walk on
open, shows RecentFilesService entries on an empty query, and opens the
selection through a shared openWorkspaceFile helper (.md → markdown
reader bus, else editor.open) that the files panel now also routes
through, so recents stay in sync from every open site.
Bound to ctrl+p / meta+p with `when: !palette.open` so it never
collides with the palette's ctrl+p navigation; in-overlay arrows/enter/
escape reuse the palette's keymap-driven model via quickOpen.* intents.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 34s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 28s
ClidePane.didChangeDependencies/didUpdateWidget run in the build phase and
called FocusTracker.setStatusWidget -> notifyListeners() synchronously,
rebuilding the focus-listening status-bar item mid-build — Flutter threw
"markNeedsBuild called during build" on every frame once a Claude pane was
focused. The convey now defers to a post-frame callback when mid-build
(re-checking focus then) and applies immediately otherwise. The T-150
widget tests missed this because no focus listener was in their tree;
added a regression test with PaneContextStatusItem present.
T-159.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the MessageBus-based pane-context slot with a focus-driven one.
Panes keep their status widget locally; the FocusTracker holds the
focused pane's widget (activeStatusWidget) and ClidePane conveys it to
the shared slot only while its contribution is focused, re-conveying on
change and clearing on blur. The status-bar item just renders
focus.activeStatusWidget, height-clamped and marquee-scrolled when it
overflows. Removes the publish/subscribe race the bus version had.
T-150.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation for rendering the Claude conversation natively (T-132) with
the cross-widget select+copy the terminal gives today. Converts the raw
RichText in clide_markdown + clide_code_block to Text.rich, which
registers with a Flutter SelectionArea's selection machinery (raw
RichText does not). Adds a selectionBackground surface token
(globalFocus at ~40% alpha, matching the terminal's selection tint) via
tokens + resolver default; bundled palettes are untouched (D-69).
Text and code blocks now select across each other under a SelectionArea;
tables and tappable link-spans remain non-selectable islands for now.
The selection contrast pair is intentionally not added to the WCAG gate:
the tint is semi-transparent and the gate's neutral-grey compositor would
false-fail it (documented in contrast.dart); deferred to the -hc/-cb pass.
Co-Authored-By: Claude <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m5s
Plug widgets into the keymap layer landed in T-117.
ClideTappable:
- Wrap in `Actions(ActivateIntent → onTap)` outside a `Focus` so
dispatch from the focused context walks up and hits the action.
- Add a focus ring via `tokens.globalFocus` (DecoratedBox foreground
overlay, transparent border when unfocused, no layout shift).
- Disabled (`onTap == null`) skips focus traversal and shows the
forbidden cursor.
ClidePalette:
- Register Actions for the four palette intents
(selectNext / selectPrev / accept / dismiss).
- Publish `palette.open` scope flag via `KeymapService.setScopeFlag`
so when-clauses can scope future bindings to "palette only".
- Highlight the selected row with `listItemSelectedBackground`;
scroll it into view on nav.
- `PaletteController` grows `selectedIndex` + `selectNext` /
`selectPrevious` / `acceptSelected`; index resets on open /
filter change.
Intents.dart drops the `ClideIntent` base — `ActivateIntent` and
`DismissIntent` come from Flutter; clide owns the palette and text-
scale and command-bridge subclasses. `parseIntentId('activate')` →
Flutter's class; same for dismiss. Widget code uses the canonical
Flutter Intent types where they fit.
App root grows a PaletteOpenIntent action that calls
`services.palette.open()`, completing the ctrl/cmd+shift+p path
end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add tests for `keybindings.dart` (KeyEvent → Keybinding mapping,
parse-error edges, resolver entries view), `toolchain_paths.dart`
(the Flutter-free `ToolchainView.resolved` static view), and several
`widgets/src/` primitives: tooltip hover/overlay, palette filter +
submit, multitab controller `copyWith` + size getters, and additional
markdown branches (h3–h6 headings, tables, strikethrough, default
block fallback, record-link tap).
Unfreezes the pre-push coverage floor that was held at 90 on
2026-05-14 by mistake and ratchets to 93. Tidies eight test files
that had accumulated unused imports flagged by `unnecessary_import`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small additions to push coverage toward the 90% target:
- test/widgets/icons_test.dart: one sweep test calling .paint() on
every custom ClideIconPainter (Check, ChevronRight, ChevronDown,
Dot, Folder, Gear, GitBranch, Plug, Search, Terminal, Warning).
- log_test: Logger.trace covered at minLevel.trace + filtered out
at minLevel.info.
- services_bigger_test: DialogRouter.current getter; DialogHost
widget rendered with backdrop + inner builder, then dismissed
through the router. Plus FilesDropped subsystem/kind getters
exercised through the existing notifyDropped test.
Coverage 89.08% -> 89.93%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>