A waiting user asks two questions that a plain LinearProgressIndicator
answers as one: how far along is it, and is anything still happening.
The widget keeps them apart - the value may stand still for minutes
while a highlight driven by 'busy' keeps crossing the bar, so a
stalled percentage no longer reads as a frozen app.
Also: glides to new values over 600 ms so the polling interval behind
it stays invisible, refuses to walk backwards, renders indeterminate
rather than an empty bar when no value is known, stops animating once
the work ends, and honours reduce-motion.
Eight guards in test/progress_bar_test.dart cover each of those rules.
Writing them found a real defect: with busy: false the late-final
controller was first constructed inside dispose(), where the ticker's
context lookup is already unsafe.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
New ChainInlineHelp (intro strip: what this is + what will happen, with
an optional 'Learn more' into the doc sheet) and ChainFieldHelp /
ChainFieldLabel (a '?' affordance per field). First applied to the
add-satellite dialog, which asked for a bare 'name' with no hint of
what a satellite is or does (usertest): it now leads with a plain
explanation + a federation 'Learn more', and the name field carries a
'?'. Both widgets are quiet by design.
Verified: field_help_test covers the widgets + that the dialog explains
itself; dialog_shots_test.dart (a reusable headed dialog-capture
harness) proved the layout in light + dark. Studio 0.76.0.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The same single-select choice pattern appeared as four widgets:
audit's hover pills, the store's SegmentedButton, the store filter
dialog's ChoiceChips, and the approvals TabBar (usertest finding
#14 / night-log decision 'pill segment as canon'). The audit
pattern is promoted to a shared ChainSegments widget (optional
icons, hover, selected border, button+selected semantics) and all
four sites use it; approvals switches lists via IndexedStack so
both stay alive and switching does not refetch.
Guard per the no-bugfix-without-a-guard rule: widget tests for
selection + semantics, plus a canon sweep that bans
TabBar/TabBarView/TabController/SegmentedButton/ChoiceChip from
lib/ (comments exempt). Deliberately out of scope: the flow
editor's Graph/Text/Run tabs live in the separate editor package.
Studio 0.75.0; guide images regenerated, dark + light verified.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The widget suites used to talk to whatever listens on the real
endpoint — results depended on the operator's machine (a running
hub fed real data into a11y/responsive runs and its gRPC channel
timers caused the historic flake). Hardening round:
- HubService.instance is now injectable (debugSetInstance);
FakeHubService (test/support/fake_hub.dart) answers every member
the pages touch with healthy-empty defaults and scripts per-RPC
failures (UNIMPLEMENTED / UNAVAILABLE / detached gate) through a
GrpcError-shaped fake. Unimplemented members are recorded and
fail the sweep with the exact list.
- state_matrix_test.dart pins the app-wide invariants for every
sidebar page x hub condition: healthy => no unreachable claims
and no raw error text; hub gone => honest unreachable states;
UNIMPLEMENTED => never 'not reachable' while the sidebar shows
connected; detached gate => plain-language feature-off state.
- a11y + responsive sweeps now inject the fake (hermetic); the
6-minute idle-timer drain workaround is gone with the cause.
Real bugs the new sweep caught immediately:
- every data page (store, doctor, audit, approvals, federation)
folded ANY load failure into 'hub not reachable' — the runs-page
bug class; they now share HubLoadErrorView, which classifies
into unreachable / needs-newer-hub / load-failed-with-copyable-
detail (new generic DE+EN strings)
- the approvals page's hidden tab had no future listener: a load
failure there surfaced as an uncaught async error
- the audit status bar rendered the raw gRPC error wall verbatim;
it now shows the classified friendly headline (still selectable)
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
One-click installs showed no trust signal at all (top security
finding of the usertest panel). Every install path — store card,
detail sheet, and the flow list's quick fix — now routes through
one confirmation dialog showing what the hub actually knows
before download: origin store, version, license, maturity, and
required services/capabilities, plus the sandbox model and an
honest note that per-entry signature status is not in the store
index yet (verification happens hub-side at install). Widget
tests cover content and confirm/cancel semantics.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The workspace switcher now lists the operator's sealed areas (read from
~/.chain/sealed/ manifests, the same source the CLI uses) below the
shared projects, each with a lock icon and a running/stopped status.
Selecting one is a real connection switch: Studio reconnects its hub
client to the area's own port with a full state reload — one window,
one truth. A stopped area is started first (chain project start) with a
visible notice; a failure surfaces as a copyable error and rolls back to
the shared hub.
While in a sealed area an identity bar under the AppBar is painted in
the area's accent colour and names it, with a one-click Leave back to
the shared hub. The area colour is marking, not theming — Studio's blue
stays the app accent. Selecting a shared project from inside an area
switches the connection back first. The sealed connection is never
persisted across restarts.
New: SealedAreaService (manifest + PID discovery), Workspace sealed
switch logic, ChainSealedIdentityBar, SystemActions.chainProjectStart.
l10n DE+EN. flutter analyze clean; 33 tests green (switcher lists sealed
with lock+status, pill shows active area, identity bar renders in the
area colour). Runtime plumbing (discovery, start, endpoint, reach)
verified headlessly against real sealed instances under a redirected
HOME; the identity-bar screenshot is deferred (display click-automation
failed after sleep on the shared desktop — an environment issue, not a
code gap; the visible components are widget-tested).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Multi-project stage 1 against the shared hub (platform design
docs/architecture/projects.md, § Studio):
- ChainWorkspaceSwitcher in the Audit + Approvals AppBars: lists the
registry (colour dot per project, shield for protected, honesty
tooltip), 'All projects' stays reachable — a filter, not a jail.
Selection is persisted and shared via the Workspace notifier.
- Audit page: list query AND live stream re-scoped hub-side on switch.
- Approvals page: pending + history scoped; the sidebar badge counts
the active workspace's pending approvals.
- Flow runs are stamped with the active workspace; a flow file
carrying its own project: keeps it (file wins, CLI semantics).
- Data layer: listProjects/ProjectRef; project fields on AuditEvent,
PendingApproval(+Record), SavedFlow; project params through
HubService. l10n DE+EN. Widget tests for the switcher contract.
Visual verification (light+dark screenshots) still pending — the
shared desktop was in active use; code paths are covered by
flutter test (26 green).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
A store icon in the Store app bar opens a dialog that lists the
configured module stores + the bundled seed (with per-source module
counts), lets the operator add a store by index URL (the hub fetches +
merges it live so its modules appear immediately), and remove a store.
Backed by the new ListStores/AddStore/RemoveStore RPCs + SDK methods.
This is how a domain app's published modules (e.g. reclaim's) become
visible in the Store without touching the CLI.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The Studio design system, widgets and helpers carried a Fai* / fai_
prefix (FaiSpace, FaiColors, FaiTheme, FaiLog, 17 fai_*.dart files, the
faiBinary* l10n keys). Studio is the Ch∆In product, so rename them to
Chain* / chain_ — carefully preserving English fail/failure/failed.
Also fix stale references: the 'fai' binary in l10n strings -> 'chain',
FAI_* env vars (FAI_BIN/DATA_DIR/MODULES_DIR/TODAY/BOOTSTRAP_TOKEN) ->
CHAIN_*, fai_platform -> fai_chain, fai_hub -> chain_hub. Vendor
security-hook tooling (FAI_BANNED_TERMS_FILE) + the .fai bundle ext left.
flutter analyze + test: clean (20 passed).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Studio follows the platform rename: product branding F∆I -> Ch∆In in UI
strings, command examples fai -> chain, and — critically — the spawned
hub binary path ~/.fai/bin/fai -> ~/.fai/bin/chain so Studio launches
the renamed binary. The fai_* Dart identifiers (FaiLog, widget files,
the generated SDK) stay = vendor/internal namespace. flutter analyze:
no issues.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
- Connection-aware Welcome: when the hub is down, show a hero with a
primary "Start hub" CTA + install fallback instead of a dead,
all-unchecked onboarding checklist (the first-run cliff).
- Actionable binary-not-found (file picker + install link, not a
"set FAI_BIN" dead end) and a connect-failure banner after
repeated failed health polls.
- Localize six hardcoded English error/toast clusters (DE+EN ARB).
- Bundle Inter + JetBrains Mono as assets; drop the runtime
google_fonts fetch (air-gap / KRITIS safe, no font-swap flash).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Bundles the error-UX overhaul and the inline log viewer:
- New `FaiLog` (`~/.fai/logs/studio-errors.log`, 256 KiB rotation,
JSON-per-line). Every operator-visible failure is appended so
`fai admin doctor` and the new viewer can show the trail
without the operator having to reproduce the failure.
- New `showFaiErrorSnack` / `showFaiErrorDialog` helpers wrap
`FaiErrorBox` in copyable surfaces; 27 ad-hoc
`SnackBar(content: Text(e.toString()))` sites swept to use
them (settings, doctor, audit, store, module sheet, system-AI
editor, flow output).
- New `FaiLogViewer` modal (`showFaiLogViewer`) renders log
files inline with line numbers, JSON-key + `[level]` token
colouring, Copy-all, Refresh, Open-externally. Doctor's
daemon-paths panel grows a "View" button next to "Open" for
every `.log` row and now also lists the Studio errors log.
- Today carousel: CTAs now actually re-run search after a
`filterCategory` / `runQuery` story is tapped (was only
flipping the chip state). Fallback story list bumped to 8.
- Editor bumped to git ref carrying 0.15.0 (type-token
colouring + analyzer diagnostics).
Studio bumped to 0.62.0.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Second half of the May-2026 trust pass. Drops the wall of
gRPC trailers from every error surface and makes the Store
honest about what is and isn't installable.
Friendly errors:
- New `friendlyError(Object, AppLocalizations)` mapper turns
GrpcError + arbitrary throwables into a localised headline,
optional recovery hint, and a verbatim detail string kept
behind a "Show details" expander. Duck-typed on `.code` /
`.message` so Studio doesn't have to depend on package:grpc
directly.
- `FaiErrorBox` gains an `error:` constructor that runs the
mapper. Every call site that used to render
`snap.error.toString()` (flows, welcome, store) switches to
it.
- 9 .arb entries per locale cover the gRPC codes we actually
emit (INVALID_ARGUMENT, NOT_FOUND, ALREADY_EXISTS,
PERMISSION_DENIED, FAILED_PRECONDITION, INTERNAL,
UNAVAILABLE, UNAUTHENTICATED) plus copy/details affordances.
- `test/friendly_error_test.dart` — 6 unit tests for the
mapper. Covers the mapping table, locale-switching, and the
non-gRPC fallback so future regressions show up in CI.
Capability discovery:
- New `HubService.allCapabilities()` reads the kind-aware
capability list (wasm + builtin + federated) and returns a
Dart-side `CapabilityInfo` value type. The flow page's
missing-dependency check uses it so `system.approval` and
federated MCP/n8n tools count as "available" — fixes the
Run button staying disabled forever.
- `HubService.listModules()` filters to kind=wasm so the
Modules page doesn't sprout synthetic "system" entries that
the operator can't uninstall.
Store clarity:
- New "Installable only" filter, on by default. Roughly 2/3
of seed entries currently carry `status: planned`; the
default view stops being noise.
- Featured-strip cards for planned modules now show a
"Coming soon" pill instead of an empty action area.
- Main-grid cards for non-installable modules dim to 60%
opacity so the eye lands on actionable cards first.
- Detail-sheet "Nicht installierbar" tooltip → inline hint
box. The reason is visible without hovering.
MCP localisation:
- `_kMcpSuggestions` no longer holds 11 hardcoded English
description strings. The `description` field is replaced
with a `resolveDescription(AppLocalizations)` lookup that
switches on the suggestion `name` to read the matching
`mcpSuggestion*Desc` .arb key. EN + DE shipped.
- New `FaiEnBadge` widget renders a small `[EN]` pill when
the active locale isn't English. Used next to MCP /
federated store entries' tagline + description because
the server supplies them in English and we can't translate
on the fly yet — the badge is the honest signal until the
planned `studio.translate` plugin lands.
Plus housekeeping: removed the unused `_keepImport` lint
escape in the test and the dangling library doc-comment in
`format.dart`.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Documentation system (the original "whole docs system is broken"
complaint):
- Studio reads inline docs from disk via the new
getInstalledModuleDocs RPC. No network, no auth, no provider
lock-in.
- store.dart probes for MODULE.md eagerly when a module-detail
sheet opens (a single file stat). The Documentation section
only renders when the bundle actually shipped docs.
- Shared FaiTheme.markdownStyle helper used by Welcome's
DocReaderSheet and Store's DocsPanel so every inline doc
reads in the same typography. The shared style forces
code.backgroundColor = transparent to suppress the per-span
bands flutter_markdown's default code style draws on dark
themes.
Flow run dialog:
- Run-button gating now strips the @version suffix from
installed capabilities before the contains() check. Without
this fix the button stayed disabled for every flow with
dependencies, even after a successful install.
- Studio derives a MIME type from the picked file's extension
(small per-suffix map; .pdf, .docx, .txt, .json, ...) and
forwards it to the hub. Fixes "unsupported MIME type:
application/octet-stream" from text.extract.
- Dialog title tracks the future's state: running -> "extract
laeuft", success -> "extract -- Ergebnis", failure ->
"extract -- fehlgeschlagen".
- Output rendering moved from String stringification to a
sealed FlowOutput type (Text / Json / Bytes / File / Unknown).
A new FaiFlowOutput widget dispatches per variant: markdown
for text/markdown (heuristic), pretty JSON for proto Struct,
inline image preview + Save-As for bytes, Open for file URIs.
Byte sizes:
- New humanBytes() helper renders 23.3 kB / 1.04 MB style values
with three significant digits, matching Finder / GNOME Files.
Wired into flow-card pills, picked-file readouts, and the
bytes-payload preview line.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Sweeping pass against six user reports collected this session.
1. "Capabilities ist nicht deutsch, Chain auch nicht."
The DE locale still leaked English vocabulary. Replaced
"Capabilities" → "Fähigkeiten" and "Chain" / "Hash-Chain"
→ "Kette" / "Hash-Kette" everywhere — store search hint,
recommended-source body, federated toast, doctor summary
chain row, modules panel summary, MCP / n8n hints, the
approvals history blurb. Wire-level identifiers
(`chain.reset`) stay as code.
2. "Bei Fehlern unten muss man die auch ins clipboard
kopieren können." New `FaiErrorBox` widget: selectable
monospace block with a small copy-to-clipboard icon
button that flips to a checkmark for two seconds after
click. Applied to the Doctor update banner output and
the Settings channel toast — the two places long
stderr / stdout lands.
3. "Öffnen bei Log kann es nicht öffnen. Audit-DB auch
nicht. PID auch nicht."
Cause: `SystemActions.openInOs` shells out to `open` /
`xdg-open` on file paths the OS has no default handler
for (SQLite DB, PID file, log without an .ext that
binds). New `revealInOs` uses `open -R` on macOS,
`explorer /select,` on Windows, and the parent
directory via `xdg-open` on Linux. Doctor's path rows
carry an `isDirectory` flag that routes through the new
`openOrReveal` so files reveal in Finder / Explorer
instead of failing silently.
4. "Oben im Store könnte man diesen Redaktionshinweis auch
so bauen, dass man mit pfeil nach rechts links auch
weitere anzeigen kann."
The Today-Hero became a carousel. Curated fallback
list grew from one entry to four (public sources, the
sandbox-by-default permission story, the hash-chained
audit story, the air-gap-ready single-binary pitch).
Hero gets prev / next chevrons plus a dot indicator
when the current snapshot has more than one slide.
Operator-accepted stories stay single — the carousel
collapses when there's only one to show.
5. "Ich fände es schöner wenn rechts und links im Store
die Abstände konsistent sind, das Reload-Symbol rechts
ist zu weit rechts und Store links auch nicht bündig."
AppBar now has `titleSpacing: FaiSpace.xl` so the
title's left edge sits flush with the body's left
padding (24 dp), and the trailing `SizedBox` after the
reload icon shrunk so the icon's outer edge meets the
right edge of the rightmost grid card.
6. "Oben der Titel zeigt fai_studio an, das sollte F∆I
Studio sein." The OS window title was the
pubspec-derived "fai_studio". Macos/Linux/Windows
runners now hard-code "F∆I Studio" (with the U+2206
triangle escape so the C++ source stays ASCII). macOS
bundle name and display name lifted out of the
PRODUCT_NAME variable for the same reason.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Replaces the read-only System AI block in Settings with a real
editor. Operator never has to touch ~/.fai/config.yaml again
to change provider, endpoint, model, API-key env-var, or
privacy mode.
UX choices that align with the zero-learning-curve rule:
* Provider dropdown lists Ollama / OpenAI / LM Studio / vLLM
/ Custom by friendly name. Each selection auto-fills the
endpoint and api_key_env defaults — but only when the
field is still empty or matches a different preset's
default, so manual overrides are never clobbered.
* Each provider has a one-line description rendered under
the dropdown ("Ollama → Local `ollama serve`. Models stay
on your machine. No API key needed.") and a model-hint
placeholder ("gemma3:4b · llama3.2:3b · qwen2.5-coder:7b")
that goes away when a model is typed.
* Privacy mode is a vertical radio group with each option's
description in place — no doc-lookup needed.
* "Test connection" sends an `ok`-ping and renders the
same error-kind → fix-hint mapping the audit drill-down
uses.
* "Save" persists + hot-reloads the hub. No daemon restart
required. UI status badge flips to "enabled · <privacy>"
immediately.
`fai_dart_sdk` 0.5.0 carries the underlying `updateSystemAi` /
`testSystemAi` RPCs.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Tapping a module card on the Modules page slides up a detail
sheet showing the full manifest data: directory path
(monospaced + selectable), every capability with version, and
every declared permission with a semantic icon (net = globe,
fs.read = folder, fs.write = edit, env = terminal, hub =
shield).
Empty permission list shows "(none — pure-computation module)"
explicitly — the operator should see *why* a module needed
zero perms, not be left guessing.
Backed by HubAdmin.ModuleInfo. New widget FaiModuleSheet with a
small drag-handle, FutureBuilder for the load state, error
surfacing if the RPC fails.
Bumps fai_studio 0.5.0 -> 0.6.0.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Studio is no longer hardcoded to localhost:50051. A gear icon
in the sidebar footer opens a small settings dialog where the
operator picks host / port / TLS-on-or-off. Saved values
persist via shared_preferences and Studio reconnects to the
new endpoint immediately.
- data/hub.dart: HubService.loadPersistedEndpoint() called once
before the first frame; reconnect() persists on every change.
- widgets/fai_settings_dialog.dart: typed form with port range
validation, live URL preview pill, error surfacing if the
reconnect fails.
- main.dart: gear icon at the sidebar bottom, replaces the
static "platform v0.10.42" footer.
shared_preferences added as dependency. flutter analyze clean,
flutter test 2/2, macOS debug build succeeds.
Bumps fai_studio 0.4.0 -> 0.5.0.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Replaces the Material-default look with a deliberate visual
language. Single accent (sky-cyan), Inter + JetBrains Mono via
google_fonts, dense rows over puffy cards, micro-interactions
under 250ms. Dark-first, light reaches parity.
New foundation under lib/theme/:
- tokens.dart FaiColors / FaiSpace / FaiRadius / FaiMotion
- theme.dart ColorScheme + typography + component themes for
light and dark, plus FaiTheme.mono helper for
technical strings (IDs, paths, capability refs).
Six primitives under lib/widgets/:
- FaiCard flat card, optional accent stripe (top or
left). No shadows.
- FaiPill small inline label with five tones
(neutral / accent / success / warning /
danger), optional mono and leading icon.
- FaiStatusDot breathing dot, used as a "live" indicator.
- FaiDataRow Linear-style dense row for the audit
stream — hover-elevation, mono leading,
coloured leading stripe.
- FaiEmptyState gracious icon + title + hint + action,
replaces "(no data)" everywhere.
- FaiDeltaMark the ∆ signature element. Three modes —
idle (still), live (gentle pulse), busy
(slow rotation). Drawn from primitives,
not a font glyph. Lives in the sidebar
header so the brand is always visible.
Page-level changes:
- main.dart custom 220px sidebar replaces the Material
NavigationRail. ∆ on top, hub-connection
pill below it, hover-animated destinations,
page transitions are 200ms slide+fade.
- modules.dart FaiCard rows with capability pills.
- audit.dart FaiDataRow stream, segmented filter chips,
live status bar with hash-chain badge.
- approvals.dart FaiCard with accent-top stripe, structured
action footer, toast on approve/reject,
themed reject-reason dialog.
google_fonts added as dep. flutter analyze clean. flutter test
2/2. flutter build macos --debug succeeds.
Bumps fai_studio 0.2.0 -> 0.3.0.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>