From 9b2b0e59e520def69b4d7afa68931b1a65cc22bb Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 7 Sep 2026 14:58:29 -0400 Subject: [PATCH] refactor(mobile): run hosted task operations over the generic host lane The hosted page now uses the same task operation modules as the native app, so the twelve web adapters that only re-shaped shell results are gone. Only the project table keeps a page-side module, because it arrives windowed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- mobile/host-web-app/h/[hostId]/tasks.tsx | 48 +++--- ...le-web-tasks-screen-binding-source.test.ts | 12 +- ...ative-host-task-project-read-operations.ts | 2 +- .../tasks/web-host-task-detail-operations.ts | 28 ---- .../web-host-task-item-file-operations.ts | 45 ----- .../web-host-task-item-mutation-operations.ts | 25 --- .../web-host-task-item-review-operations.ts | 44 ----- .../tasks/web-host-task-linear-operations.ts | 50 ------ .../tasks/web-host-task-list-operations.ts | 18 -- .../tasks/web-host-task-operations.test.ts | 156 ++++++++++++++++++ ...eb-host-task-preference-operations.test.ts | 41 ----- .../web-host-task-preference-operations.ts | 16 -- .../web-host-task-project-file-operations.ts | 40 ----- ...b-host-task-project-mutation-operations.ts | 93 ----------- .../web-host-task-project-read-operations.ts | 89 +++++----- ...web-host-task-provider-write-operations.ts | 13 -- .../tasks/web-host-task-read-operations.ts | 36 ---- 17 files changed, 234 insertions(+), 522 deletions(-) delete mode 100644 mobile/src/tasks/web-host-task-detail-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-item-file-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-item-mutation-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-item-review-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-linear-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-list-operations.ts create mode 100644 mobile/src/tasks/web-host-task-operations.test.ts delete mode 100644 mobile/src/tasks/web-host-task-preference-operations.test.ts delete mode 100644 mobile/src/tasks/web-host-task-preference-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-project-file-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-project-mutation-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-provider-write-operations.ts delete mode 100644 mobile/src/tasks/web-host-task-read-operations.ts diff --git a/mobile/host-web-app/h/[hostId]/tasks.tsx b/mobile/host-web-app/h/[hostId]/tasks.tsx index 58974df04c2..cbc265a013d 100644 --- a/mobile/host-web-app/h/[hostId]/tasks.tsx +++ b/mobile/host-web-app/h/[hostId]/tasks.tsx @@ -1,19 +1,20 @@ import { useMemo } from 'react' import MobileTasksScreen from '../../../app/h/[hostId]/tasks' import { useMobileWebNativeShell } from '../../../../src/mobile-web/src/native-shell-channel' -import { webHostTaskDetailOperations } from '../../../src/tasks/web-host-task-detail-operations' +import { nativeHostTaskDetailOperations } from '../../../src/tasks/native-host-task-detail-operations' +import { nativeHostTaskItemFileOperations } from '../../../src/tasks/native-host-task-item-file-operations' +import { nativeHostTaskItemMutationOperations } from '../../../src/tasks/native-host-task-item-mutation-operations' +import { nativeHostTaskItemReviewOperations } from '../../../src/tasks/native-host-task-item-review-operations' +import { nativeHostTaskLinearOperations } from '../../../src/tasks/native-host-task-linear-operations' +import { nativeHostTaskListOperations } from '../../../src/tasks/native-host-task-list-operations' +import { nativeHostTaskPreferenceOperations } from '../../../src/tasks/native-host-task-preference-operations' +import { nativeHostTaskProjectFileOperations } from '../../../src/tasks/native-host-task-project-file-operations' +import { nativeHostTaskProjectMutationOperations } from '../../../src/tasks/native-host-task-project-mutation-operations' +import { nativeHostTaskProviderWriteOperations } from '../../../src/tasks/native-host-task-provider-write-operations' +import { nativeHostTaskReadOperations } from '../../../src/tasks/native-host-task-read-operations' import { webHostTaskDeviceOperations } from '../../../src/tasks/web-host-task-device-operations' -import { webHostTaskItemFileOperations } from '../../../src/tasks/web-host-task-item-file-operations' -import { webHostTaskItemMutationOperations } from '../../../src/tasks/web-host-task-item-mutation-operations' -import { webHostTaskItemReviewOperations } from '../../../src/tasks/web-host-task-item-review-operations' -import { webHostTaskLinearOperations } from '../../../src/tasks/web-host-task-linear-operations' -import { webHostTaskListOperations } from '../../../src/tasks/web-host-task-list-operations' -import { webHostTaskPreferenceOperations } from '../../../src/tasks/web-host-task-preference-operations' -import { webHostTaskProjectFileOperations } from '../../../src/tasks/web-host-task-project-file-operations' -import { webHostTaskProjectMutationOperations } from '../../../src/tasks/web-host-task-project-mutation-operations' import { webHostTaskProjectReadOperations } from '../../../src/tasks/web-host-task-project-read-operations' -import { webHostTaskProviderWriteOperations } from '../../../src/tasks/web-host-task-provider-write-operations' -import { webHostTaskReadOperations } from '../../../src/tasks/web-host-task-read-operations' +import { webHostTaskRpcSender } from '../../../src/tasks/web-host-task-rpc-sender' import { webHostWorkspaceCreationOperations } from '../../../src/worktree/web-host-workspace-creation-operations' const HOSTED_PAGE_HOST_ID = 'paired-orca-desktop' @@ -24,20 +25,21 @@ export default function HostMobileWebTasksRoute() { if (!shell.client) { return null } + const sender = webHostTaskRpcSender(shell.client.host) return { - detail: webHostTaskDetailOperations(shell.client), + detail: nativeHostTaskDetailOperations(sender), device: webHostTaskDeviceOperations(shell.client), - itemFile: webHostTaskItemFileOperations(shell.client), - itemMutation: webHostTaskItemMutationOperations(shell.client), - itemReview: webHostTaskItemReviewOperations(shell.client), - linear: webHostTaskLinearOperations(shell.client), - list: webHostTaskListOperations(shell.client), - preference: webHostTaskPreferenceOperations(shell.client), - projectFile: webHostTaskProjectFileOperations(shell.client), - projectMutation: webHostTaskProjectMutationOperations(shell.client), - projectRead: webHostTaskProjectReadOperations(shell.client), - providerWrite: webHostTaskProviderWriteOperations(shell.client), - read: webHostTaskReadOperations(shell.client), + itemFile: nativeHostTaskItemFileOperations(sender), + itemMutation: nativeHostTaskItemMutationOperations(sender), + itemReview: nativeHostTaskItemReviewOperations(sender), + linear: nativeHostTaskLinearOperations(sender), + list: nativeHostTaskListOperations(sender), + preference: nativeHostTaskPreferenceOperations(sender), + projectFile: nativeHostTaskProjectFileOperations(sender), + projectMutation: nativeHostTaskProjectMutationOperations(sender), + projectRead: webHostTaskProjectReadOperations(sender), + providerWrite: nativeHostTaskProviderWriteOperations(sender), + read: nativeHostTaskReadOperations(sender), workspaceCreation: webHostWorkspaceCreationOperations(shell.client) } }, [shell.client]) diff --git a/mobile/src/tasks/mobile-web-tasks-screen-binding-source.test.ts b/mobile/src/tasks/mobile-web-tasks-screen-binding-source.test.ts index 24b717db759..51bf4152cbc 100644 --- a/mobile/src/tasks/mobile-web-tasks-screen-binding-source.test.ts +++ b/mobile/src/tasks/mobile-web-tasks-screen-binding-source.test.ts @@ -16,17 +16,7 @@ describe('mobile web Tasks screen binding', () => { "import MobileTasksScreen from '../../../app/h/[hostId]/tasks'" ) expect(hostedTasksRoute).toContain('(request: Promise): Promise { +export async function projectResult(request: Promise): Promise { const response = (await request) as { ok: boolean result?: { ok?: boolean; error?: { message?: string } } diff --git a/mobile/src/tasks/web-host-task-detail-operations.ts b/mobile/src/tasks/web-host-task-detail-operations.ts deleted file mode 100644 index b0a41b70364..00000000000 --- a/mobile/src/tasks/web-host-task-detail-operations.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { HostTaskDetailOperations } from './host-task-detail-operations' - -export function webHostTaskDetailOperations( - client: MobileWebBridgeClient -): HostTaskDetailOperations { - return { - async listGitHubLabels(repoId) { - return (await client.task.listGitHubLabels({ repoId })).labels - }, - async listGitHubAssignableUsers(repoId) { - return (await client.task.listGitHubAssignableUsers({ repoId })).users - }, - loadGitHub: (payload) => client.task.loadGitHubDetail(payload), - loadGitLab(payload) { - if (!payload.targetId) { - throw new Error('Task target is unavailable') - } - return client.task.loadGitLabDetail({ targetId: payload.targetId }) - }, - loadLinear(payload) { - if (!payload.targetId) { - throw new Error('Linear task target is unavailable') - } - return client.task.loadLinearDetail({ targetId: payload.targetId }) - } - } -} diff --git a/mobile/src/tasks/web-host-task-item-file-operations.ts b/mobile/src/tasks/web-host-task-item-file-operations.ts deleted file mode 100644 index d3899614388..00000000000 --- a/mobile/src/tasks/web-host-task-item-file-operations.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { HostTaskGitHubItemTarget } from './host-task-item-mutation-operations' -import type { HostTaskItemFileOperations } from './host-task-item-file-operations' - -export function webHostTaskItemFileOperations( - client: MobileWebBridgeClient -): HostTaskItemFileOperations { - return { - async refreshChecks(target) { - return (await client.task.refreshHostedTaskChecks({ targetId: targetId(target) })).checks - }, - async rerunChecks(target, _headSha, failedOnly) { - await client.task.rerunHostedTaskChecks({ targetId: targetId(target), failedOnly }) - }, - async setFileViewed(target, payload) { - await client.task.setHostedTaskFileViewed({ - targetId: targetId(target), - path: payload.path, - viewed: payload.viewed - }) - }, - loadFileContents: (target, payload) => - client.task.loadHostedTaskFileContents({ - targetId: targetId(target), - path: payload.path - }), - async addInlineComment(target, payload) { - return ( - await client.task.addHostedTaskInlineComment({ - targetId: targetId(target), - path: payload.path, - line: payload.line, - body: payload.body - }) - ).comment - } - } -} - -function targetId(target: HostTaskGitHubItemTarget): string { - if (!target.targetId) { - throw new Error('Task file authority is unavailable') - } - return target.targetId -} diff --git a/mobile/src/tasks/web-host-task-item-mutation-operations.ts b/mobile/src/tasks/web-host-task-item-mutation-operations.ts deleted file mode 100644 index d974921e06b..00000000000 --- a/mobile/src/tasks/web-host-task-item-mutation-operations.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { - HostTaskItemMutationOperations, - HostTaskItemMutationTarget -} from './host-task-item-mutation-operations' - -export function webHostTaskItemMutationOperations( - client: MobileWebBridgeClient -): HostTaskItemMutationOperations { - return { - async setClosed(target, closed) { - await client.task.updateHostedTaskStatus({ targetId: targetId(target), closed }) - }, - async updateMetadata(target, updates) { - await client.task.updateHostedTaskMetadata({ targetId: targetId(target), updates }) - } - } -} - -function targetId(target: HostTaskItemMutationTarget): string { - if (!target.targetId) { - throw new Error('Task mutation authority is unavailable') - } - return target.targetId -} diff --git a/mobile/src/tasks/web-host-task-item-review-operations.ts b/mobile/src/tasks/web-host-task-item-review-operations.ts deleted file mode 100644 index cee93b65e4e..00000000000 --- a/mobile/src/tasks/web-host-task-item-review-operations.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { HostTaskItemMutationTarget } from './host-task-item-mutation-operations' -import type { HostTaskItemReviewOperations } from './host-task-item-review-operations' - -export function webHostTaskItemReviewOperations( - client: MobileWebBridgeClient -): HostTaskItemReviewOperations { - return { - async addComment(target, body) { - return (await client.task.addHostedTaskComment({ targetId: targetId(target), body })).comment - }, - async requestReviewers(target, reviewers) { - await client.task.requestHostedTaskReviewers({ - targetId: targetId(target), - reviewers - }) - }, - async resolveThread(target, threadId, resolve) { - await client.task.resolveHostedTaskReviewThread({ - targetId: targetId(target), - threadId, - resolve - }) - }, - async replyReviewComment(target, payload) { - // The hosted bridge publishes no comment on reply; callers fall back to a local entry. - await client.task.replyHostedTaskReviewComment({ - targetId: targetId(target), - ...payload - }) - return undefined - }, - async merge(target, method) { - await client.task.mergeHostedTaskReview({ targetId: targetId(target), method }) - } - } -} - -function targetId(target: HostTaskItemMutationTarget): string { - if (!target.targetId) { - throw new Error('Task review authority is unavailable') - } - return target.targetId -} diff --git a/mobile/src/tasks/web-host-task-linear-operations.ts b/mobile/src/tasks/web-host-task-linear-operations.ts deleted file mode 100644 index 80f793674dc..00000000000 --- a/mobile/src/tasks/web-host-task-linear-operations.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { HostTaskLinearOperations, HostTaskLinearTarget } from './host-task-linear-operations' - -export function webHostTaskLinearOperations( - client: MobileWebBridgeClient -): HostTaskLinearOperations { - return { - async connect(apiKey) { - await client.task.connectLinear({ apiKey }) - }, - async listTeams() { - return (await client.task.listLinearTeams({})).teams - }, - async teamStates(target) { - return (await client.task.listLinearTeamStates({ targetId: targetId(target) })).states - }, - async selectWorkspace(workspaceId) { - await client.task.selectLinearWorkspace({ workspaceId }) - }, - async updateState(target, stateId) { - await client.task.updateLinearIssueState({ targetId: targetId(target), stateId }) - }, - async addComment(target, body) { - return (await client.task.addLinearIssueComment({ targetId: targetId(target), body })).id - }, - async loadIssue(target) { - return (await client.task.loadLinearIssue({ targetId: targetId(target) })).issue - }, - async createSubIssue(target, title) { - return (await client.task.createLinearSubIssue({ targetId: targetId(target), title })).issue - }, - async createIssue(payload) { - return ( - await client.task.createLinearIssue({ - teamId: payload.team.id, - ...(payload.team.workspaceId ? { workspaceId: payload.team.workspaceId } : {}), - title: payload.title, - ...(payload.description ? { description: payload.description } : {}) - }) - ).issue - } - } -} - -function targetId(target: HostTaskLinearTarget): string { - if (!target.targetId) { - throw new Error('Linear task authority is unavailable') - } - return target.targetId -} diff --git a/mobile/src/tasks/web-host-task-list-operations.ts b/mobile/src/tasks/web-host-task-list-operations.ts deleted file mode 100644 index 0b32f74f390..00000000000 --- a/mobile/src/tasks/web-host-task-list-operations.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { HostTaskListOperations } from './host-task-list-operations' - -export function webHostTaskListOperations(client: MobileWebBridgeClient): HostTaskListOperations { - return { - listGitHub: (payload) => client.task.listGitHub(payload), - async countGitHub(payload) { - return (await client.task.countGitHub(payload)).count - }, - listGitLab: (payload) => client.task.listGitLab(payload), - async listGitLabTodos(repoId) { - return (await client.task.listGitLabTodos({ repoId })).items - }, - async listLinear(payload) { - return (await client.task.listLinear(payload)).items - } - } -} diff --git a/mobile/src/tasks/web-host-task-operations.test.ts b/mobile/src/tasks/web-host-task-operations.test.ts new file mode 100644 index 00000000000..5c27f6c259f --- /dev/null +++ b/mobile/src/tasks/web-host-task-operations.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it, vi } from 'vitest' +import type { MobileWebHostRequestClient } from '../../../src/mobile-web/src/mobile-web-host-request-client' +import { nativeHostTaskItemMutationOperations } from './native-host-task-item-mutation-operations' +import { nativeHostTaskListOperations } from './native-host-task-list-operations' +import { nativeHostTaskPreferenceOperations } from './native-host-task-preference-operations' +import { nativeHostTaskReadOperations } from './native-host-task-read-operations' +import { webHostTaskProjectReadOperations } from './web-host-task-project-read-operations' +import { webHostTaskRpcSender } from './web-host-task-rpc-sender' + +type HostRequest = { method: string; params: Record } + +function hostFixture(reply: (request: HostRequest) => unknown) { + const requests: HostRequest[] = [] + const request = vi.fn(async (payload: HostRequest) => { + requests.push(payload) + return reply(payload) + }) + const host = { request } as unknown as MobileWebHostRequestClient + return { requests, sender: webHostTaskRpcSender(host) } +} + +function projectRow(index: number) { + return { + id: `row-${index}`, + itemType: 'ISSUE' as const, + position: index, + updatedAt: '2026-09-07T00:00:00Z', + fieldValuesByFieldId: {}, + content: { + number: index + 1, + title: `Item ${index}`, + body: null, + url: 'https://github.com/octo/app/issues/1', + state: 'OPEN', + isDraft: false, + repository: 'octo/app', + labels: [], + assignees: [] + } + } +} + +function projectTable(rows: ReturnType[], totalCount: number) { + return { + project: { + owner: 'octo', + ownerType: 'organization', + number: 4, + host: 'github.com', + id: 'PVT_1', + title: 'Roadmap', + url: 'https://github.com/orgs/octo/projects/4' + }, + selectedView: { + id: 'PVTV_1', + number: 1, + name: 'Board', + filter: '', + layout: 'BOARD_LAYOUT', + fields: [], + groupByFields: [], + sortByFields: [] + }, + totalCount, + rows + } +} + +const projectRequest = { + owner: 'octo', + ownerType: 'organization' as const, + number: 4, + viewId: 'PVTV_1' +} + +describe('hosted task operations over the generic host lane', () => { + it('names the desktop method the native app calls and sends no workspace handle', async () => { + const f = hostFixture(() => ({ repos: [{ id: 'repo-1', displayName: 'app' }] })) + await nativeHostTaskReadOperations(f.sender).listRepositories() + expect(f.requests).toEqual([{ method: 'repo.list', params: {} }]) + }) + + it('addresses a work item by its host repository and number, not an opaque handle', async () => { + const f = hostFixture(() => ({ ok: true })) + await nativeHostTaskItemMutationOperations(f.sender).setClosed( + { provider: 'github', repoId: 'repo-1', number: 12, type: 'issue' }, + true + ) + expect(f.requests[0]!.method).toBe('github.updateIssue') + expect(f.requests[0]!.params).toMatchObject({ repo: 'id:repo-1', number: 12 }) + }) + + it('forwards a list request unchanged and returns the host items', async () => { + const f = hostFixture(() => ({ items: [{ number: 7, type: 'issue', title: 'Bug' }] })) + const result = await nativeHostTaskListOperations(f.sender).listGitHub({ + repoId: 'repo-1', + limit: 25, + filter: 'all' + } as Parameters['listGitHub']>[0]) + expect(f.requests[0]!.method).toBe('github.listWorkItems') + expect(result.items).toHaveLength(1) + }) + + it('writes a task preference straight to the desktop settings method', async () => { + const f = hostFixture(() => ({})) + await nativeHostTaskPreferenceOperations(f.sender).updateSettings({ + defaultTaskSource: 'github' + }) + expect(f.requests).toEqual([ + { method: 'settings.update', params: { defaultTaskSource: 'github' } } + ]) + }) + + it('rejects when the desktop refuses the request', async () => { + const host = { + request: vi.fn(async () => { + throw new Error('forbidden') + }) + } as unknown as MobileWebHostRequestClient + await expect( + nativeHostTaskReadOperations(webHostTaskRpcSender(host)).listRepositories() + ).rejects.toThrow('forbidden') + }) + + it('reassembles a project table from every row window the desktop returns', async () => { + const rows = [projectRow(0), projectRow(1), projectRow(2)] + const f = hostFixture((request) => { + const offset = (request.params.rowOffset as number) ?? 0 + const window = rows.slice(offset, offset + 2) + return { + data: projectTable(window, rows.length), + ...(offset + window.length < rows.length ? { nextRowOffset: offset + window.length } : {}) + } + }) + const table = await webHostTaskProjectReadOperations(f.sender).loadTable(projectRequest) + expect(table.rows.map((row) => row.id)).toEqual(['row-0', 'row-1', 'row-2']) + expect(f.requests.map((request) => request.params.rowOffset)).toEqual([0, 2]) + expect(f.requests.every((request) => request.method === 'mobileWeb.tasks.projectTable')).toBe( + true + ) + }) + + it('stops paging a project table when a window adds no rows', async () => { + const f = hostFixture(() => ({ data: projectTable([], 0), nextRowOffset: 0 })) + const table = await webHostTaskProjectReadOperations(f.sender).loadTable(projectRequest) + expect(table.rows).toEqual([]) + expect(f.requests).toHaveLength(1) + }) + + it('rejects a project table the page schema does not accept', async () => { + const f = hostFixture(() => ({ data: { rows: [{ id: 7 }] } })) + await expect( + webHostTaskProjectReadOperations(f.sender).loadTable(projectRequest) + ).rejects.toThrow() + }) +}) diff --git a/mobile/src/tasks/web-host-task-preference-operations.test.ts b/mobile/src/tasks/web-host-task-preference-operations.test.ts deleted file mode 100644 index 57970a51474..00000000000 --- a/mobile/src/tasks/web-host-task-preference-operations.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import { webHostTaskPreferenceOperations } from './web-host-task-preference-operations' - -describe('web host task preference operations', () => { - it('uses strict task updates and the existing opaque trust operation', async () => { - const updateResume = vi.fn().mockResolvedValue(null) - const updateSettings = vi.fn().mockResolvedValue(null) - const persistTrust = vi.fn().mockResolvedValue({ - 'repo-page-1': { all: { approvedAt: 10 } } - }) - const operations = webHostTaskPreferenceOperations({ - task: { updateResume, updateSettings }, - workspaceCreation: { persistTrust } - } as unknown as MobileWebBridgeClient) - - await operations.updateResume({ githubMode: 'project' }) - await operations.updateSettings({ defaultTaskSource: 'linear' }) - await expect( - operations.persistSetupTrust({ - trust: {}, - repoId: 'repo-page-1', - contentHash: 'f'.repeat(64), - alwaysTrust: true - }) - ).resolves.toEqual({ - 'repo-page-1': { all: { approvedAt: 10 } } - }) - - expect(updateResume).toHaveBeenCalledWith({ - taskResumeState: { githubMode: 'project' } - }) - expect(updateSettings).toHaveBeenCalledWith({ defaultTaskSource: 'linear' }) - expect(persistTrust).toHaveBeenCalledWith({ - trust: {}, - repoId: 'repo-page-1', - contentHash: 'f'.repeat(64), - alwaysTrust: true - }) - }) -}) diff --git a/mobile/src/tasks/web-host-task-preference-operations.ts b/mobile/src/tasks/web-host-task-preference-operations.ts deleted file mode 100644 index eec5a20cd47..00000000000 --- a/mobile/src/tasks/web-host-task-preference-operations.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { HostTaskPreferenceOperations } from './host-task-preference-operations' - -export function webHostTaskPreferenceOperations( - client: MobileWebBridgeClient -): HostTaskPreferenceOperations { - return { - async updateResume(taskResumeState) { - await client.task.updateResume({ taskResumeState }) - }, - async updateSettings(settings) { - await client.task.updateSettings(settings) - }, - persistSetupTrust: (args) => client.workspaceCreation.persistTrust(args) - } -} diff --git a/mobile/src/tasks/web-host-task-project-file-operations.ts b/mobile/src/tasks/web-host-task-project-file-operations.ts deleted file mode 100644 index 0cfc11ba91a..00000000000 --- a/mobile/src/tasks/web-host-task-project-file-operations.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { HostTaskProjectFileOperations } from './host-task-project-file-operations' -import type { HostTaskProjectItemTarget } from './host-task-project-mutation-operations' - -export function webHostTaskProjectFileOperations( - client: MobileWebBridgeClient -): HostTaskProjectFileOperations { - return { - async refreshChecks(target, repoId, headSha) { - return ( - await client.task.refreshProjectChecks({ - targetId: targetId(target), - repoId, - ...(headSha ? { headSha } : {}) - }) - ).checks - }, - async setFileViewed(target, repoId, payload) { - await client.task.setProjectFileViewed({ targetId: targetId(target), repoId, ...payload }) - }, - loadFileContents: (target, repoId, payload) => - client.task.loadProjectFileContents({ targetId: targetId(target), repoId, ...payload }), - async addInlineComment(target, repoId, payload) { - return ( - await client.task.addProjectInlineComment({ - targetId: targetId(target), - repoId, - ...payload - }) - ).comment - } - } -} - -function targetId(target: HostTaskProjectItemTarget): string { - if (!target.targetId) { - throw new Error('Project mutation authority is unavailable') - } - return target.targetId -} diff --git a/mobile/src/tasks/web-host-task-project-mutation-operations.ts b/mobile/src/tasks/web-host-task-project-mutation-operations.ts deleted file mode 100644 index eee6a189359..00000000000 --- a/mobile/src/tasks/web-host-task-project-mutation-operations.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { - HostTaskProjectItemTarget, - HostTaskProjectMutationOperations -} from './host-task-project-mutation-operations' - -export function webHostTaskProjectMutationOperations( - client: MobileWebBridgeClient -): HostTaskProjectMutationOperations { - return { - async updateItem(target, updates) { - await client.task.updateProjectItem({ targetId: targetId(target), updates }) - }, - async addComment(target, body) { - return (await client.task.addProjectComment({ targetId: targetId(target), body })).comment - }, - async updateComment(target, commentId, body) { - await client.task.updateProjectComment({ - targetId: targetId(target), - commentId, - body - }) - }, - async deleteComment(target, commentId) { - await client.task.deleteProjectComment({ targetId: targetId(target), commentId }) - }, - async updateMetadata(target, updates) { - await client.task.updateProjectMetadata({ targetId: targetId(target), updates }) - }, - async updateField(target, fieldId, value) { - await client.task.updateProjectField({ targetId: targetId(target), fieldId, value }) - }, - async updateIssueType(target, issueTypeId) { - await client.task.updateProjectIssueType({ - targetId: targetId(target), - issueTypeId - }) - }, - async resolveReviewThread(target, repoId, threadId, resolve) { - await client.task.resolveProjectReviewThread({ - targetId: targetId(target), - repoId, - threadId, - resolve - }) - }, - // The hosted bridge publishes no comment on these; callers fall back to a local entry. - async replyReviewComment(target, repoId, payload) { - await client.task.replyProjectReviewComment({ - targetId: targetId(target), - repoId, - ...payload - }) - return undefined - }, - async addConversationComment(target, repoId, body) { - await client.task.addProjectConversationComment({ - targetId: targetId(target), - repoId, - body - }) - return undefined - }, - async requestReviewers(target, repoId, reviewers) { - await client.task.requestProjectReviewers({ - targetId: targetId(target), - repoId, - reviewers - }) - }, - async rerunChecks(target, repoId, payload) { - await client.task.rerunProjectChecks({ - targetId: targetId(target), - repoId, - ...payload - }) - }, - async merge(target, repoId, method) { - await client.task.mergeProjectPullRequest({ - targetId: targetId(target), - repoId, - method - }) - } - } -} - -function targetId(target: HostTaskProjectItemTarget): string { - if (!target.targetId) { - throw new Error('Project mutation authority is unavailable') - } - return target.targetId -} diff --git a/mobile/src/tasks/web-host-task-project-read-operations.ts b/mobile/src/tasks/web-host-task-project-read-operations.ts index 7da3329fdee..8f7cb627d62 100644 --- a/mobile/src/tasks/web-host-task-project-read-operations.ts +++ b/mobile/src/tasks/web-host-task-project-read-operations.ts @@ -1,45 +1,58 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import { MobileWebTaskProjectTableSchema } from '../../../src/shared/mobile-web/task-project-table-contract' +import { + MobileWebTaskProjectTableSchema, + type MobileWebTaskProjectTable, + type MobileWebTaskProjectTablePayload +} from '../../../src/shared/mobile-web/task-project-table-contract' import type { HostTaskProjectReadOperations } from './host-task-project-read-operations' +import { + nativeHostTaskProjectReadOperations, + projectResult +} from './native-host-task-project-read-operations' +import type { RpcRequestSender } from '../transport/rpc-client' +type ProjectTableWindow = { + data: MobileWebTaskProjectTable + nextRowOffset?: number +} + +/** Same reads as the native app, except the table arrives one row window at a time because the + * whole table can exceed the bridge envelope. */ export function webHostTaskProjectReadOperations( - client: MobileWebBridgeClient + client: RpcRequestSender ): HostTaskProjectReadOperations { return { - listAccessible: (host) => client.task.listProjects({ host }), - async listViews(project) { - return (await client.task.listProjectViews(project)).views - }, - resolveRef: (payload) => client.task.resolveProjectRef(payload), - async loadTable(payload) { - const first = await client.task.projectTablePage(payload) - if (!first.project || !first.selectedView || first.totalCount === undefined) { - throw new Error('Project table metadata is unavailable') - } - const rows = [...first.rows] - let cursor = first.nextCursor - while (cursor) { - const next = await client.task.projectTablePage({ ...payload, cursor }) - rows.push(...next.rows) - cursor = next.nextCursor - } - return MobileWebTaskProjectTableSchema.parse({ - project: first.project, - selectedView: first.selectedView, - totalCount: first.totalCount, - parentFieldDropped: first.parentFieldDropped, - rows - }) - }, - loadItemDetail: (payload) => client.task.loadProjectItemDetail(payload), - async listItemLabels(payload) { - return (await client.task.listProjectItemLabels(payload)).labels - }, - async listItemAssignableUsers(payload) { - return (await client.task.listProjectItemAssignableUsers(payload)).users - }, - async listIssueTypes(payload) { - return (await client.task.listProjectIssueTypes(payload)).types - } + ...nativeHostTaskProjectReadOperations(client), + loadTable: (payload) => loadWindowedTable(client, payload) } } + +async function loadWindowedTable( + client: RpcRequestSender, + payload: Omit +): Promise { + const rows: MobileWebTaskProjectTable['rows'] = [] + let table: MobileWebTaskProjectTable | null = null + let rowOffset: number | undefined = 0 + while (rowOffset !== undefined) { + const page: ProjectTableWindow = await projectResult( + client.sendRequest( + 'mobileWeb.tasks.projectTable', + { + owner: payload.owner, + host: payload.host, + ownerType: payload.ownerType, + projectNumber: payload.number, + viewId: payload.viewId, + queryOverride: payload.queryOverride, + rowOffset + }, + { timeoutMs: 60_000 } + ) + ) + table = page.data + rows.push(...page.data.rows) + // A window that added nothing cannot be followed by one that does, so stop rather than spin. + rowOffset = page.data.rows.length > 0 ? page.nextRowOffset : undefined + } + return MobileWebTaskProjectTableSchema.parse({ ...table, rows }) +} diff --git a/mobile/src/tasks/web-host-task-provider-write-operations.ts b/mobile/src/tasks/web-host-task-provider-write-operations.ts deleted file mode 100644 index 33cf190e7f7..00000000000 --- a/mobile/src/tasks/web-host-task-provider-write-operations.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { HostTaskProviderWriteOperations } from './host-task-provider-write-operations' - -export function webHostTaskProviderWriteOperations( - client: MobileWebBridgeClient -): HostTaskProviderWriteOperations { - return { - createIssue: (payload) => client.task.createProviderIssue(payload), - async updateIssueSource(repoId, preference) { - await client.task.updateIssueSource({ repoId, preference }) - } - } -} diff --git a/mobile/src/tasks/web-host-task-read-operations.ts b/mobile/src/tasks/web-host-task-read-operations.ts deleted file mode 100644 index 6bcb64c6734..00000000000 --- a/mobile/src/tasks/web-host-task-read-operations.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { MobileWebBridgeClient } from '../../../src/mobile-web/src/mobile-web-bridge-client' -import type { TuiAgent } from '../../../src/shared/tui-agent' -import { normalizeWorkspaceAgent } from './workspace-agent-selection' -import type { HostTaskBootstrap, HostTaskReadOperations } from './host-task-read-operations' - -export function webHostTaskReadOperations(client: MobileWebBridgeClient): HostTaskReadOperations { - return { - async bootstrap() { - return webBootstrap(await client.task.bootstrap()) - }, - async listRepositories() { - return (await client.task.repositories()).repositories - }, - loadLinearContext: () => client.task.linearContext(), - async resolveGitHubRepoSlug(repoId) { - return (await client.task.resolveRepoSlug({ repoId })).repository - } - } -} - -function webBootstrap( - bootstrap: Awaited> -): HostTaskBootstrap { - const defaultTuiAgent = normalizeWorkspaceAgent(bootstrap.settings.defaultTuiAgent) - return { - ...bootstrap, - settings: { - ...bootstrap.settings, - defaultTuiAgent, - disabledTuiAgents: bootstrap.settings.disabledTuiAgents?.flatMap((agent) => { - const normalized = normalizeWorkspaceAgent(agent) - return normalized && normalized !== 'blank' ? [normalized as TuiAgent] : [] - }) - } - } -}