From 46ef7c49900b9726527b4324dda0033f6f715ebd Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:28:38 -0700 Subject: [PATCH 01/17] Drain queued SQLite replies before faulting after a main-loop stall (#26444) * Give Windows Electron writer probes a larger test budget * Drain queued writer replies across two poll turns before timing out * Check writer shutdown after both reply-drain turns --- .../profile-state-writer-close.test.ts | 1 + .../profile-state-writer-deadline.test.ts | 37 +++++++++++++------ .../profile-state-writer-deadline.ts | 14 ++++--- .../profile-state-writer-electron-fixture.ts | 13 ++++++- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/main/persistence/profile-state/profile-state-writer-close.test.ts b/src/main/persistence/profile-state/profile-state-writer-close.test.ts index a082a1cb4ade..cf8b6a442585 100644 --- a/src/main/persistence/profile-state/profile-state-writer-close.test.ts +++ b/src/main/persistence/profile-state/profile-state-writer-close.test.ts @@ -68,6 +68,7 @@ it.each([true, false])( await waitForPoll() vi.advanceTimersByTime(TIMEOUT_MS) await waitForPoll() + await waitForPoll() expect(worker.terminate).toHaveBeenCalledOnce() expect(settled).toBe(false) worker.emit('exit', 1) diff --git a/src/main/persistence/profile-state/profile-state-writer-deadline.test.ts b/src/main/persistence/profile-state/profile-state-writer-deadline.test.ts index 3a60c019717c..95bd251d6cbb 100644 --- a/src/main/persistence/profile-state/profile-state-writer-deadline.test.ts +++ b/src/main/persistence/profile-state/profile-state-writer-deadline.test.ts @@ -13,6 +13,11 @@ import { const TIMEOUT_MS = 30_000 +async function drainTimeoutCheck(): Promise { + await waitForPoll() + await waitForPoll() +} + afterEach(() => { vi.useRealTimers() publishSystemResume() @@ -48,6 +53,16 @@ function harness() { } describe('profile state writer deadline', () => { + it('lets a queued reply clear the deadline after the first poll', async () => { + const { deadline, onTimeout, unsubscribe, run } = harness() + run(TIMEOUT_MS) + setImmediate(() => deadline.clear()) + await drainTimeoutCheck() + expect(onTimeout).not.toHaveBeenCalled() + expect(unsubscribe).toHaveBeenCalledOnce() + expect(vi.getTimerCount()).toBe(0) + }) + it('times out a request after allowing queued replies to drain', async () => { const { onTimeout, onGrace, unsubscribe, run } = harness() run(TIMEOUT_MS - 1) @@ -55,7 +70,7 @@ describe('profile state writer deadline', () => { run(1) expect(onGrace).not.toHaveBeenCalled() expect(onTimeout).not.toHaveBeenCalled() - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ overdueMs: 0, graces: 0, powerState: 'awake' }) ) @@ -74,7 +89,7 @@ describe('profile state writer deadline', () => { run(TIMEOUT_MS - 1) expect(onTimeout).not.toHaveBeenCalled() run(1) - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ overdueMs: 0, graces: 1 }) ) @@ -85,7 +100,7 @@ describe('profile state writer deadline', () => { stall(PROFILE_STATE_WRITER_OVERDUE_GRACE_MS - 1) run(TIMEOUT_MS) expect(onGrace).not.toHaveBeenCalled() - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).toHaveBeenCalledOnce() }) @@ -99,7 +114,7 @@ describe('profile state writer deadline', () => { stall(60 * 60_000) run(TIMEOUT_MS) expect(onGrace).toHaveBeenCalledTimes(PROFILE_STATE_WRITER_MAX_GRACES) - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ graces: PROFILE_STATE_WRITER_MAX_GRACES }) ) @@ -114,7 +129,7 @@ describe('profile state writer deadline', () => { run(TIMEOUT_MS - 1) expect(onTimeout).not.toHaveBeenCalled() run(1) - await waitForPoll() + await drainTimeoutCheck() expect(onGrace).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ graces: 1 })) expect(onTimeout).toHaveBeenCalledOnce() }) @@ -129,7 +144,7 @@ describe('profile state writer deadline', () => { expect(onGrace).toHaveBeenCalledTimes(PROFILE_STATE_WRITER_MAX_GRACES) expect(onTimeout).not.toHaveBeenCalled() run(TIMEOUT_MS) - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ powerState: 'suspended' }) ) @@ -148,7 +163,7 @@ describe('profile state writer deadline', () => { power().onResume() } run(1) - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ graces: PROFILE_STATE_WRITER_MAX_GRACES }) ) @@ -170,7 +185,7 @@ describe('profile state writer deadline', () => { run(TIMEOUT_MS) // An exhausted resume must not cancel the final queued timeout check. power().onResume() - await waitForPoll() + await drainTimeoutCheck() expect(onGrace).toHaveBeenCalledTimes(PROFILE_STATE_WRITER_MAX_GRACES) expect(onTimeout).toHaveBeenCalledOnce() expect(vi.getTimerCount()).toBe(0) @@ -183,7 +198,7 @@ describe('profile state writer deadline', () => { const { deadline, onTimeout, unsubscribe, run } = harness() run(TIMEOUT_MS) deadline.clear() - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).not.toHaveBeenCalled() expect(unsubscribe).toHaveBeenCalledOnce() expect(vi.getTimerCount()).toBe(0) @@ -193,10 +208,10 @@ describe('profile state writer deadline', () => { const { onTimeout, run, power } = harness() run(TIMEOUT_MS) power().onResume() - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).not.toHaveBeenCalled() run(TIMEOUT_MS) - await waitForPoll() + await drainTimeoutCheck() expect(onTimeout).toHaveBeenCalledOnce() expect(vi.getTimerCount()).toBe(0) }) diff --git a/src/main/persistence/profile-state/profile-state-writer-deadline.ts b/src/main/persistence/profile-state/profile-state-writer-deadline.ts index fc17faea8b7d..9bc2db835004 100644 --- a/src/main/persistence/profile-state/profile-state-writer-deadline.ts +++ b/src/main/persistence/profile-state/profile-state-writer-deadline.ts @@ -97,13 +97,15 @@ export function createProfileStateWriterDeadline( ) { return } - // Even an on-time timer can run before a reply already queued by the worker. + // Windows can deliver a queued worker reply after the first poll following a stall. timeoutCheck = setImmediate(() => { - timeoutCheck = undefined - if (active) { - clear() - onTimeout(expiry) - } + timeoutCheck = setImmediate(() => { + timeoutCheck = undefined + if (active) { + clear() + onTimeout(expiry) + } + }) }) } // Subscription replays the current state synchronously; only later resumes re-arm. diff --git a/src/main/persistence/profile-state/profile-state-writer-electron-fixture.ts b/src/main/persistence/profile-state/profile-state-writer-electron-fixture.ts index e67832e8fc7e..12f946c27bb2 100644 --- a/src/main/persistence/profile-state/profile-state-writer-electron-fixture.ts +++ b/src/main/persistence/profile-state/profile-state-writer-electron-fixture.ts @@ -38,19 +38,29 @@ async function run(): Promise { const counters = new SharedArrayBuffer(8) const counts = new Int32Array(counters) const failures: string[] = [] + let phase = 'initialize' const initialization = { databasePath, profileId, revision: 0, counters } const client = new ProfileStateWriteWorkerClient(initialization, { workerPath: join(root, 'observed-worker.cjs'), timeoutMs, - onFailure: (error) => failures.push(error.message) + onFailure: (error) => { + failures.push(error.message) + console.error('[writer-fixture] failure', { + phase, + queuedReplies: Atomics.load(counts, 0), + workerStarts: Atomics.load(counts, 1) + }) + } }) const write = (marker: string) => client.writeSerializedDomains([{ domain: 'ui', payload: JSON.stringify({ marker }) }]) const revisions: number[] = [] try { await client.ready + phase = 'before' assert.equal(await write('before'), 1) for (let cycle = 0; cycle < 4; cycle += 1) { + phase = `stall-${cycle}` let pending: Promise | undefined await new Promise((resolve, reject) => { // Dispatch from check so the overdue timer runs before the next poll. @@ -76,6 +86,7 @@ async function run(): Promise { } revisions.push(await pending) } + phase = 'after' assert.equal(await write('after'), 6) } finally { await client.close() From 69900f023269fd81f51b52b9357d12b4351329f7 Mon Sep 17 00:00:00 2001 From: Neil Date: Wed, 7 Oct 2026 22:06:46 -0700 Subject: [PATCH 02/17] Share provider search between workspace creation and reference editing --- ...spaceNameField-provider-boundaries.test.ts | 2 +- ...rkspaceNameField-repo-slug-routing.test.ts | 2 +- ...spaceNameField.jira-accessibility.test.tsx | 5 +- .../new-workspace/SmartWorkspaceNameField.tsx | 7 - .../SmartWorkspaceSourceResultList.tsx | 166 ++++++++++++++ .../SmartWorkspaceSourceTabs.tsx | 57 +++++ .../base-ref-picker-visibility.ts | 4 +- .../jira-source-status-message.ts | 43 ++++ .../new-workspace-composer-card-props.ts | 6 +- .../smart-workspace-localized-options.tsx | 2 +- .../smart-workspace-name-field-copy.ts | 2 +- .../smart-workspace-name-field-model.ts | 54 +++-- .../smart-workspace-name-field-surface.tsx | 119 +--------- .../smart-workspace-name-input-surface.tsx | 2 +- ...smart-workspace-source-results-surface.tsx | 123 +---------- .../smart-workspace-source-results.test.ts | 2 +- .../smart-workspace-source-results.ts | 2 - .../use-linear-source-connection.test.tsx | 182 +++++++++++++++ .../use-linear-source-connection.ts | 82 +++++++ .../use-smart-workspace-field-availability.ts | 161 ++------------ ...rt-workspace-field-focus-controls.test.tsx | 31 +++ ...se-smart-workspace-field-focus-controls.ts | 41 ++-- .../use-smart-workspace-github-search.ts | 10 +- .../use-smart-workspace-gitlab-search.ts | 14 +- .../use-smart-workspace-name-field-actions.ts | 43 ++-- ...e-smart-workspace-name-field-controller.ts | 207 +++++++----------- ...pace-name-field-foundation.source.test.tsx | 167 ++++++++++++++ .../use-smart-workspace-secondary-searches.ts | 20 +- ....ts => use-work-item-source-foundation.ts} | 76 ++++--- ...s => use-work-item-source-presentation.ts} | 55 +---- .../use-work-item-source-search.ts | 155 +++++++++++++ ...state.ts => use-work-item-source-state.ts} | 24 +- .../work-item-source-selection.test.ts | 69 ++++++ .../work-item-source-selection.ts | 103 +++++++++ .../folder-workspace-composer-helpers.ts | 2 +- ...rkspace-reference-fixtures.test-support.ts | 78 +++++++ .../composer-state/composer-source-model.ts | 2 +- .../hooks/composer-state/identity-model.ts | 2 +- .../composer-state/issue-source-actions.ts | 2 +- .../workspace-identity-state.ts | 2 +- .../src/i18n/en-runtime-required.json | 1 - 41 files changed, 1421 insertions(+), 706 deletions(-) create mode 100644 src/renderer/src/components/new-workspace/SmartWorkspaceSourceResultList.tsx create mode 100644 src/renderer/src/components/new-workspace/SmartWorkspaceSourceTabs.tsx create mode 100644 src/renderer/src/components/new-workspace/jira-source-status-message.ts delete mode 100644 src/renderer/src/components/new-workspace/smart-workspace-source-results.ts create mode 100644 src/renderer/src/components/new-workspace/use-linear-source-connection.test.tsx create mode 100644 src/renderer/src/components/new-workspace/use-linear-source-connection.ts create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-field-focus-controls.test.tsx create mode 100644 src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx rename src/renderer/src/components/new-workspace/{use-smart-workspace-name-field-foundation.ts => use-work-item-source-foundation.ts} (72%) rename src/renderer/src/components/new-workspace/{use-smart-workspace-name-field-presentation.ts => use-work-item-source-presentation.ts} (84%) create mode 100644 src/renderer/src/components/new-workspace/use-work-item-source-search.ts rename src/renderer/src/components/new-workspace/{use-smart-workspace-name-field-state.ts => use-work-item-source-state.ts} (74%) create mode 100644 src/renderer/src/components/new-workspace/work-item-source-selection.test.ts create mode 100644 src/renderer/src/components/new-workspace/work-item-source-selection.ts create mode 100644 src/renderer/src/components/sidebar/workspace-reference-fixtures.test-support.ts diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField-provider-boundaries.test.ts b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField-provider-boundaries.test.ts index 51a394e416be..3e1912250d51 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField-provider-boundaries.test.ts +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField-provider-boundaries.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { canUseGitLabSmartSource } from './SmartWorkspaceNameField' +import { canUseGitLabSmartSource } from './smart-workspace-provider-availability' describe('SmartWorkspaceNameField provider boundaries', () => { it('advertises local GitLab smart lookup only when local glab is available', () => { diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts index f2f6d4bd787a..32572a3b7eff 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { callRuntimeRpc } from '@/runtime/runtime-rpc-client' import type * as RuntimeRpcClient from '@/runtime/runtime-rpc-client' import type { TaskSourceContext } from '../../../../shared/task-source-context' -import { getRepoSlugCached } from './SmartWorkspaceNameField' +import { getRepoSlugCached } from './smart-workspace-repo-slug' vi.mock('@/runtime/runtime-rpc-client', async (importOriginal) => ({ ...(await importOriginal()), diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx index 54c2e0832c74..51018fb8c446 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx @@ -4,9 +4,8 @@ import React, { act } from 'react' import { cleanup, createEvent, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { JiraIssue } from '../../../../shared/jira-types' -import SmartWorkspaceNameField, { - type SmartWorkspaceNameSelection -} from './SmartWorkspaceNameField' +import SmartWorkspaceNameField from './SmartWorkspaceNameField' +import type { SmartWorkspaceNameSelection } from './smart-workspace-name-field-model' const jiraMock = vi.hoisted(() => ({ retry: vi.fn(), diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx index f9165998e5af..4d7c34423fa1 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx @@ -3,13 +3,6 @@ import { renderSmartWorkspaceNameField } from './smart-workspace-name-field-surf import type { SmartWorkspaceNameFieldProps } from './smart-workspace-name-field-model' import { useSmartWorkspaceNameFieldController } from './use-smart-workspace-name-field-controller' -export type { - SmartWorkspaceNameSelection, - SmartWorkspaceNameFieldProps -} from './smart-workspace-name-field-model' -export { canUseGitLabSmartSource } from './smart-workspace-provider-availability' -export { getRepoSlugCached } from './smart-workspace-repo-slug' - export default function SmartWorkspaceNameField( props: SmartWorkspaceNameFieldProps ): React.JSX.Element { diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceSourceResultList.tsx b/src/renderer/src/components/new-workspace/SmartWorkspaceSourceResultList.tsx new file mode 100644 index 000000000000..f0c62a5f14e5 --- /dev/null +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceSourceResultList.tsx @@ -0,0 +1,166 @@ +import React from 'react' +import { Button } from '@/components/ui/button' +import { CommandGroup, CommandItem, CommandList } from '@/components/ui/command' +import { translate } from '@/i18n/i18n' +import { cn } from '@/lib/utils' +import { getSmartWorkspaceEmptyHint } from '../../../../shared/new-workspace/smart-workspace-source-results' +import { getRowItemClassName, RowIcon, RowLabel } from './smart-workspace-source-row-content' +import type { RowEntry } from './smart-workspace-name-field-model' +import type { WorkItemSourceSearch } from './use-work-item-source-search' + +export function SmartWorkspaceSourceResultList({ + model, + onSelect, + isLinked, + beforeResults, + emptyHint, + isRowDisabled +}: { + model: Pick< + WorkItemSourceSearch, + | 'mode' + | 'mrStateFilters' + | 'mrStateFilter' + | 'setMrStateFilter' + | 'typedTextActionRow' + | 'jiraSource' + | 'loading' + | 'searchResultRows' + | 'linearStatusChecked' + | 'linearStatus' + | 'showJiraSiteContext' + | 'jiraConnectionStatus' + | 'reserveLinearLoadingResults' + | 'showLinearUrlLoadingFeedback' + > + onSelect: (row: RowEntry) => void + isLinked?: (row: RowEntry) => boolean + beforeResults?: React.ReactNode + emptyHint?: string + isRowDisabled?: (row: RowEntry) => boolean +}): React.JSX.Element { + const { + mode, + mrStateFilters, + mrStateFilter, + setMrStateFilter, + typedTextActionRow, + jiraSource, + loading, + searchResultRows, + linearStatusChecked, + linearStatus, + showJiraSiteContext, + jiraConnectionStatus, + reserveLinearLoadingResults, + showLinearUrlLoadingFeedback + } = model + return ( + <> + {mode === 'gitlab' ? ( + // Why: state chips mirror GitLab's merge-request tab strip. +
e.preventDefault()} + > + {mrStateFilters.map(({ id, label }) => ( + + ))} +
+ ) : null} + + {beforeResults} + {typedTextActionRow ? ( +
event.preventDefault()} + > + onSelect(typedTextActionRow)} + className={getRowItemClassName(typedTextActionRow, { pinnedAction: true })} + > + + + +
+ ) : null} + {jiraSource.errorKind ? null : reserveLinearLoadingResults ? ( +