diff --git a/src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts b/src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts index c2d02d7d995..4d26e7e00af 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts +++ b/src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts @@ -102,6 +102,47 @@ describe('Branch source results', () => { ]) }) + it('uses Linear rows from a paginated collection shape', () => { + const rows = buildSmartWorkspaceSourceRows({ + mode: 'smart', + value: '', + branches: [], + githubItems: [], + gitlabItems: [], + linearIssues: { + items: [{ id: 'linear-1', identifier: 'ENG-1', title: 'Fix composer crash' } as never], + hasMore: true + }, + gitlabAvailable: false, + linearAvailable: true, + resultLimit: 12 + }) + + expect(rows).toEqual([ + { + kind: 'linear', + value: 'linear-linear-1', + issue: { id: 'linear-1', identifier: 'ENG-1', title: 'Fix composer crash' } + } + ]) + }) + + it('ignores malformed Linear collection rows instead of throwing during render', () => { + expect(() => + buildSmartWorkspaceSourceRows({ + mode: 'smart', + value: '', + branches: [], + githubItems: [], + gitlabItems: [], + linearIssues: { items: { id: 'not-an-array' } } as never, + gitlabAvailable: false, + linearAvailable: true, + resultLimit: 12 + }) + ).not.toThrow() + }) + it('describes empty Branch results after the empty-query search runs', () => { expect(getSmartWorkspaceEmptyHint('branches')).toBe('No matching branches.') }) diff --git a/src/renderer/src/components/new-workspace/smart-workspace-source-results.ts b/src/renderer/src/components/new-workspace/smart-workspace-source-results.ts index 6c3cb694050..82c10ff19f7 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-source-results.ts +++ b/src/renderer/src/components/new-workspace/smart-workspace-source-results.ts @@ -2,6 +2,7 @@ import type { BaseRefSearchResult, GitHubWorkItem, GitLabWorkItem, + LinearCollectionResult, LinearIssue } from '../../../../shared/types' @@ -15,6 +16,8 @@ export type SmartWorkspaceSourceRow = | { kind: 'branch'; value: string; refName: string; localBranchName: string } | { kind: 'linear'; value: string; issue: LinearIssue } +type LinearIssueSourceInput = LinearIssue[] | LinearCollectionResult | null | undefined + const EMPTY_HINT_BY_MODE: Record = { smart: 'Start typing to create a name or find a source.', github: 'Start typing to search GitHub PRs and issues.', @@ -92,7 +95,7 @@ export function buildSmartWorkspaceSourceRows({ gitlabAvailable: boolean gitlabItems: GitLabWorkItem[] linearAvailable: boolean - linearIssues: LinearIssue[] + linearIssues: LinearIssueSourceInput mode: SmartNameMode resultLimit: number value: string @@ -141,8 +144,15 @@ export function buildSmartWorkspaceSourceRows({ ) } if (linearAvailable && (mode === 'smart' || mode === 'linear')) { + // Why: mixed-version runtime responses may briefly carry the paginated + // collection shape into this render path; rendering must stay recoverable. + const resolvedLinearIssues = Array.isArray(linearIssues) + ? linearIssues + : Array.isArray(linearIssues?.items) + ? linearIssues.items + : [] nextRows.push( - ...linearIssues.map((issue) => ({ + ...resolvedLinearIssues.map((issue) => ({ kind: 'linear' as const, value: `linear-${issue.id}`, issue diff --git a/src/renderer/src/runtime/runtime-linear-client.test.ts b/src/renderer/src/runtime/runtime-linear-client.test.ts index 111887f750e..2802c09d631 100644 --- a/src/renderer/src/runtime/runtime-linear-client.test.ts +++ b/src/renderer/src/runtime/runtime-linear-client.test.ts @@ -132,6 +132,14 @@ describe('runtime linear client', () => { expect(runtimeEnvironmentCall).not.toHaveBeenCalled() }) + it('wraps legacy local Linear issue list arrays as collection results', async () => { + linearListIssuesLocal.mockResolvedValue([{ id: 'legacy-issue' }]) + + await expect( + linearListIssues({ activeRuntimeEnvironmentId: null }, 'assigned', 20) + ).resolves.toEqual({ items: [{ id: 'legacy-issue' }] }) + }) + it('does not throw when an older local preload lacks project listing', async () => { delete (window.api.linear as { listProjects?: unknown }).listProjects @@ -285,6 +293,39 @@ describe('runtime linear client', () => { expect(linearListIssuesLocal).not.toHaveBeenCalled() }) + it('wraps legacy remote Linear issue list arrays as collection results', async () => { + runtimeEnvironmentCall.mockResolvedValueOnce({ + id: 'rpc-list', + ok: true, + result: [{ id: 'legacy-issue' }], + _meta: { runtimeId: 'runtime-1' } + }) + + await expect( + linearListIssues({ activeRuntimeEnvironmentId: 'env-1' }, 'assigned', 20) + ).resolves.toEqual({ items: [{ id: 'legacy-issue' }] }) + + expect(runtimeEnvironmentCall).toHaveBeenCalledWith({ + selector: 'env-1', + method: 'linear.listIssues', + params: { filter: 'assigned', limit: 20, workspaceId: undefined }, + timeoutMs: 30_000 + }) + }) + + it('falls back to an empty Linear issue collection for malformed list responses', async () => { + runtimeEnvironmentCall.mockResolvedValueOnce({ + id: 'rpc-list', + ok: true, + result: { items: { id: 'not-an-array' }, hasMore: true }, + _meta: { runtimeId: 'runtime-1' } + }) + + await expect( + linearListIssues({ activeRuntimeEnvironmentId: 'env-1' }, 'assigned', 20) + ).resolves.toEqual({ items: [] }) + }) + it('routes Linear mutations and metadata through the selected runtime environment', async () => { runtimeEnvironmentCall .mockResolvedValueOnce({ diff --git a/src/renderer/src/runtime/runtime-linear-client.ts b/src/renderer/src/runtime/runtime-linear-client.ts index c934c298312..9071c434915 100644 --- a/src/renderer/src/runtime/runtime-linear-client.ts +++ b/src/renderer/src/runtime/runtime-linear-client.ts @@ -39,6 +39,26 @@ function linearReadForce(options?: LinearReadOptions): { force: true } | {} { return options?.force ? { force: true } : {} } +function normalizeLinearIssueCollectionResult( + result: unknown +): LinearCollectionResult { + if (Array.isArray(result)) { + return { items: result as LinearIssue[] } + } + if (!result || typeof result !== 'object') { + return { items: [] } + } + const collection = result as Partial> + if (!Array.isArray(collection.items)) { + return { items: [] } + } + return { + items: collection.items, + ...(Array.isArray(collection.errors) ? { errors: collection.errors } : {}), + ...(typeof collection.hasMore === 'boolean' ? { hasMore: collection.hasMore } : {}) + } +} + export async function linearStatus( settings: RuntimeLinearSettings ): Promise { @@ -144,14 +164,20 @@ export async function linearListIssues( workspaceId?: LinearWorkspaceSelection | null ): Promise> { const target = getActiveRuntimeTarget(settings) - return target.kind === 'environment' - ? callRuntimeRpc>( - target, - 'linear.listIssues', - { filter, limit, workspaceId: workspaceId ?? undefined }, - { timeoutMs: 30_000 } - ) - : window.api.linear.listIssues({ filter, limit, workspaceId: workspaceId ?? undefined }) + const result = + target.kind === 'environment' + ? await callRuntimeRpc( + target, + 'linear.listIssues', + { filter, limit, workspaceId: workspaceId ?? undefined }, + { timeoutMs: 30_000 } + ) + : await window.api.linear.listIssues({ + filter, + limit, + workspaceId: workspaceId ?? undefined + }) + return normalizeLinearIssueCollectionResult(result) } export async function linearCreateIssue(