Merge remote-tracking branch 'origin/main' into fix/small-batch-1
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -10,6 +10,7 @@ the frozen-backend fallback mirror it for their toolchains.
|
||||
|
||||
**Highlights**
|
||||
- A generation timeout now points at the compute-time budget in Settings rather than an environment variable (#1808)
|
||||
- The Accessibility prompt no longer floats over first-run setup and every other app until you grant it (#1845, #1886)
|
||||
- The last onboarding step offers to install a speech-to-text model instead of failing three times when none is installed (#1856)
|
||||
- A download that fails because the folder sits behind a mount point Windows will not cross now says so, and where to move it (#1957)
|
||||
- A GPU that is merely short on free memory is no longer told to reinstall its drivers (#1812) — thanks @michaelhuamanflores!
|
||||
|
||||
@@ -122,6 +122,20 @@ const IDLE_VISIBLE_POLL_MS = 600;
|
||||
// Reconcile only while the Accessibility blocker is visible.
|
||||
const A11Y_SETUP_RECHECK_MS = 1000;
|
||||
|
||||
// How long the Accessibility prompt may hold the always-on-top pill on screen.
|
||||
//
|
||||
// The pill is created always-on-top, and the setup state had no time limit at
|
||||
// all: it sat above every application, including the first-run setup window it
|
||||
// was covering, until Accessibility was granted or the user dismissed it by
|
||||
// hand (#1845, #1886). A permission the user has not granted yet is not urgent
|
||||
// enough to outrank whatever they are actually doing, and on a clean install
|
||||
// they are usually mid-setup and cannot grant it yet anyway.
|
||||
//
|
||||
// Polling does NOT stop when the window hides. The check keeps running, so
|
||||
// granting Accessibility later still returns the widget to idle on its own —
|
||||
// what expires is the pill's claim on the screen, not the reconciliation.
|
||||
const A11Y_SETUP_VISIBLE_MS = 20_000;
|
||||
|
||||
// A dictation model id is a sherpa-onnx live model when it carries the
|
||||
// `sherpa-` prefix the backend assigns (see services/sherpa_dictation.py). Only
|
||||
// then do we open the low-latency raw-PCM streaming path. Other models use a
|
||||
@@ -652,6 +666,10 @@ export default function CaptureWidget({ onDismiss }) {
|
||||
let cancelled = false;
|
||||
let timerId;
|
||||
|
||||
// Wall-clock start of this setup episode, so the budget covers the whole
|
||||
// time the prompt has been up rather than one poll interval.
|
||||
const shownAt = Date.now();
|
||||
let hiddenForBudget = false;
|
||||
const reconcileAccessibility = async () => {
|
||||
const ok = await checkAccessibility();
|
||||
if (cancelled || stateRef.current !== 'setup') return;
|
||||
@@ -661,6 +679,12 @@ export default function CaptureWidget({ onDismiss }) {
|
||||
await hideWidgetWindow();
|
||||
return;
|
||||
}
|
||||
if (!hiddenForBudget && Date.now() - shownAt >= A11Y_SETUP_VISIBLE_MS) {
|
||||
// Stop covering the screen, but stay in `setup` so the label is right
|
||||
// if something shows the window again, and keep polling below.
|
||||
hiddenForBudget = true;
|
||||
await hideWidgetWindow();
|
||||
}
|
||||
timerId = setTimeout(() => {
|
||||
void reconcileAccessibility();
|
||||
}, A11Y_SETUP_RECHECK_MS);
|
||||
|
||||
@@ -295,3 +295,67 @@ describe('the widget window never strands empty', () => {
|
||||
expect(mocks.holder.hide).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the Accessibility prompt does not own the screen forever', () => {
|
||||
// #1845 / #1886: the pill is created always-on-top and the setup state had
|
||||
// no time limit, so on a clean macOS install it sat over the first-run setup
|
||||
// window — covering the disk-space line and the Start installation button —
|
||||
// and over every other application, until Accessibility was granted or the
|
||||
// user dismissed it by hand. A permission not yet granted does not outrank
|
||||
// what the user is actually doing, and mid-setup they usually cannot grant
|
||||
// it yet anyway.
|
||||
it('hides the pill once its on-screen budget is spent, and keeps polling', async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
mocks.holder.a11y = false;
|
||||
renderWidget();
|
||||
|
||||
// Settle the mount, where the idle-visibility reconcile legitimately
|
||||
// hides a window that was already open. Measure from there.
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
});
|
||||
const baseline = mocks.holder.hide.mock.calls.length;
|
||||
|
||||
// Still up a few seconds in — the prompt has something to say.
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
});
|
||||
expect(mocks.holder.hide.mock.calls.length).toBe(baseline);
|
||||
|
||||
// Past the budget it stops covering the screen.
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(20_000);
|
||||
});
|
||||
await waitFor(() => expect(mocks.holder.hide.mock.calls.length).toBeGreaterThan(baseline));
|
||||
const hidesAfterBudget = mocks.holder.hide.mock.calls.length;
|
||||
|
||||
// Hiding is not giving up: granting Accessibility later must still settle
|
||||
// the widget back to idle on its own.
|
||||
mocks.holder.a11y = true;
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mocks.holder.hide.mock.calls.length).toBeGreaterThan(hidesAfterBudget),
|
||||
);
|
||||
});
|
||||
|
||||
it('hides once, not on every poll', async () => {
|
||||
// The budget check is latched; re-issuing hide() every second would fight
|
||||
// anything that legitimately shows the window again.
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
mocks.holder.a11y = false;
|
||||
renderWidget();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(25_000);
|
||||
});
|
||||
await waitFor(() => expect(mocks.holder.hide).toHaveBeenCalled());
|
||||
const afterFirst = mocks.holder.hide.mock.calls.length;
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
});
|
||||
expect(mocks.holder.hide.mock.calls.length).toBe(afterFirst);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user