Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
df082bf
Speed up live Meeting segment merges and prefetch Meeting details on …
claude Oct 5, 2026
6e263e0
Keep Meeting playback work bounded and skip unchanged transcript renders
MyButtermilk Oct 5, 2026
e6e65c8
Reduce shared UI and history read work across Scriber
MyButtermilk Oct 5, 2026
f96764e
Bound long-file merge work and summary scroll layout reads
MyButtermilk Oct 6, 2026
3c11db7
Skip unchanged search indexing and upload-driven history work
MyButtermilk Oct 6, 2026
54dfbba
Show Settings before device discovery and reduce log-console render work
MyButtermilk Oct 6, 2026
52a3b22
Align frontend source gates with independent Settings resources
MyButtermilk Oct 6, 2026
94e7107
Overlap app startup and remove redundant tray work
MyButtermilk Oct 6, 2026
2beef24
Protect Settings preload and bound speculative Meeting work
MyButtermilk Oct 6, 2026
39bdf44
Make recovery tests tolerate completion during startup scans
MyButtermilk Oct 6, 2026
79b812a
Reuse Meeting display segments and isolate transcript row renders
MyButtermilk Oct 6, 2026
60c7f95
Isolate frequent text and live-stream updates in the frontend
MyButtermilk Oct 6, 2026
5789414
Align frontend source gates with isolated recording and draft state
MyButtermilk Oct 6, 2026
d665029
Keep slider drafts local and skip unchanged virtual histories
MyButtermilk Oct 6, 2026
5443a4c
Keep every tray action visible without scrolling
MyButtermilk Oct 6, 2026
807418a
Preserve live Meeting updates during prefetch and prepare 0.5.134
MyButtermilk Oct 6, 2026
72f16fe
Create the tray WebView outside synchronous Windows callbacks
MyButtermilk Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 115 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,13 @@ Frontend and shell:
- `Frontend/client/src/main.tsx` starts the initial locale catalog and only the
selected main/tray/overlay module together, then waits for both before React
renders. Keep the overlay's transparent document markers ahead of its import.
Main and tray also prefetch their bounded initial native backend access during
module loading. Reuse that startup promise at mount without blocking the
localized loader; normal health/restart checks must still refresh access.
Idle Settings preloading publishes the settings resource independently of
microphone discovery. Before writing the query cache, verify the bootstrap
generation and unchanged query state so a late preload cannot replace a save
or an independent query response, even within the same millisecond.
- `Frontend/client/src/hooks/use-backend-status.tsx` subscribes to the native
`backend-status-changed` invalidation before its initial authoritative query.
Coalesce concurrent checks and retain one follow-up for events received during
Expand Down Expand Up @@ -762,7 +769,15 @@ Packaging and scripts:
single-instance show actions must reveal that same WebView again; do not leave
a headless tray process whose main window no longer exists. Explicit tray
Quit and app exit still use the bounded graceful backend/audio cleanup path.
- The custom tray WebView refreshes recent transcripts on every native show
- The custom tray WebView keeps all action targets visible without a scroll
container. Create it from an async command or background event task because
synchronous WebView2 creation can deadlock Windows IPC. Serialize competing
show requests. Rows share
the available height; supplementary descriptions collapse before actions.
The native window fits the monitor work area in logical pixels, including
high-DPI displays. Verify normal, update, error, and recent-history views
at 668, 560, and 480 logical pixels high when changing this layout. It refreshes
recent transcripts on every native show
through `scriber-tray-opened`, and after installing its listener on first
startup. Abort and generation guards prevent older requests from replacing
newer history. The Transcript routes' `/recent` and `/{id}/copy` reads use
Expand All @@ -772,6 +787,14 @@ Packaging and scripts:
Labels are bounded plain-text previews; empty text stays visibly disabled.
Copy validates the selected ID and final status again, runs off the UI
dispatcher, and uses an owned Scriber HWND for Windows clipboard ownership.
Entering Recent Transcripts may reuse an in-flight read from that opening;
a new native opening must still invalidate it. Refresh shortcut labels on
opening, never on recording/update status events. Subscribe before reading
initial tray status, bound listener setup, and reject late snapshots after
newer events. Native unchanged status skips icon/menu/event work; app-wide
status delivery already reaches the tray and must not be duplicated.
A failed or stalled opened-listener registration must still perform the
initial shortcut/history read. Dispose timers and late listeners on unmount.
- Rust registers both live-mic shortcuts and the Meeting shortcut after the
token-protected backend identity is ready. Fresh installs default to
`Ctrl+Shift+D` for Live Mic, `Ctrl+Shift+F` for post-processing, and
Expand Down Expand Up @@ -2147,7 +2170,81 @@ Already implemented and should not be regressed:
provider replay evidence.
- Canvas/RAF waveform drawing instead of per-frame React state.
- Buffered transcript appends for long live sessions.
- Timed file stitching retires only the sorted prefix ending strictly before
the next part. Preserve conservative overlap evidence, speakers and warnings;
compare changes with the frozen reference in the merge diagnostic benchmark.
Keep two alignment score rows and byte-sized traceback moves, without
truncating difficult overlaps. Tests bound timestamp reads and traced memory.
- Summary contents navigation reuses heading elements and reads current
geometry with binary search for ordinary block headings. Preserve the linear
fallback for tables/snapshot grids and refresh references on summary changes.
- Saved Meeting playback reuses `createReviewPlaybackLookup` per segment
snapshot; preserve canonical/alignment/start-time priority and stable ties.
Do not build the interval index for live previews without saved playback audio.
Keep transcript edit/undo callbacks and virtualizer item keys stable, and
avoid whole-transcript scans on playback ticks. `Meetings.test.tsx` gates zero
row renders within an unchanged active segment; the timeline tests gate at
most 64 timestamp reads per seek on 12,000 overlapping synthetic segments.
`npm run benchmark:meeting-review` reports informational CPU timings and
index construction cost; it is not an installed Windows latency claim.
- Settings renders persisted values independently of microphone discovery and
native autostart. Preserve shared bootstrap requests, cache expiry, generation
guards, and in-flight detachment on invalidation; late peripheral results must
not overwrite user edits or notify after unmount.
- Settings keeps custom vocabulary, the summary and live-cleanup prompts, and
both custom OpenRouter model codes in `lib/text-draft-store.ts` stores, not
page state. A keystroke renders only its field. Loads, resets, rollback and
canonicalization write the store; blur saves and "Use" read it. Vocabulary
autosave and localized-default prompt replacement subscribe to their store
with the former debounce/recheck semantics. Tests require zero page-wide
tooltip renders while typing.
- The Visualizer-bars Settings control owns drag state and its saved-value
reference. Commit through the existing Settings write queue. Failed earlier
writes must not overwrite a newer draft, and reverting to the saved value
behind an outstanding write must still persist that revert.
- `VirtualTranscriptHistory` is memoized; Live Mic/YouTube/File must keep both
row-render callbacks and next-page callbacks stable across unrelated inputs.
Include all row-visible action state in callback dependencies. Preserve
internal scroll/resize updates, replacement data, locale and layout changes.
- Live Mic interim text, elapsed time, status and warnings stay inside the
memoized `LiveMicStage`; history query, search and cards stay in the page.
The stage receives only the history refresh callback for a finished session.
- The Meetings page owns each note queue, pagehide flush and autosave delay
without subscribing to drafts; `MeetingNotesEditor` subscribes through
`useMeetingNotesAutosaveSnapshot`. Restart the delay only when draft text or
dirtiness changes, as the former render-driven effect did. The page-owned
chat question is a draft store read by `MeetingChatComposer`; Meeting
switches and successful answers still clear it, view switches keep it.
- Meeting transcript rows keep separate memo boundaries. Pass draft/saving
state only to the affected row, and keep playback callbacks independent of
unrelated detail changes. The page-local display projector consumes immutable
segment snapshots and a stable label resolver; change resolver identity when
speaker metadata or locale changes. Its positional fast path and weak-key
fallback must preserve text, timing, revision, speaker and locale updates.
Tests bound row formatting and new display objects; benchmark CPU timings
remain informational and include the initial projection cost.
- Debug Console keeps virtualizer key callbacks and level counts stable for
unchanged log snapshots. Memoized log messages receive stable copy callbacks
and row-local feedback; serialize raw structured JSON only when opened.
- Paginated transcript endpoints and virtualized history lists.
- Shared history virtualizers and the Live Mic/YouTube/File callers keep stable
item-key functions; scrolling must not reconstruct every loaded row's key.
Completed summary renderers skip unrelated UI updates but invalidate when
content or locale changes. Intl formatter caches are bounded to 32 instances
per kind, retain no rendered text, and reset date formatters on window focus.
- Idle settings/device, active-Meeting, and history preloads are independent;
keep history requests sequential/frame-yielded, forward query abort signals,
and suppress late settings publication after cancellation.
- Transcript history indexes include the ID tie-breaker and status for ordered,
covering reads. Migrate only their app-owned definitions once. Punctuation
search reuses FTS visible-summary text with a fallback for missing/mismatched
index rows; preserve literal substring semantics and hidden-HTML exclusion.
- Metadata-only transcript saves compare the real indexed projection under the
parent write transaction and skip unchanged FTS rewrites. Preserve missing/
stale-row repair, summary-format semantics and rollback on projection errors.
- Upload patches that change no field keep snapshot identity and do not notify
subscribers. Preserve the server-processing transition after rounded 100%.
File history partitions depend on history data, not upload or copy UI state.
- Meeting detail assembly validates existence once and reuses its SQLite
connection for related collections instead of repeating helper lookups.
- The native 10-ms Meeting Mic/System/AEC relay reuses decode, clean-output,
Expand All @@ -2159,6 +2256,23 @@ Already implemented and should not be regressed:
lease, applies duration-scaled provider budgets, and deduplicates cross-track
echo with a timeline sweep instead of all mic-by-system pairs.
- Coalesced `history_updated` events.
- Live Meeting `meeting_segment` merges are linear: one scan plus binary-search
insertion, with the stable full sort only for unsorted input or comparator
ties. `meeting-segment-merge.test.ts` proves equivalence with
replace/append-then-sort and holds a deterministic `startMs`-read ceiling
for a 3,000-event stream; lower that ceiling when work drops, never raise it
to admit a regression. Measured 2026-10-05 on 6,000 segments: about 2.3x less
merge time than the former per-event copy-and-sort.
- Meeting library rows prefetch detail after 150 ms of sustained pointer or
keyboard-focus intent, with the query's own 30-second stale time. Keep one
speculative request active, cancel unused owned requests on leave, and retain
requests adopted by navigation or observed elsewhere. Never cancel an existing
consumer's request or prefetch the selected Meeting or one pending discard.
A speculative response must retain cache updates received while it was in
flight, including live segments and edits, instead of restoring its stale snapshot.
Review matching uses one pass; build the membership set only for active
filters and reuse visible rows for timeline matches. Unfiltered previous/next
navigation still needs the ordered IDs.
- Chunked/offloaded upload writes and export/cleanup work where practical.
- JobStore and latency metrics store connection reuse.
- App-owned aiohttp provider connection reuse with DNS caching and bounded
Expand Down
33 changes: 33 additions & 0 deletions Frontend/benchmarks/formatters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import { dateFormatter, numberFormatter } from "../client/src/i18n/formatters";

