Repository navigation
Faster traffic table and startup, hand-drawn charts, Rust shell clean-up, menu bar menu with Open first and a Today block - #293
Merged
Conversation
…t entry is used Deletes button-group, progress, the mobile sidebar branch (the window is never narrower than 820px), nav.tsx's revealSection, seven unused exports, the Segmented re-export and the unused date-fns dependency. i18n.test.ts now uses the type checker to fail on any messages() entry nothing reads; it found four more dead entries besides the eleven from the audit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@modelcontextprotocol/sdk 1.32.1 and proxy-addr 2.0.8 (through shadcn's MCP server), sharp 0.35.5 (screenshot pipeline) and source-map-js 1.2.2 (through vite, postcss and tailwind). Lockfile-only for the transitive ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… key read Each control call opened a fresh connection, did a Noise handshake and re-read config.yaml for the key. The dashboard made nine of them one after another (about 27 round trips on a remote core). ControlClient::pin now resolves the target and key once; the dashboard, key_usage and upstream_stats send their queries concurrently through it. A pinned key that went stale after a rotation still gets in: a wrong-key handshake re-reads the config as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
list_clients fetched /v1/models six times one after another, building a new HTTP client (and connection) each time, although the clients without a key of their own all ask with the same default key. It now groups the clients by key, shares one client and asks each key once, concurrently. The model list is still fetched for clients that are not installed: the manual-setup dialog of an absent client shows it. Reading the clients' config files (ops::list, and the WSL listing that goes through \\wsl.localhost) now runs on a blocking thread instead of an async worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd retry Both dialogs fetched by hand and only toasted on failure, so the reading line spun forever. They now use useResource + Loadable: LoadingState for the editor, a table skeleton for the history, ErrorState with Retry, and an outlined empty state. Persistent notices in the editor are Banners (ui/alert.tsx is gone), buttons use pending, the editor box uses the popover token, and the last direct sonner imports go through notify. The plugin code fallback uses LoadingState too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every local core start ran $SHELL -l -i first (up to 5 s) and waited
for it. The read now starts ahead of time: at the top of run() when the
startup connection is local (overlapping Tauri and plugin setup), when a
restart is requested (overlapping the old core's shutdown), and during a
crash backoff. load() takes that result if it was started within the
last 30 s and reads afresh otherwise, so each start still gets the
values of that moment, as promised for ${VAR} resolution.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the local core was down the event bridge retried every second forever, each time re-reading config.yaml and dialling the socket, and the heartbeat woke every five seconds even when core was not running or the app was on a remote connection. Both now wait on the supervisor's state: the bridge subscribes once core answers its control plane (running, or the safe-mode core that is up), the heartbeat starts counting once core is running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
atomic_file::write flushes to disk (about 6 ms per write on this Mac) and was called from synchronous Tauri commands, which run on the main thread: the notice list commands, the settings setters and the update check toggle. They are now async and write on a blocking thread, as is the mark-as-read that follows a click on a notification or on a notice in the menu bar. prefs::update now serialises its read-modify-write so that two settings changed at the same moment both stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s pending AlertDialogAction now has a pending prop like Button, and keys/parts' ConfirmAction becomes AlertDialogConfirm in ui/alert-dialog, replacing the eight hand-built "action + busy + spinner" copies. focusSelf/useDialogFocus, CopyButton/CopyIconButton, Tile, openable/OPENABLE_ROW/stop, DISCLOSURE and RowsSkeleton move from keys/parts into src/ui (the sparkline stays put). The two copyText helpers become one in ui/notify; traffic/cells re-exports it so the traffic files keep their imports. README documents the pieces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
menubar::install, show_picker and show_main_window returned their
errors with ? from setup, so a tray builder failure (possible on
Windows and Linux) or a window that could not be created ended at
.expect("Tauri 起不来") and took core supervision and the gateway down
with it. Setup now logs the error, raises a notice (so a system
notification still says it when no window exists) and keeps running,
the same way a missing core binary is handled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scan() rebuilt the built-in rule set (compiling every regex) on each MCP-page scan; compiling them took about 20 ms in a debug build against 0.1 ms for a scan of a small home. The rules are now built once and shared with the file watcher. scan_clients and the MCP page commands (targets, plan, apply) read and write the clients' config files on a blocking thread instead of inside the async command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both dialogs carried the same alive/settled refs, settle guard, login_finished subscription and polling fallback. waitForLogin (a plain function, tested with fake timers) and its useLoginWait hook now hold that logic once: event first, status fetched for the account, polling as fallback, settle once, nothing after stop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They fetched every upstream's model list directly, so the lists the upstreams page and pinned models share were neither reused nor refreshed. useResources (lib/resource) reads several entries of the same cache at once; upstreams/data gets modelsKey, useModelLists and enabledModels, and every reader and invalidation uses the one key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 1346-line RequestDrawer.tsx becomes one file per tab (Timeline, Routing, Usage, Payload, Replay), the shell (RequestDrawer.tsx: sheet, header, skeleton) and the shared rows, stats, state and cost text (parts.tsx). Its text table and test move along; Conversation takes BodyText from Payload, the drawer's autofocus uses focusSelf. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tw-adopt: one unique() for the gateway's model list (qwen, grok, pi, opencode and models_stale each deduplicated it themselves), one NO_KEY, Gateway::v1 used by qwen, grok, hermes and opencode instead of four format! copies, plan::lookup_str in place of the identical field() in qwen and grok, and qwen/grok models_in reading their specs back with pi's count_of/flag_of like opencode already does. One crate-level now_ms() and wide() replace the copies in plan, foreign, detect and wsl. App crate: one wide() in lib.rs for dialog, private_dir, single, theme and the portable test; scan::now_ms goes in favour of notices::now_ms. user_env keeps its own wide(), it has to stay self-contained, and the confirm dialog's wide() is different (it replaces NUL with a space). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Latency was 1182ms in the table, 1,182ms in the drawer, 1,182 ms on the upstreams page and 1.18s on Overview; dates 10-08 and 10/8; tokens 1.5k, 2k and 200K. format.ts now has one helper per quantity: ms (1182ms, used by the drawer, replay, upstream/proxy/connection tests, speed test and dry run), msShort only for Overview's latency ranking, when/whenMinute/monthDay (also for Overview's axis labels and the model-list timestamps that had their own clock()), compact for all token counts including context windows, and span for a session's length. traffic/format.ts and contextWindow are gone. README lists the helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test modules of supervisor/mod.rs, clients/ops.rs and menubar/model.rs move to sibling tests.rs files, as notices/ does (multi-line string literals are kept byte for byte). menubar/macos.rs loses the self-drawn rows (Info, InfoView, column widths, drawing and their tests) to macos/info.rs, and the helpers only the menubar_* examples use (offscreen preview, PNG writing, opening, closing and dumping the menu) to macos/preview.rs, compiled only with debug_assertions like memcheck. The examples fall back to an empty main in builds without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The real ~/.thinkwatch/backups on a dev Mac had collected thousands of folders from older tests. No current test writes there (the whole suite run under a throwaway HOME leaves nothing behind), and data_dir() in the app crate and tw-adopt's backup_root() now assert under cfg(test) that THINKWATCH_HOME points somewhere else, so a future test that forgets its temporary directory fails instead of writing into the user's data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useEditing held the two-way sync (debounces, sequence and form-generation guards, pristine revert, flush before save) in refs and callbacks with no tests. EditingEngine (plugins/editing.ts) is the same logic as a plain class with injected core calls; useEditing only wires it to React through useSyncExternalStore. editing.test.ts drives it with fake timers and a hand-released fake core through every guard. Behaviour unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as remove, raw button The AWS profile field label moves into the text table (the glossary keeps it in English in both languages), the spinner's screen-reader label follows the UI language, removing a header row or a price-sheet override is a written action like the other list editors (x only closes), and the "show all" toggle under unpriced models is a kit Button. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
recharts and its dependencies (d3, immer, Redux Toolkit, es-toolkit, decimal.js-light, reselect) cost 331 kB to draw one chart and dominated the live range's 500 ms tick. StackedArea now draws the same thing itself: monotone-X cubic curves (the algorithm d3's curveMonotoneX uses; paths match recharts' output to 0.001 px), stacked gradient areas, the half/top ticks with the same label placement, the dashed grid, the hover cursor, active dot and live-edge dot, and the tooltip with recharts' placement and edge-flip rules and its 120 ms glide. Screenshots of every range, theme, language, hover position, the live range and the empty state are pixel-identical to the recharts version. Intentional differences: the tooltip appears where it belongs instead of flying in from the chart's top-left corner, and its glide honours reduced motion; the chart is no longer a focusable element that did nothing with the keyboard; switching between tokens and cost morphs the shapes over 250 ms, as the code always described. recharts swept the chart in from the left again there, and kept the layers in their old stacking order, so the model on top could end up drawn in the middle until the page was reopened. The shadcn chart container goes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three hand-written sparklines had drifted apart: the keys one had no minimum peak (one request filled the bar), upstreams drew failures on top, the traffic header drew them at the bottom in grey. They now share src/ui/sparkline.tsx: each row scales to its own peak but never below 4, a bar with requests is at least 3px, failures sit at the bottom in red, bars use --chart-2 with --chart-1 for the current slot, and empty slots keep the faint baseline. Per-bar tooltips (traffic header) make each slot a 4px hover cell; without them bars are 2px with 1px gaps as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven places drew their own bar (the overview's Meter, latency spreads, speed bars, the session waterfall, retention usage, the quota bar and the update download), with three different track greys, two grey fill strengths and tiny values that could vanish. They now share src/ui/meter.tsx: one track (foreground at 8%), sizes sm/md/lg (4, 6, 8px), a data-viz colour or a state tone (neutral grey, strong, warn, error), an optional range start and a start/end mark for medians, a 2px minimum for any non-zero value, and meter/progressbar semantics only when a label is given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `core-state` string was taken apart in five places with slightly
different checks ("running:" vs "running", "safe_mode" with and without
the colon). parseCoreState() turns it into a tagged union once, with
isRunning() and controlUp() for the two questions callers actually ask;
anything unrecognised is "unknown" and treated as not running.
trouble(), launchPhase(), the upstream in-flight counter and the routing
flights now go through it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live range (the overview default) re-rendered every 500 ms forever, including while minimized or covered. The step timer now stops on visibilitychange to hidden; events are still recorded, samples outside the window are dropped as they arrive instead of piling up, and on return the curve is re-laid at the current time before the timer resumes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The date picker (react-day-picker + date-fns, ~70 kB) is now loaded when the custom-range button is hovered or focused, with a same-size placeholder in the popover. ConnectionProvider sits in the first chunk the main window loads, so its two dialogs and the error toast's translation table are imported when needed instead of up front. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live request rows move out of React state into a per-connection store read through useSyncExternalStore (useRequestRows, useRequestsView, useSessionViews, useSettled, useBusyKeys). Only the traffic page, the open command palette, the busy-key cells and the overview/security refetch tick subscribe, so a streaming request no longer re-renders the shell and the current page every frame. Pages that take shell data are memoized and get stable callbacks. While the window is hidden, events are applied to the store as they arrive (still capped at 2,000 rows) instead of queuing behind a requestAnimationFrame that never fires; the list is published and the settle/reconcile runs once when the window is shown again. Reconcile after requests land asks History only from the earliest start among the rows waiting to be matched (from_ms) instead of re-reading the newest 2,000 rows every 2.5 s, and falls back to a full read when the event stream cannot be trusted (dropped events, core stopped, an ending for a row we never saw). mergeHistory builds a patch of changed fields and copies only rows that changed. Session summaries reload on the same settle tick, replacing traffic/useSessions. App.tsx is now the small first chunk: connection layer, useCoreLink (core state, status/overview retry loop, launch handover) and the launch screen, which no longer pulls in tailwind-merge, the button kit or the core translation table. The rest of the shell is shell/Workspace.tsx (toolbar, shortcuts, overlays), with SourceList, Banners and Pages split out; every page, the command palette and the config/import dialogs are separate chunks loaded through usePart (no Suspense, so nested loads do not hit React's 300 ms reveal throttle) and preloaded once the window has handed over. describeCore/surfaceOf live in shell/describe.ts with tests. JS needed before the launch screen paints: ~2.1 MB -> 250 kB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…zations # Conflicts: # src/keys/parts.tsx
…zations # Conflicts: # src/connection/ConnectionProvider.tsx # src/upstreams/data.ts
… only The new-row entrance animated the row's background-color and the live status dot animated box-shadow: both repaint every frame, so under steady traffic the traffic table was repainting continuously. The flash is now an overlay (::after on each cell of a table row, on the element itself elsewhere) whose opacity holds until 45% and then fades; the pulse is a same-coloured ::after disc that scales to 5px outside the dot while fading from 0.55. Same keyframe stops, timing and colour; reduced motion still turns both off. In WebKit the computed ring radius/alpha and the flash alpha match the old values at every sampled time. Chromium (WebView2) never interpolated the old color-mix(currentcolor) box-shadow, so the pulse ring now shows there as it always did on macOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- One context menu and one "…" dropdown for the whole table (TableMenus) instead of a pair per row; items are built for the clicked row when the menu opens. Items, positions, keyboard (Enter/Space/ArrowDown on "…", Tab to the selected row's "…") and focus return are unchanged. RowContextMenuContent / RowDropdownMenuContent are split out of ui/row-menu so both paths render the same entries. - Tip gets a `lazy` mode that mounts the Radix tooltip on first hover; the table's cells use it. - Only the visible stretch of rows (plus overscan) is rendered, in both the flat and the grouped view; spacer rows keep the scroll height. Heights are measured after each render, WebKit's missing scroll anchoring is compensated, and keyboard reveal scrolls to rows that are not rendered yet. Column widths stay exactly as with every row rendered: the widest few cells of each column (estimated by text width) are rendered, invisible and zero-height, inside the header cells. - Rows of a just-expanded group fade in once, not again when scrolled into view. - The latency tooltip writes milliseconds without thousands separators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…zations # Conflicts: # src/traffic/SessionRow.tsx
…nts point at the split shell Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…row per quota window The menu now opens with "Open ThinkWatch Lite" in every state, instead of burying it under the stats. While the gateway runs, the old status header, the Today section and the quota section become one dashboard: "Today" with the gateway state (server name for a remote core, generation speed) on top, today's tokens as the hero number, then requests, failures and cost in a smaller line (the same tokens-over-cost order as the menu bar), and a sparkline of today's tokens per local hour on the right. Each reported quota window gets its own row with a full-width bar. Copy Gateway Address shows the address it copies. "Install Version" leads the app group when an update is pending; "Check for Updates" comes last otherwise. When the gateway is not running, a status block (dot, state, reason that wraps to two lines) replaces the header. Today's totals and the hourly series come from one control-plane call, /summary/buckets/by with one-hour buckets from local midnight, instead of /summary: same window and filter, so the sums are the summary's numbers and the hero number always equals the sum of the bars. No extra request, no polling; the current hour follows the clock while the menu is open. The tray on Windows and Linux follows the same order as text items; the Linux-only "move Open to the top" step is gone because the model already puts it first. The preview exporter renders busy, idle, alert, remote, stopped, unlinked, starting and safe mode in Chinese and English. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A performance and clean-up round across the app, plus a redesigned menu bar menu. No core change: still pinned to v0.64.1. Measurements are from a production build of the full preview harness in headless Chromium with about 2,000 history rows, unless noted.
Traffic table
background-color/box-shadowevery frame). Frame-by-frame identical in WebKit; on Windows the ring now draws at all (Chromium could not animate the oldcolor-mix(currentcolor)shadow).Shell, data flow, startup
from_ms) and copies only changed rows: ~2,000 → 12–20 rows per reconcile.Shell(975 lines) is split intosrc/shell/(useCoreLink,Workspace,SourceList,Banners,Pages); the core state string is parsed in one place (src/coreState.ts, tested).Charts
ui/charts.tsx): same monotone curves (path coordinates match to 0.001 px), ticks, gradients, tooltip and its edge flip. Pixel-identical in 14 of 15 before/after scenes; switching tokens ↔ cost now morphs instead of sweeping in again, which also fixes the stacking order staying stale after a switch. −337 kB of JS.ui/sparkline.tsxand oneui/meter.tsxreplace three drifted sparklines and seven hand-rolled bars.Clean-up and consistency
useResource: a failed read shows an error with Retry instead of spinning forever.src/i18n.test.tsnow fails on unused keys.date-fnsdropped; dev-only lockfile bumps close the open Dependabot alerts (MCP SDK, proxy-addr, sharp, source-map-js).AlertDialogConfirmwithpendingreplaces 8 hand-built copies; shared helpers moved fromkeys/parts.tsxtosrc/ui/; onecopyText;useLoginWaitfor the ChatGPT and Z.ai dialogs; model lists through one cache;RequestDrawersplit intosrc/traffic/drawer/; plugin code↔form sync extracted into a testedEditingEngine.1182ms, dates10-08, tokens1.5k(src/format.ts).Rust shell
list_clientsshares one HTTP client and one models request per key.run()and overlaps setup; the event bridge and heartbeat wait for core instead of polling while it is down; settings and notice writes leave the main thread.tests.rs;menubar/macos.rssplit intoinfo.rsand a debug-onlypreview.rs. Tests can no longer reach the real data directory.Menu bar menu (design approved by the owner)
requests · failures · costbeneath, and a sparkline of tokens per local hour (from/summary/buckets/by, replacing the/summarycall, so no extra request). One row per quota window actually reported, with its reset time. Then live requests, tools (group switch, copy address with the address shown, copy key), then update / settings / connection, then quit. Not-running states keep Open first, then the status and its actions. The tray follows the same order.Checks:
pnpm typecheck,pnpm test(929),pnpm build;cargo fmt --check,cargo clippy --all-targets -D warnings,cargo test(897); Windows/Linux cross-target clippy on the platform code touched; every page loaded in zh/en/empty/live with no console errors.🤖 Generated with Claude Code