const count = 5_000;
const date = new Date("2026-10-05T12:34:56Z");
const options = { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" } as const;
function run(cached: boolean): number {
let checksum = 0;
for (let i = 0; i < count; i++) {
const locale = i % 2 ? "en-US" : "de-DE";
checksum += (cached ? dateFormatter(locale, { ...options }) : new Intl.DateTimeFormat(locale, options)).format(
date,
).length;
checksum += (
cached
? numberFormatter(locale, { maximumFractionDigits: 2 })
: new Intl.NumberFormat(locale, { maximumFractionDigits: 2 })
).format(i / 3).length;
}
return checksum;
}
assert.equal(run(false), run(true));
function median(cached: boolean) {
return Array.from({ length: 5 }, () => {
const start = performance.now();
run(cached);
return performance.now() - start;
}).sort((a, b) => a - b)[2];
}
console.log(
JSON.stringify({ node: process.version, rows: count, beforeMs: median(false), afterMs: median(true) }, null, 2),
);
94 changes: 94 additions & 0 deletions Frontend/benchmarks/meeting-display.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import type { MeetingSegment } from "../client/src/lib/api-types";
import { createMeetingSegmentProjector } from "../client/src/lib/meeting-segment-display";

const initial: MeetingSegment[] = Array.from({ length: 12_000 }, (_, i) => ({
id: `s${i}`,
meetingId: "synthetic",
revision: "live",
source: "system",
speakerId: `speaker-${i % 6}`,
speakerLabel: `Speaker ${i % 6}`,
startMs: i * 600,
endMs: i * 600 + 900,
durationMs: 900,
text: "Synthetic benchmark passage",
confidence: null,
alignmentQuality: "provider_segment",
isFinal: true,
sequence: i,
createdAt: "2026-10-06",
editVersion: 0,
editedAt: null,
}));
const snapshots: MeetingSegment[][] = [];
let current = initial;
for (let i = 0; i < 100; i++) {
const index = (i * 7919) % current.length;
current = current.map((segment, position) =>
position === index ? { ...segment, text: `Synthetic update ${i}` } : segment,
);
snapshots.push(current);
}
const labelFor = (segment: MeetingSegment) => segment.speakerLabel;
const reference = (segments: MeetingSegment[]) => segments.map((segment) => ({ ...segment, label: labelFor(segment) }));
const check = createMeetingSegmentProjector();
let prior = check(initial, labelFor);
let newDisplays = 0;
for (const snapshot of snapshots) {
const result = check(snapshot, labelFor);
assert.deepEqual(result, reference(snapshot));
newDisplays += result.filter((value, index) => value !== prior[index]).length;
prior = result;
}
assert.equal(newDisplays, snapshots.length);
let checksum = 0;
function measure(setup: () => () => void) {
const values: number[] = [];
for (let i = 0; i < 12; i++) {
const run = setup();
const start = performance.now();
run();
if (i >= 3) values.push(performance.now() - start);
}
return Number(values.sort((a, b) => a - b)[4].toFixed(3));
}
const referenceMs = measure(() => () => {
for (const snapshot of snapshots) checksum += reference(snapshot).length;
});
const reusedMs = measure(() => {
const project = createMeetingSegmentProjector();
project(initial, labelFor);
return () => {
for (const snapshot of snapshots) checksum += project(snapshot, labelFor).length;
};
});
const firstReferenceMs = measure(() => () => {
checksum += reference(initial).length;
});
const firstProjectionMs = measure(() => {
const project = createMeetingSegmentProjector();
return () => {
checksum += project(initial, labelFor).length;
};
});
console.log(
JSON.stringify(
{
node: process.version,
segments: initial.length,
updates: snapshots.length,
referenceNewDisplays: initial.length * snapshots.length,
reusedNewDisplays: newDisplays,
referenceMs,
reusedMs,
firstProjectionMs,
firstReferenceMs,
medianOf: 9,
checksum,
},
null,
2,
),
);
61 changes: 61 additions & 0 deletions Frontend/benchmarks/meeting-review.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import { performance } from "node:perf_hooks";
import {
activeReviewSegmentId,
createReviewPlaybackLookup,
type ReviewTimelineSegment,
} from "../client/src/lib/meeting-review-timeline";

// Synthetic two-hour timeline, with overlapping microphone/system turns.
// No transcript content, network, credentials, or installed app required.
const segments: ReviewTimelineSegment[] = Array.from({ length: 12_000 }, (_, i) => ({
id: `segment-${i}`,
revision: "canonical",
speakerId: null,
label: "Speaker",
startMs: i * 600,
endMs: i * 600 + 900,
alignmentQuality: i % 3 ? "provider_segment" : "exact_word",
text: "Synthetic benchmark text",
}));
const samples = Array.from({ length: 2_000 }, (_, i) => (i * 7919) % 7_200_000);
const lookup = createReviewPlaybackLookup(segments);
for (const atMs of samples) assert.equal(lookup(atMs), activeReviewSegmentId(segments, atMs));

function measure(run: () => void) {
for (let i = 0; i < 3; i++) run();
const timings = Array.from({ length: 9 }, () => {
const started = performance.now();
run();
return performance.now() - started;
}).sort((a, b) => a - b);
return Number(timings[4].toFixed(3));
}

let checksum = 0;
const linearMs = measure(() => {
for (const atMs of samples) checksum += activeReviewSegmentId(segments, atMs)?.length ?? 0;
});
const indexedMs = measure(() => {
for (const atMs of samples) checksum += lookup(atMs)?.length ?? 0;
});
const buildMs = measure(() => {
checksum += createReviewPlaybackLookup(segments)(600)?.length ?? 0;
});
console.log(
JSON.stringify(
{
node: process.version,
segments: segments.length,
lookups: samples.length,
medianOf: 9,
linearMs,
indexedMs,
buildMs,
speedup: Number((linearMs / indexedMs).toFixed(2)),
checksum,
},
null,
2,
),
);
18 changes: 6 additions & 12 deletions Frontend/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { recordingErrorToastMessageFromPayload, showRecordingErrorToast } from "
import { showPostProcessingFallbackToast, type CredentialSettingsRequest } from "@/lib/post-processing-fallback-toast";
import { useToast } from "@/hooks/use-toast";
import { lazy, Suspense, useCallback, useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from "react";
import { isTauriRuntime, loadBackendBaseUrlFromTauri, setTrayRecordingState } from "@/lib/backend";
import { withPromiseTimeout } from "@/lib/fetch-with-timeout";
import { isTauriRuntime, setTrayRecordingState } from "@/lib/backend";
import { isInitialBackendAccessReady, loadInitialBackendAccess } from "@/lib/initial-backend-access";
import { preloadPrimaryTabData } from "@/lib/tab-data-preload";
import { flushFrontendPerformanceReport, setFrontendPerformanceReportingEnabled } from "@/lib/frontend-performance";
import { ToastAction } from "@/components/ui/toast";
Expand Down Expand Up @@ -610,19 +610,13 @@ function FrontendPerformanceFlushBridge() {
}

function App() {
const [backendBaseReady, setBackendBaseReady] = useState(!isTauriRuntime());
const [backendBaseReady, setBackendBaseReady] = useState(isInitialBackendAccessReady);

useEffect(() => {
let cancelled = false;
void withPromiseTimeout(loadBackendBaseUrlFromTauri(), 5_000, "Initial Tauri backend lookup")
.catch((error) => {
console.debug("Initial Tauri backend lookup failed; continuing with health fallback.", error);
})
.finally(() => {
if (!cancelled) {
setBackendBaseReady(true);
}
});
void loadInitialBackendAccess().then(() => {
if (!cancelled) setBackendBaseReady(true);
});
return () => {
cancelled = true;
};
Expand Down
Loading
Loading