mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 16:02:41 +00:00
fix(lint): enable anti-slop/no-pass-through-type-alias
`anti-slop/no-pass-through-type-alias` rejects a type alias whose entire right-hand side is a bare reference to another named type, including the generic form where every type parameter is forwarded positionally and unchanged (`type A<T> = B<T>`). Those aliases add a second name for one type: readers have to resolve the indirection, "go to definition" lands on a rename rather than the shape, and the two names drift apart in review. Flipped the rule from "off" to "error" in `config/oxlint-anti-slop.json` and fixed all 195 violations reported across `src`, `config`, `tests`, and `mobile`. Fix approach, in order of preference per site: - Delete the alias and use the target type directly at every reference, updating imports. This covers the large majority of the 195. - Where the alias name was the better or more widely used name, rename the target declaration to the alias name instead of renaming call sites (for example `GitUncommittedEntry` -> `GitStatusEntry` in `src/shared/git-status-types.ts`). - Where a pass-through sat in front of a type that was itself only used through that alias, collapse the pair into a single declaration that keeps the real shape (intersection, `Pick`/`Omit`, or union) under one name. No alias was converted into an equivalent `interface X extends Y` to dodge the rule, and no new pass-through was introduced. No suppressions were added. The vendored anti-slop plugin source under `config/oxlint-plugins/anti-slop/` is excluded from the audit by the `--ignore-pattern` flag in `audit:anti-slop`, and stays byte-identical to upstream. Verified: - `npx oxlint --config config/oxlint-anti-slop.json --ignore-pattern 'config/oxlint-plugins/anti-slop/**' src config tests mobile` exits 0 (195 -> 0; baseline counted on a scratch worktree of `nwparker/2x-lint` with the rule flipped on). - `node config/scripts/run-typecheck-projects-in-parallel.mjs` exits 0. - `cd mobile && pnpm typecheck` exits 0 (the parallel script covers only the three desktop tsconfigs). - Root vitest over the changed files and their sibling tests: 210 files, 4072 passed, 1 skipped. - Mobile vitest over the changed files and their sibling tests: 36 files, 445 passed. - `npx oxlint` with the repo's default config over every changed file (root and mobile) exits 0. - `npx oxfmt --write` run over the changed files in both workspaces.
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
"anti-slop/no-known-value-widening": "off",
|
||||
"anti-slop/no-module-mocking": "off",
|
||||
"anti-slop/no-object-parameters": "off",
|
||||
"anti-slop/no-pass-through-type-alias": "off",
|
||||
"anti-slop/no-pass-through-type-alias": "error",
|
||||
"anti-slop/no-reduce-accumulator-copy": "off",
|
||||
"anti-slop/no-reflect-apply": "off",
|
||||
"anti-slop/no-reflect-get": "off",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MobileGitStatusEntry } from '../src/source-control/mobile-git-status'
|
||||
import type { GitStatusEntry } from '../../src/shared/git-status-types'
|
||||
|
||||
type FakeGitEntry = MobileGitStatusEntry & {
|
||||
type FakeGitEntry = GitStatusEntry & {
|
||||
stagedFromUntracked?: boolean
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ let fakeAhead = 1
|
||||
let fakeBehind = 0
|
||||
let fakeHasUpstream = true
|
||||
|
||||
function toGitStatusEntry(entry: FakeGitEntry): MobileGitStatusEntry {
|
||||
function toGitStatusEntry(entry: FakeGitEntry): GitStatusEntry {
|
||||
const { stagedFromUntracked: _stagedFromUntracked, ...statusEntry } = entry
|
||||
return statusEntry
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RuntimeSpeechSetupState } from '../../../src/shared/runtime-types'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ActivityIndicator, Pressable, StyleSheet, Switch, Text, View } from 'react-native'
|
||||
import { Check, Download } from 'lucide-react-native'
|
||||
@@ -11,8 +12,7 @@ import {
|
||||
fetchDictationSetup,
|
||||
isModelInFlight,
|
||||
setDictationConfig,
|
||||
type MobileSpeechModel,
|
||||
type MobileSpeechSetup
|
||||
type MobileSpeechModel
|
||||
} from '../dictation/mobile-dictation-setup'
|
||||
|
||||
const POLL_INTERVAL_MS = 1500
|
||||
@@ -35,7 +35,7 @@ function formatSize(bytes: number | null): string {
|
||||
// Lets the user enable dictation and download a speech model on the paired
|
||||
// desktop, from the phone. Polls while a download is in flight.
|
||||
export function MobileDictationSetupSheet({ visible, client, onClose, onReady }: Props) {
|
||||
const [setup, setSetup] = useState<MobileSpeechSetup | null>(null)
|
||||
const [setup, setSetup] = useState<RuntimeSpeechSetupState | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState<string | null>(null)
|
||||
const refresh = useCallback(async (): Promise<boolean | undefined> => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-native'
|
||||
import { RotateCw } from 'lucide-react-native'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
@@ -18,7 +19,6 @@ import { usePRBotAuthorOverrides } from '../session/use-pr-bot-author-overrides'
|
||||
import { buildFixChecksPrompt, buildResolveConflictsPrompt } from '../session/pr-ai-triage-prompt'
|
||||
import { prSidebarRenderBranch } from './mobile-pr-sidebar-presentation'
|
||||
import { mobilePrSidebarStyles as styles } from './pr-sidebar/mobile-pr-sidebar-styles'
|
||||
import type { MobileGitStatusResult } from '../source-control/mobile-git-status'
|
||||
import { PRSidebarHeader } from './pr-sidebar/PRSidebarHeader'
|
||||
import { PRConflictingFilesSection } from './pr-sidebar/PRConflictingFilesSection'
|
||||
import { PRActionsSection } from './pr-sidebar/PRActionsSection'
|
||||
@@ -37,7 +37,7 @@ type Props = {
|
||||
connState: ConnectionState
|
||||
worktreeId: string
|
||||
gitBranch: string | null
|
||||
gitStatus: MobileGitStatusResult | null
|
||||
gitStatus: GitStatusResult | null
|
||||
headSha: string | null
|
||||
bottomInset?: number
|
||||
// Hub chrome already shows open-on-web; hide the in-body icon there.
|
||||
@@ -154,7 +154,7 @@ function PrSidebarContent({
|
||||
connState: ConnectionState
|
||||
worktreeId: string
|
||||
gitBranch: string | null
|
||||
gitStatus: MobileGitStatusResult | null
|
||||
gitStatus: GitStatusResult | null
|
||||
actions: MobilePrActions
|
||||
commentActions: MobilePrCommentActions
|
||||
titleAction: MobilePrTitleAction
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Pressable, Switch, Text, View } from 'react-native'
|
||||
import type { WorkspaceCreateSetupDecision } from '../tasks/workspace-create-params'
|
||||
import type { SetupDecision } from '../../../src/shared/worktree/create-types'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import { newWorktreeFormStyles as styles } from './new-worktree-form-styles'
|
||||
import type { SetupRunPolicy } from './new-worktree-modal-types'
|
||||
@@ -16,9 +16,9 @@ export function NewWorkspaceSetupScriptField({
|
||||
command: string
|
||||
source: string | null
|
||||
runPolicy: SetupRunPolicy
|
||||
decision: Exclude<WorkspaceCreateSetupDecision, 'inherit'> | null
|
||||
decision: Exclude<SetupDecision, 'inherit'> | null
|
||||
runSetup: boolean
|
||||
onDecisionChange: (decision: Exclude<WorkspaceCreateSetupDecision, 'inherit'>) => void
|
||||
onDecisionChange: (decision: Exclude<SetupDecision, 'inherit'>) => void
|
||||
onRunSetupChange: (run: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ActivityIndicator, Pressable, Text, TextInput, View } from 'react-native'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react-native'
|
||||
import type { WorkspaceCreateSetupDecision } from '../tasks/workspace-create-params'
|
||||
import type { SetupDecision } from '../../../src/shared/worktree/create-types'
|
||||
import type { WorkspaceSshGate } from '../tasks/workspace-ssh-gate'
|
||||
import type { useMobileComposerSource } from '../tasks/use-mobile-composer-source'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
@@ -37,7 +37,7 @@ export function NewWorktreeFormSheet(props: {
|
||||
setupCommand: string | null
|
||||
setupSource: string | null
|
||||
setupRunPolicy: SetupRunPolicy
|
||||
setupDecisionChoice: Exclude<WorkspaceCreateSetupDecision, 'inherit'> | null
|
||||
setupDecisionChoice: Exclude<SetupDecision, 'inherit'> | null
|
||||
runSetup: boolean
|
||||
error: string
|
||||
creating: boolean
|
||||
@@ -52,7 +52,7 @@ export function NewWorktreeFormSheet(props: {
|
||||
onOpenAgent: () => void
|
||||
onShowAdvancedChange: (show: boolean) => void
|
||||
onNoteChange: (note: string) => void
|
||||
onSetupDecisionChange: (decision: Exclude<WorkspaceCreateSetupDecision, 'inherit'>) => void
|
||||
onSetupDecisionChange: (decision: Exclude<SetupDecision, 'inherit'>) => void
|
||||
onRunSetupChange: (run: boolean) => void
|
||||
onCreate: () => void
|
||||
}) {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import type { RuntimeSpeechSetupState } from '../../../src/shared/runtime-types'
|
||||
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { Check, Download, Trash2 } from 'lucide-react-native'
|
||||
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
import {
|
||||
isModelInFlight,
|
||||
type MobileSpeechModel,
|
||||
type MobileSpeechSetup
|
||||
} from '../dictation/mobile-dictation-setup'
|
||||
import { isModelInFlight, type MobileSpeechModel } from '../dictation/mobile-dictation-setup'
|
||||
|
||||
type Props = {
|
||||
setup: MobileSpeechSetup
|
||||
setup: RuntimeSpeechSetupState
|
||||
// Disabled mirrors desktop: the model list greys out when dictation is off.
|
||||
disabled: boolean
|
||||
busyAction: { modelId: string; type: 'download' | 'select' | 'delete' } | null
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GitStatusResult } from '../../../../src/shared/git-status-types'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { colors } from '../../theme/mobile-theme'
|
||||
import type { ConnectionState } from '../../transport/types'
|
||||
import type { RpcClient } from '../../transport/rpc-client'
|
||||
import type { MobileGitStatusResult } from '../../source-control/mobile-git-status'
|
||||
import type { MobilePrSidebarController } from '../../session/use-mobile-pr-sidebar-controller'
|
||||
import { MobilePRSidebar } from '../MobilePRSidebar'
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
worktreeId: string
|
||||
branch: string | null
|
||||
headSha: string | null
|
||||
gitStatus: MobileGitStatusResult | null
|
||||
gitStatus: GitStatusResult | null
|
||||
isGithubRepo?: boolean
|
||||
branchContextLoaded?: boolean
|
||||
controller: MobilePrSidebarController
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { GitStatusResult } from '../../../../src/shared/git-status-types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ActivityIndicator, Pressable, Text, View } from 'react-native'
|
||||
import { GitPullRequestArrow, Link2, RefreshCw } from 'lucide-react-native'
|
||||
import { colors } from '../../theme/mobile-theme'
|
||||
import type { RpcClient } from '../../transport/rpc-client'
|
||||
import type { ConnectionState } from '../../transport/types'
|
||||
import type { MobileGitStatusResult } from '../../source-control/mobile-git-status'
|
||||
import {
|
||||
getMobileCommitFailureStagedEntries,
|
||||
type MobileCommitFailureRecovery
|
||||
@@ -26,7 +26,7 @@ type Props = {
|
||||
client: RpcClient | null
|
||||
worktreeId: string
|
||||
gitBranch: string | null
|
||||
gitStatus: MobileGitStatusResult | null
|
||||
gitStatus: GitStatusResult | null
|
||||
connState: ConnectionState
|
||||
// Refetches the sidebar after create or an explicit empty-state refresh.
|
||||
onCreated: () => void
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '../tasks/setup-hook-trust'
|
||||
import { createWorkspaceFromComposerSource } from '../tasks/source-workspace-create'
|
||||
import { normalizeWorkspaceAgent } from '../tasks/workspace-agent-selection'
|
||||
import type { WorkspaceCreateSetupDecision } from '../tasks/workspace-create-params'
|
||||
import type { SetupDecision } from '../../../src/shared/worktree/create-types'
|
||||
import type { WorkspaceSshGate } from '../tasks/workspace-ssh-gate'
|
||||
import type { useMobileComposerSource } from '../tasks/use-mobile-composer-source'
|
||||
import type { WorktreeCreateIdempotencySupport } from '../tasks/worktree-create-idempotency-policy'
|
||||
@@ -28,7 +28,7 @@ import type { NewWorktreeDrawerView } from './use-new-worktree-drawer-navigation
|
||||
import { getSuggestedCreatureName } from './worktree-name-suggestion'
|
||||
|
||||
type CreateOptions = {
|
||||
setupOverride?: Exclude<WorkspaceCreateSetupDecision, 'inherit'>
|
||||
setupOverride?: Exclude<SetupDecision, 'inherit'>
|
||||
approvedSetupContentHash?: string
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function useNewWorkspaceCreateSubmit(args: {
|
||||
setupCommand: string | null
|
||||
setupTrust: SetupHookTrust | null
|
||||
setupRunPolicy: SetupRunPolicy
|
||||
setupDecisionChoice: Exclude<WorkspaceCreateSetupDecision, 'inherit'> | null
|
||||
setupDecisionChoice: Exclude<SetupDecision, 'inherit'> | null
|
||||
runSetup: boolean
|
||||
trustedOrcaHooks: PersistedTrustedOrcaHooks
|
||||
setTrustedOrcaHooks: (trust: PersistedTrustedOrcaHooks) => void
|
||||
@@ -118,7 +118,7 @@ export function useNewWorkspaceCreateSubmit(args: {
|
||||
undefined,
|
||||
args.retiredWorktreeNames
|
||||
)
|
||||
let setupDecision: WorkspaceCreateSetupDecision = 'inherit'
|
||||
let setupDecision: SetupDecision = 'inherit'
|
||||
if (args.setupCommand) {
|
||||
if (options.setupOverride) {
|
||||
setupDecision = options.setupOverride
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { RpcSuccess } from '../transport/types'
|
||||
import { normalizeSetupHookTrust } from '../tasks/setup-hook-trust'
|
||||
import type { WorkspaceCreateSetupDecision } from '../tasks/workspace-create-params'
|
||||
import type { SetupDecision } from '../../../src/shared/worktree/create-types'
|
||||
import type {
|
||||
MobileWorkspaceRepo,
|
||||
RepoHooksResponse,
|
||||
@@ -17,8 +17,8 @@ export function useNewWorkspaceSetupScript(args: {
|
||||
setupSource: string | null
|
||||
setupTrust: SetupHookDetails['trust']
|
||||
setupRunPolicy: SetupHookDetails['runPolicy']
|
||||
setupDecisionChoice: Exclude<WorkspaceCreateSetupDecision, 'inherit'> | null
|
||||
setSetupDecisionChoice: (decision: Exclude<WorkspaceCreateSetupDecision, 'inherit'>) => void
|
||||
setupDecisionChoice: Exclude<SetupDecision, 'inherit'> | null
|
||||
setSetupDecisionChoice: (decision: Exclude<SetupDecision, 'inherit'>) => void
|
||||
runSetup: boolean
|
||||
setRunSetup: (run: boolean) => void
|
||||
showAdvanced: boolean
|
||||
@@ -27,7 +27,7 @@ export function useNewWorkspaceSetupScript(args: {
|
||||
const { client, selectedRepo } = args
|
||||
const [details, setDetails] = useState<SetupHookDetails | null>(null)
|
||||
const [setupDecisionChoice, setSetupDecisionChoice] = useState<Exclude<
|
||||
WorkspaceCreateSetupDecision,
|
||||
SetupDecision,
|
||||
'inherit'
|
||||
> | null>(null)
|
||||
const [runSetup, setRunSetup] = useState(true)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RuntimeSpeechSetupState } from '../../../src/shared/runtime-types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
@@ -10,8 +11,7 @@ import {
|
||||
isDictationSetupRequiredError,
|
||||
isModelInFlight,
|
||||
setDictationConfig,
|
||||
type MobileSpeechModel,
|
||||
type MobileSpeechSetup
|
||||
type MobileSpeechModel
|
||||
} from './mobile-dictation-setup'
|
||||
|
||||
function ok(result: unknown): RpcSuccess {
|
||||
@@ -62,14 +62,14 @@ describe('isDictationSetupRequiredError', () => {
|
||||
|
||||
describe('rpc wrappers', () => {
|
||||
it('fetches setup', async () => {
|
||||
const setup: MobileSpeechSetup = { enabled: false, selectedModelId: '', models: [] }
|
||||
const setup: RuntimeSpeechSetupState = { enabled: false, selectedModelId: '', models: [] }
|
||||
const client = clientWith([ok(setup)])
|
||||
await expect(fetchDictationSetup(client)).resolves.toEqual(setup)
|
||||
expect(client.calls[0]).toEqual({ method: 'speech.models.list', params: null })
|
||||
})
|
||||
|
||||
it('retries the idempotent setup read once after logical-client cutover', async () => {
|
||||
const setup: MobileSpeechSetup = { enabled: false, selectedModelId: '', models: [] }
|
||||
const setup: RuntimeSpeechSetupState = { enabled: false, selectedModelId: '', models: [] }
|
||||
const sendRequest = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new LogicalClientCutoverError())
|
||||
@@ -95,14 +95,14 @@ describe('rpc wrappers', () => {
|
||||
})
|
||||
|
||||
it('deletes a model and returns refreshed setup', async () => {
|
||||
const setup: MobileSpeechSetup = { enabled: true, selectedModelId: '', models: [] }
|
||||
const setup: RuntimeSpeechSetupState = { enabled: true, selectedModelId: '', models: [] }
|
||||
const client = clientWith([ok(setup)])
|
||||
await expect(deleteDictationModel(client, 'm1')).resolves.toEqual(setup)
|
||||
expect(client.calls[0]).toEqual({ method: 'speech.models.delete', params: { modelId: 'm1' } })
|
||||
})
|
||||
|
||||
it('sets config', async () => {
|
||||
const setup: MobileSpeechSetup = { enabled: true, selectedModelId: 'm1', models: [] }
|
||||
const setup: RuntimeSpeechSetupState = { enabled: true, selectedModelId: 'm1', models: [] }
|
||||
const client = clientWith([ok(setup)])
|
||||
await expect(setDictationConfig(client, { enabled: true, modelId: 'm1' })).resolves.toEqual(
|
||||
setup
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { RpcClient } from '../transport/rpc-client'
|
||||
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
|
||||
import type { RpcSuccess } from '../transport/types'
|
||||
|
||||
export type MobileSpeechSetup = RuntimeSpeechSetupState
|
||||
export type MobileSpeechModel = RuntimeSpeechSetupState['models'][number]
|
||||
|
||||
// Dictation-setup errors startMobileDictation throws when the desktop isn't
|
||||
@@ -31,7 +30,7 @@ export function isDictationSetupRequiredError(message: string): boolean {
|
||||
|
||||
export async function fetchDictationSetup(
|
||||
client: Pick<RpcClient, 'sendRequest'>
|
||||
): Promise<MobileSpeechSetup> {
|
||||
): Promise<RuntimeSpeechSetupState> {
|
||||
const response = await fetchDictationSetupResponse(client)
|
||||
if (!response.ok) {
|
||||
if (isLegacyDesktopSpeechSetupError(response.error)) {
|
||||
@@ -39,7 +38,7 @@ export async function fetchDictationSetup(
|
||||
}
|
||||
throw new Error(response.error?.message || 'Failed to load dictation models')
|
||||
}
|
||||
return (response as RpcSuccess).result as MobileSpeechSetup
|
||||
return (response as RpcSuccess).result as RuntimeSpeechSetupState
|
||||
}
|
||||
|
||||
async function fetchDictationSetupResponse(client: Pick<RpcClient, 'sendRequest'>) {
|
||||
@@ -68,23 +67,23 @@ export async function downloadDictationModel(
|
||||
export async function deleteDictationModel(
|
||||
client: Pick<RpcClient, 'sendRequest'>,
|
||||
modelId: string
|
||||
): Promise<MobileSpeechSetup> {
|
||||
): Promise<RuntimeSpeechSetupState> {
|
||||
const response = await client.sendRequest('speech.models.delete', { modelId })
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error?.message || 'Failed to delete model')
|
||||
}
|
||||
return (response as RpcSuccess).result as MobileSpeechSetup
|
||||
return (response as RpcSuccess).result as RuntimeSpeechSetupState
|
||||
}
|
||||
|
||||
export async function setDictationConfig(
|
||||
client: Pick<RpcClient, 'sendRequest'>,
|
||||
params: { enabled?: boolean; modelId?: string; dictationMode?: 'toggle' | 'hold' }
|
||||
): Promise<MobileSpeechSetup> {
|
||||
): Promise<RuntimeSpeechSetupState> {
|
||||
const response = await client.sendRequest('speech.dictation.setup', params)
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error?.message || 'Failed to update dictation settings')
|
||||
}
|
||||
return (response as RpcSuccess).result as MobileSpeechSetup
|
||||
return (response as RpcSuccess).result as RuntimeSpeechSetupState
|
||||
}
|
||||
|
||||
// A model is mid-download (or extracting) and the sheet should keep polling.
|
||||
@@ -93,7 +92,7 @@ export function isModelInFlight(model: MobileSpeechModel): boolean {
|
||||
}
|
||||
|
||||
// Whether dictation can be used right now: enabled + a selected model that's ready.
|
||||
export function isDictationReady(setup: MobileSpeechSetup): boolean {
|
||||
export function isDictationReady(setup: RuntimeSpeechSetupState): boolean {
|
||||
if (!setup.enabled || !setup.selectedModelId) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ export type MobileFilePreviewRequest = {
|
||||
}
|
||||
|
||||
type MobileFilePreviewClient = Pick<RpcClient, 'sendRequest'>
|
||||
type TerminalArtifactSource = MobileTerminalArtifactPreviewSource
|
||||
type TerminalArtifactSaveOptions = TerminalArtifactRetryOptions & {
|
||||
baseContent?: string
|
||||
}
|
||||
@@ -112,7 +111,7 @@ export async function loadMobileFilePreview(
|
||||
|
||||
export async function saveMobileTerminalArtifactPreview(
|
||||
client: MobileFilePreviewClient,
|
||||
source: TerminalArtifactSource,
|
||||
source: MobileTerminalArtifactPreviewSource,
|
||||
content: string,
|
||||
options: TerminalArtifactSaveOptions = {}
|
||||
): Promise<MobileFilePreviewResult | { status: 'saved' }> {
|
||||
@@ -175,11 +174,11 @@ export async function saveMobileTerminalArtifactPreview(
|
||||
|
||||
async function verifyTerminalArtifactBaseContent(
|
||||
client: MobileFilePreviewClient,
|
||||
source: TerminalArtifactSource,
|
||||
source: MobileTerminalArtifactPreviewSource,
|
||||
baseContent: string,
|
||||
options: TerminalArtifactRetryOptions
|
||||
): Promise<
|
||||
| { status: 'ok'; source: TerminalArtifactSource; refreshed: boolean }
|
||||
| { status: 'ok'; source: MobileTerminalArtifactPreviewSource; refreshed: boolean }
|
||||
| { status: 'error'; error: MobileFilePreviewResult }
|
||||
> {
|
||||
let readSource = source
|
||||
@@ -233,7 +232,7 @@ async function verifyTerminalArtifactBaseContent(
|
||||
|
||||
function writeTerminalArtifactPreview(
|
||||
client: MobileFilePreviewClient,
|
||||
source: TerminalArtifactSource,
|
||||
source: MobileTerminalArtifactPreviewSource,
|
||||
content: string
|
||||
): Promise<RpcResponse> {
|
||||
return client.sendRequest('files.writeTerminalArtifact', {
|
||||
|
||||
@@ -4,9 +4,7 @@ import {
|
||||
type ResponsiveLayoutMetrics
|
||||
} from './responsive-layout-metrics'
|
||||
|
||||
export type ResponsiveLayout = ResponsiveLayoutMetrics
|
||||
|
||||
export function useResponsiveLayout(): ResponsiveLayout {
|
||||
export function useResponsiveLayout(): ResponsiveLayoutMetrics {
|
||||
const { width, height } = useWindowDimensions()
|
||||
return getResponsiveLayoutMetrics(width, height)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { TerminalWebView } from '../terminal/TerminalWebView'
|
||||
import type { RuntimeMobileTerminalTheme } from '../../../src/shared/runtime-types'
|
||||
import type {
|
||||
MobileTerminalTheme,
|
||||
TerminalKeyboardAvoidanceMetrics,
|
||||
TerminalModes,
|
||||
TerminalWebViewHandle
|
||||
@@ -12,7 +12,7 @@ type TerminalPaneViewProps = {
|
||||
handle: string
|
||||
active: boolean
|
||||
keyboardLift: number
|
||||
terminalTheme?: MobileTerminalTheme
|
||||
terminalTheme?: RuntimeMobileTerminalTheme
|
||||
textScale: number
|
||||
onRef: (handle: string, ref: TerminalWebViewHandle | null) => void
|
||||
onWebReady: (handle: string) => void
|
||||
|
||||
@@ -13,10 +13,8 @@ import {
|
||||
readMobileReviewGitDiffResult,
|
||||
readMobileReviewWorktreeMetadata
|
||||
} from './mobile-diff-review-rpc'
|
||||
import {
|
||||
canOpenMobileBranchCompareDiff,
|
||||
type MobileGitBranchCompareResult
|
||||
} from '../source-control/mobile-branch-compare'
|
||||
import { canOpenMobileBranchCompareDiff } from '../source-control/mobile-branch-compare'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import { resolveMobileBranchCompareBaseRef } from '../source-control/mobile-branch-base-ref'
|
||||
import { isMobileGitUnavailable } from '../source-control/mobile-git-status'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
@@ -25,7 +23,7 @@ import type { ReviewDiffState, ReviewScreenState } from './mobile-diff-review-sc
|
||||
import { reviewDescriptorFromItem } from './mobile-diff-review-screen-model'
|
||||
|
||||
type BranchCompareLoadResult = {
|
||||
result: MobileGitBranchCompareResult | null
|
||||
result: GitBranchCompareResult | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
@@ -33,7 +31,7 @@ type DiffLoadInput = {
|
||||
client: RpcClient
|
||||
worktreeId: string
|
||||
item: MobileDiffReviewQueueItem
|
||||
branchCompare: MobileGitBranchCompareResult | null
|
||||
branchCompare: GitBranchCompareResult | null
|
||||
}
|
||||
|
||||
export async function loadMobileDiffReviewBranchCompare(
|
||||
@@ -163,7 +161,7 @@ async function loadBranchFileDiff(
|
||||
client: RpcClient,
|
||||
worktreeId: string,
|
||||
item: MobileDiffReviewQueueItem,
|
||||
branchCompare: MobileGitBranchCompareResult | null
|
||||
branchCompare: GitBranchCompareResult | null
|
||||
) {
|
||||
const summary = branchCompare?.summary
|
||||
if (!summary || !summary.headOid || !summary.mergeBase) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { GitStagingArea } from '../../../src/shared/git-status-types'
|
||||
import type { DiffReviewScope } from '../../../src/shared/diff-comment-types'
|
||||
import type { MobileGitStagingArea } from '../source-control/mobile-git-status'
|
||||
import {
|
||||
createMobileDiffReviewFileKey,
|
||||
type MobileDiffReviewQueueItem
|
||||
} from './mobile-diff-review-queue'
|
||||
|
||||
export type MobileDiffReviewTargetArea = MobileGitStagingArea | 'branch'
|
||||
export type MobileDiffReviewTargetArea = GitStagingArea | 'branch'
|
||||
|
||||
export type MobileDiffReviewInitialTarget = {
|
||||
filePath: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GitStatusEntry } from '../../../src/shared/git-status-types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types'
|
||||
import type { MobileGitBranchChangeEntry } from '../source-control/mobile-branch-compare'
|
||||
import type { MobileGitStatusEntry } from '../source-control/mobile-git-status'
|
||||
import type { GitBranchChangeEntry } from '../../../src/shared/git-diff-compare-types'
|
||||
import {
|
||||
buildMobileDiffReviewQueue,
|
||||
createMobileDiffReviewFileKey,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
const emptyReviewState: MobileDiffReviewState = { version: 1, files: {} }
|
||||
|
||||
function statusEntry(overrides: Partial<MobileGitStatusEntry>): MobileGitStatusEntry {
|
||||
function statusEntry(overrides: Partial<GitStatusEntry>): GitStatusEntry {
|
||||
return {
|
||||
path: 'src/app.ts',
|
||||
status: 'modified',
|
||||
@@ -21,7 +21,7 @@ function statusEntry(overrides: Partial<MobileGitStatusEntry>): MobileGitStatusE
|
||||
}
|
||||
}
|
||||
|
||||
function branchEntry(overrides: Partial<MobileGitBranchChangeEntry>): MobileGitBranchChangeEntry {
|
||||
function branchEntry(overrides: Partial<GitBranchChangeEntry>): GitBranchChangeEntry {
|
||||
return {
|
||||
path: 'src/branch.ts',
|
||||
status: 'modified',
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import type {
|
||||
GitFileStatus,
|
||||
GitStagingArea,
|
||||
GitStatusEntry
|
||||
} from '../../../src/shared/git-status-types'
|
||||
import type {
|
||||
DiffComment,
|
||||
DiffReviewScope,
|
||||
MobileDiffReviewState
|
||||
} from '../../../src/shared/diff-comment-types'
|
||||
import type { MobileGitBranchChangeEntry } from '../source-control/mobile-branch-compare'
|
||||
import type { GitBranchChangeEntry } from '../../../src/shared/git-diff-compare-types'
|
||||
import {
|
||||
isMobileGitDiscardableEntry,
|
||||
isMobileGitStageableEntry,
|
||||
type MobileGitFileStatus,
|
||||
type MobileGitStagingArea,
|
||||
type MobileGitStatusEntry
|
||||
isMobileGitStageableEntry
|
||||
} from '../source-control/mobile-git-status'
|
||||
import {
|
||||
buildMobileDiffIdentity,
|
||||
@@ -28,10 +30,10 @@ export type MobileDiffReviewQueueFilter =
|
||||
export type MobileDiffReviewQueueItem = {
|
||||
key: string
|
||||
scope: DiffReviewScope
|
||||
area: MobileGitStagingArea | 'branch'
|
||||
area: GitStagingArea | 'branch'
|
||||
filePath: string
|
||||
oldPath?: string
|
||||
status: MobileGitFileStatus
|
||||
status: GitFileStatus
|
||||
title: string
|
||||
subtitle: string
|
||||
added?: number
|
||||
@@ -51,8 +53,8 @@ export type MobileDiffReviewQueueItem = {
|
||||
|
||||
export type BuildMobileDiffReviewQueueInput = {
|
||||
worktreeId: string
|
||||
statusEntries: readonly MobileGitStatusEntry[]
|
||||
branchEntries: readonly MobileGitBranchChangeEntry[]
|
||||
statusEntries: readonly GitStatusEntry[]
|
||||
branchEntries: readonly GitBranchChangeEntry[]
|
||||
branchHeadOid?: string | null
|
||||
branchMergeBase?: string | null
|
||||
comments: readonly DiffComment[]
|
||||
@@ -65,20 +67,20 @@ const SCOPE_SORT_ORDER: Record<DiffReviewScope, number> = {
|
||||
branch: 2
|
||||
}
|
||||
|
||||
function scopeForStatusArea(area: MobileGitStagingArea): DiffReviewScope {
|
||||
function scopeForStatusArea(area: GitStagingArea): DiffReviewScope {
|
||||
return area === 'staged' ? 'staged' : 'unstaged'
|
||||
}
|
||||
|
||||
export function createMobileDiffReviewFileKey(
|
||||
scope: DiffReviewScope,
|
||||
area: MobileGitStagingArea | 'branch',
|
||||
area: GitStagingArea | 'branch',
|
||||
filePath: string,
|
||||
oldPath?: string
|
||||
): string {
|
||||
return [scope, area, oldPath ?? '', filePath].join('\0')
|
||||
}
|
||||
|
||||
function statusEntryIdentity(entry: MobileGitStatusEntry, scope: DiffReviewScope): string {
|
||||
function statusEntryIdentity(entry: GitStatusEntry, scope: DiffReviewScope): string {
|
||||
return buildMobileDiffIdentity([
|
||||
scope,
|
||||
entry.area,
|
||||
@@ -92,7 +94,7 @@ function statusEntryIdentity(entry: MobileGitStatusEntry, scope: DiffReviewScope
|
||||
}
|
||||
|
||||
function branchEntryIdentity(
|
||||
entry: MobileGitBranchChangeEntry,
|
||||
entry: GitBranchChangeEntry,
|
||||
branchHeadOid: string | null | undefined,
|
||||
branchMergeBase: string | null | undefined
|
||||
): string {
|
||||
@@ -163,7 +165,7 @@ function queueNoteCounts(
|
||||
}
|
||||
|
||||
function statusEntryToQueueItem(
|
||||
entry: MobileGitStatusEntry,
|
||||
entry: GitStatusEntry,
|
||||
comments: readonly DiffComment[],
|
||||
reviewState: MobileDiffReviewState
|
||||
): MobileDiffReviewQueueItem {
|
||||
@@ -199,7 +201,7 @@ function statusEntryToQueueItem(
|
||||
}
|
||||
|
||||
function branchEntryToQueueItem(
|
||||
entry: MobileGitBranchChangeEntry,
|
||||
entry: GitBranchChangeEntry,
|
||||
input: BuildMobileDiffReviewQueueInput
|
||||
): MobileDiffReviewQueueItem {
|
||||
const scope: DiffReviewScope = 'branch'
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import type {
|
||||
MobileGitBranchChangeEntry,
|
||||
MobileGitBranchCompareResult,
|
||||
MobileGitBranchCompareSummary
|
||||
} from '../source-control/mobile-branch-compare'
|
||||
import type {
|
||||
MobileGitFileStatus,
|
||||
MobileGitStagingArea,
|
||||
MobileGitStatusEntry,
|
||||
MobileGitStatusResult,
|
||||
MobileGitUpstreamStatus
|
||||
} from '../source-control/mobile-git-status'
|
||||
GitFileStatus,
|
||||
GitStagingArea,
|
||||
GitStatusEntry,
|
||||
GitStatusResult,
|
||||
GitUpstreamStatus
|
||||
} from '../../../src/shared/git-status-types'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import type { GitBranchChangeEntry } from '../../../src/shared/git-diff-compare-types'
|
||||
import type { GitBranchCompareSummary } from '../../../src/shared/git-diff-compare-types'
|
||||
|
||||
export type MobileReviewGitDiffResult =
|
||||
| {
|
||||
@@ -47,7 +45,7 @@ function readBoolean(value: unknown): boolean | undefined {
|
||||
return typeof value === 'boolean' ? value : undefined
|
||||
}
|
||||
|
||||
function readFileStatus(value: unknown): MobileGitFileStatus | null {
|
||||
function readFileStatus(value: unknown): GitFileStatus | null {
|
||||
return value === 'modified' ||
|
||||
value === 'added' ||
|
||||
value === 'deleted' ||
|
||||
@@ -58,17 +56,17 @@ function readFileStatus(value: unknown): MobileGitFileStatus | null {
|
||||
: null
|
||||
}
|
||||
|
||||
function readStagingArea(value: unknown): MobileGitStagingArea | null {
|
||||
function readStagingArea(value: unknown): GitStagingArea | null {
|
||||
return value === 'staged' || value === 'unstaged' || value === 'untracked' ? value : null
|
||||
}
|
||||
|
||||
function readConflictOperation(value: unknown): MobileGitStatusResult['conflictOperation'] {
|
||||
function readConflictOperation(value: unknown): GitStatusResult['conflictOperation'] {
|
||||
return value === 'merge' || value === 'rebase' || value === 'cherry-pick' || value === 'unknown'
|
||||
? value
|
||||
: 'unknown'
|
||||
}
|
||||
|
||||
function readUpstreamStatus(value: unknown): MobileGitUpstreamStatus | undefined {
|
||||
function readUpstreamStatus(value: unknown): GitUpstreamStatus | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined
|
||||
}
|
||||
@@ -88,7 +86,7 @@ function readUpstreamStatus(value: unknown): MobileGitUpstreamStatus | undefined
|
||||
}
|
||||
}
|
||||
|
||||
function readStatusEntry(value: unknown): MobileGitStatusEntry | null {
|
||||
function readStatusEntry(value: unknown): GitStatusEntry | null {
|
||||
if (!isRecord(value)) {
|
||||
return null
|
||||
}
|
||||
@@ -117,12 +115,12 @@ function readStatusEntry(value: unknown): MobileGitStatusEntry | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function readMobileGitStatusResult(value: unknown): MobileGitStatusResult | null {
|
||||
export function readMobileGitStatusResult(value: unknown): GitStatusResult | null {
|
||||
if (!isRecord(value) || !Array.isArray(value.entries)) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
entries: value.entries.flatMap((entry): MobileGitStatusEntry[] => {
|
||||
entries: value.entries.flatMap((entry): GitStatusEntry[] => {
|
||||
const parsed = readStatusEntry(entry)
|
||||
return parsed ? [parsed] : []
|
||||
}),
|
||||
@@ -133,7 +131,7 @@ export function readMobileGitStatusResult(value: unknown): MobileGitStatusResult
|
||||
}
|
||||
}
|
||||
|
||||
function readBranchStatus(value: unknown): MobileGitBranchCompareSummary['status'] {
|
||||
function readBranchStatus(value: unknown): GitBranchCompareSummary['status'] {
|
||||
return value === 'ready' ||
|
||||
value === 'invalid-base' ||
|
||||
value === 'unborn-head' ||
|
||||
@@ -144,7 +142,7 @@ function readBranchStatus(value: unknown): MobileGitBranchCompareSummary['status
|
||||
: 'error'
|
||||
}
|
||||
|
||||
function readBranchEntry(value: unknown): MobileGitBranchChangeEntry | null {
|
||||
function readBranchEntry(value: unknown): GitBranchChangeEntry | null {
|
||||
if (!isRecord(value)) {
|
||||
return null
|
||||
}
|
||||
@@ -162,7 +160,7 @@ function readBranchEntry(value: unknown): MobileGitBranchChangeEntry | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function readMobileBranchCompareResult(value: unknown): MobileGitBranchCompareResult | null {
|
||||
export function readMobileBranchCompareResult(value: unknown): GitBranchCompareResult | null {
|
||||
if (!isRecord(value) || !isRecord(value.summary) || !Array.isArray(value.entries)) {
|
||||
return null
|
||||
}
|
||||
@@ -184,7 +182,7 @@ export function readMobileBranchCompareResult(value: unknown): MobileGitBranchCo
|
||||
status: readBranchStatus(value.summary.status),
|
||||
errorMessage: readString(value.summary.errorMessage)
|
||||
},
|
||||
entries: value.entries.flatMap((entry): MobileGitBranchChangeEntry[] => {
|
||||
entries: value.entries.flatMap((entry): GitBranchChangeEntry[] => {
|
||||
const parsed = readBranchEntry(entry)
|
||||
return parsed ? [parsed] : []
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import type { DiffComment, MobileDiffReviewState } from '../../../src/shared/diff-comment-types'
|
||||
import type { MobileGitBranchCompareResult } from '../source-control/mobile-branch-compare'
|
||||
import type { MobileGitStatusResult } from '../source-control/mobile-git-status'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import type { MobileDiffLine } from './mobile-diff-lines'
|
||||
import type { MobileDiffHunk } from './mobile-diff-hunks'
|
||||
import type {
|
||||
@@ -15,8 +15,8 @@ export type ReviewScreenState =
|
||||
| { kind: 'loading' }
|
||||
| {
|
||||
kind: 'ready'
|
||||
status: MobileGitStatusResult
|
||||
branchCompare: MobileGitBranchCompareResult | null
|
||||
status: GitStatusResult
|
||||
branchCompare: GitBranchCompareResult | null
|
||||
branchError?: string
|
||||
comments: DiffComment[]
|
||||
reviewState: MobileDiffReviewState
|
||||
|
||||
@@ -3,12 +3,9 @@ import type { DiffComment } from '../../../src/shared/diff-comment-types'
|
||||
import type { TuiAgent } from '../../../src/shared/tui-agent'
|
||||
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
|
||||
import type { MobileBrowserTab } from '../browser/MobileBrowserPane'
|
||||
import type { MobileTerminalTheme } from '../terminal/terminal-webview-contract'
|
||||
import type { RuntimeMobileTerminalTheme } from '../../../src/shared/runtime-types'
|
||||
import type { MobileDiffLine } from './mobile-diff-lines'
|
||||
import type { MobileHighlightedDiffLine, MobileSyntaxSegment } from './mobile-file-syntax'
|
||||
import type { TerminalRecord } from './mobile-terminal-records'
|
||||
|
||||
export type Terminal = TerminalRecord
|
||||
|
||||
export type MobileSessionTabType = 'terminal' | 'markdown' | 'file' | 'browser' | 'agent-session'
|
||||
|
||||
@@ -28,7 +25,7 @@ export type MobileSessionTab =
|
||||
/** Host-provided launch context still parked as an unsent TUI-input draft. */
|
||||
launchDraft?: string
|
||||
launchDraftCreatedAt?: number
|
||||
terminalTheme?: MobileTerminalTheme
|
||||
terminalTheme?: RuntimeMobileTerminalTheme
|
||||
isActive: boolean
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { MobileTerminalTheme } from '../terminal/terminal-webview-contract'
|
||||
import type { RuntimeMobileTerminalTheme } from '../../../src/shared/runtime-types'
|
||||
import type { AgentStatusEntry } from '../../../src/shared/agent-status-types'
|
||||
|
||||
export type TerminalRecord = {
|
||||
handle: string
|
||||
title: string
|
||||
terminalTheme?: MobileTerminalTheme
|
||||
terminalTheme?: RuntimeMobileTerminalTheme
|
||||
isActive: boolean
|
||||
/** From `terminal.list`; parked and proven-absent leaves report false. */
|
||||
connected?: boolean
|
||||
@@ -24,7 +24,7 @@ export type MobileTerminalSessionTab = {
|
||||
/** Host-provided launch context still parked as an unsent TUI-input draft. */
|
||||
launchDraft?: string
|
||||
launchDraftCreatedAt?: number
|
||||
terminalTheme?: MobileTerminalTheme
|
||||
terminalTheme?: RuntimeMobileTerminalTheme
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ type MobileSessionTabLike =
|
||||
}
|
||||
|
||||
export function mobileTerminalThemesEqual(
|
||||
left: MobileTerminalTheme | null | undefined,
|
||||
right: MobileTerminalTheme | null | undefined
|
||||
left: RuntimeMobileTerminalTheme | null | undefined,
|
||||
right: RuntimeMobileTerminalTheme | null | undefined
|
||||
): boolean {
|
||||
if (left === right) {
|
||||
return true
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { MobileGitBranchCompareResult } from '../source-control/mobile-branch-compare'
|
||||
import type { MobileGitStatusResult } from '../source-control/mobile-git-status'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import {
|
||||
deriveMobilePrBranchContext,
|
||||
loadMobilePrBranchContext,
|
||||
loadMobilePrRepoContext
|
||||
} from './use-mobile-pr-branch-context'
|
||||
|
||||
function status(overrides: Partial<MobileGitStatusResult>): MobileGitStatusResult {
|
||||
function status(overrides: Partial<GitStatusResult>): GitStatusResult {
|
||||
return {
|
||||
entries: [],
|
||||
conflictOperation: 'unknown',
|
||||
@@ -17,7 +17,7 @@ function status(overrides: Partial<MobileGitStatusResult>): MobileGitStatusResul
|
||||
}
|
||||
}
|
||||
|
||||
function branchCompare(headOid: string | null): MobileGitBranchCompareResult {
|
||||
function branchCompare(headOid: string | null): GitBranchCompareResult {
|
||||
return {
|
||||
summary: {
|
||||
baseRef: 'main',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { ConnectionState } from '../transport/types'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { MobileGitBranchCompareResult } from '../source-control/mobile-branch-compare'
|
||||
import type { MobileGitStatusResult } from '../source-control/mobile-git-status'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import { resolveMobileBranchCompareBaseRef } from '../source-control/mobile-branch-base-ref'
|
||||
import { fetchGithubRepoSlug } from './github-pr-rpc'
|
||||
import { readMobileBranchCompareResult, readMobileGitStatusResult } from './mobile-diff-review-rpc'
|
||||
@@ -10,7 +10,7 @@ import { readMobileBranchCompareResult, readMobileGitStatusResult } from './mobi
|
||||
export type MobilePrBranchContext = {
|
||||
branch: string | null
|
||||
headSha: string | null
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
isGithubRepo: boolean
|
||||
repoLoaded: boolean
|
||||
loaded: boolean
|
||||
@@ -21,9 +21,9 @@ export type MobilePrBranchContext = {
|
||||
// `status.head ?? branchCompare.summary.headOid ?? null` — a status-only read would lose
|
||||
// the SHA when `status.head` is absent and diverge from the review surface's check status.
|
||||
export function deriveMobilePrBranchContext(
|
||||
status: MobileGitStatusResult | null,
|
||||
branchCompare: MobileGitBranchCompareResult | null
|
||||
): { branch: string | null; headSha: string | null; status: MobileGitStatusResult | null } {
|
||||
status: GitStatusResult | null,
|
||||
branchCompare: GitBranchCompareResult | null
|
||||
): { branch: string | null; headSha: string | null; status: GitStatusResult | null } {
|
||||
return {
|
||||
branch: status?.branch ?? null,
|
||||
headSha: status?.head ?? branchCompare?.summary.headOid ?? null,
|
||||
@@ -172,7 +172,7 @@ export async function loadMobilePrBranchIdentity(
|
||||
async function readGitStatus(
|
||||
client: RpcClient,
|
||||
worktreeId: string
|
||||
): Promise<MobileGitStatusResult | null> {
|
||||
): Promise<GitStatusResult | null> {
|
||||
const response = await client.sendRequest('git.status', { worktree: `id:${worktreeId}` })
|
||||
return response.ok ? readMobileGitStatusResult(response.result) : null
|
||||
}
|
||||
@@ -180,7 +180,7 @@ async function readGitStatus(
|
||||
async function readBranchCompare(
|
||||
client: RpcClient,
|
||||
worktreeId: string
|
||||
): Promise<MobileGitBranchCompareResult | null> {
|
||||
): Promise<GitBranchCompareResult | null> {
|
||||
// branchCompare requires a baseRef; without one (or on error) the headOid fallback is
|
||||
// simply unavailable and headSha relies on status.head.
|
||||
const baseRef = await resolveMobileBranchCompareBaseRef(client, worktreeId)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MobileSessionTab, Terminal } from './mobile-session-route-types'
|
||||
import type { MobileSessionTab } from './mobile-session-route-types'
|
||||
import type { TerminalRecord } from './mobile-terminal-records'
|
||||
import type { MobileSessionContentCreateActionsModel } from './use-mobile-session-content-create-actions'
|
||||
|
||||
export function useMobileSessionCloseActions(scope: MobileSessionContentCreateActionsModel) {
|
||||
@@ -60,7 +61,7 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCloseTerminal(target: Terminal) {
|
||||
async function handleCloseTerminal(target: TerminalRecord) {
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,16 +20,16 @@ import type {
|
||||
MarkdownDocState,
|
||||
MobileDisplayMode,
|
||||
MobileNewTabAgentLoadState,
|
||||
MobileSessionTab,
|
||||
Terminal
|
||||
MobileSessionTab
|
||||
} from './mobile-session-route-types'
|
||||
import type { TerminalRecord } from './mobile-terminal-records'
|
||||
import { useMobileSessionTabActionTargets } from './use-mobile-session-tab-action-targets'
|
||||
import type { MobileSessionFoundationModel } from './use-mobile-session-foundation'
|
||||
|
||||
export function useMobileSessionScreenState(scope: MobileSessionFoundationModel) {
|
||||
const { worktreeId, hostId, initialCreateWarning } = scope
|
||||
const [terminals, setTerminals] = useState<Terminal[]>([])
|
||||
const terminalsRef = useRef<Terminal[]>([])
|
||||
const [terminals, setTerminals] = useState<TerminalRecord[]>([])
|
||||
const terminalsRef = useRef<TerminalRecord[]>([])
|
||||
const [sessionTabs, setSessionTabs] = useState<MobileSessionTab[]>([])
|
||||
const sessionTabsRef = useRef<MobileSessionTab[]>([])
|
||||
// Why: track the last applied (epoch, version) so a late older snapshot can't overwrite a newer one and resurrect closed tabs (session-tab-snapshot-gate).
|
||||
@@ -97,7 +97,7 @@ export function useMobileSessionScreenState(scope: MobileSessionFoundationModel)
|
||||
{ type: 'markdown' }
|
||||
> | null>(null)
|
||||
const [leaveDrafts, setLeaveDrafts] = useState<DirtyMarkdownDraft[] | null>(null)
|
||||
const [renameTarget, setRenameTarget] = useState<Terminal | null>(null)
|
||||
const [renameTarget, setRenameTarget] = useState<TerminalRecord | null>(null)
|
||||
const [customKeys, setCustomKeys] = useState<CustomKey[]>([])
|
||||
const [visibleBuiltInIds, setVisibleBuiltInIds] = useState<string[]>(
|
||||
getDefaultTerminalAccessoryBuiltInIds
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
type MutableRefObject,
|
||||
type SetStateAction
|
||||
} from 'react'
|
||||
import type { MobileSessionTab, Terminal } from './mobile-session-route-types'
|
||||
import type { MobileSessionTab } from './mobile-session-route-types'
|
||||
import type { TerminalRecord } from './mobile-terminal-records'
|
||||
|
||||
type MarkdownTab = Extract<MobileSessionTab, { type: 'markdown' }>
|
||||
type FileTab = Extract<MobileSessionTab, { type: 'file' }>
|
||||
@@ -14,7 +15,7 @@ type AgentSessionTab = Extract<MobileSessionTab, { type: 'agent-session' }>
|
||||
type SetActionTarget<T> = Dispatch<SetStateAction<T | null>>
|
||||
|
||||
export function useMobileSessionTabActionTargets() {
|
||||
const [actionTarget, setActionTarget] = useState<Terminal | null>(null)
|
||||
const [actionTarget, setActionTarget] = useState<TerminalRecord | null>(null)
|
||||
const [markdownActionTarget, setMarkdownActionTarget] = useState<MarkdownTab | null>(null)
|
||||
const [fileActionTarget, setFileActionTarget] = useState<FileTab | null>(null)
|
||||
const [browserActionTarget, setBrowserActionTarget] = useState<BrowserTab | null>(null)
|
||||
@@ -38,7 +39,7 @@ export function useMobileSessionTabActionTargets() {
|
||||
|
||||
export function useMobileSessionTabActionSheetOpener(args: {
|
||||
activeHandleRef: MutableRefObject<string | null>
|
||||
setActionTarget: SetActionTarget<Terminal>
|
||||
setActionTarget: SetActionTarget<TerminalRecord>
|
||||
setMarkdownActionTarget: SetActionTarget<MarkdownTab>
|
||||
setFileActionTarget: SetActionTarget<FileTab>
|
||||
setBrowserActionTarget: SetActionTarget<BrowserTab>
|
||||
|
||||
@@ -8,7 +8,8 @@ import { buildTerminalSendParams } from '../terminal/terminal-send-request'
|
||||
import { terminalRecordsEqual } from './mobile-terminal-records'
|
||||
import type { MobileNewTabAgentOption } from './mobile-new-tab-agent-options'
|
||||
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
|
||||
import type { Terminal, TerminalCreateResult } from './mobile-session-route-types'
|
||||
import type { TerminalCreateResult } from './mobile-session-route-types'
|
||||
import type { TerminalRecord } from './mobile-terminal-records'
|
||||
import type { MobileSessionAttachmentsModel } from './use-mobile-session-attachments'
|
||||
import { isAgentSessionHandleProvider } from '../../../src/shared/agent-session-provider-handle'
|
||||
import { createMobileStructuredAgentSession } from './mobile-structured-agent-session-launch'
|
||||
@@ -145,7 +146,7 @@ export function useMobileSessionTerminalCreateActions(scope: MobileSessionAttach
|
||||
setActiveHandle(createdHandle)
|
||||
setTerminals((prev) => {
|
||||
const existing = prev.find((terminal) => terminal.handle === createdHandle)
|
||||
const createdTerminal: Terminal = {
|
||||
const createdTerminal: TerminalRecord = {
|
||||
handle: createdHandle,
|
||||
title: created.title || existing?.title || 'Terminal',
|
||||
terminalTheme: created.terminalTheme ?? existing?.terminalTheme,
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
TERMINAL_GESTURE_INPUT_MAX_QUEUE_AGE_MS,
|
||||
TERMINAL_GESTURE_INPUT_REFILL_PER_SECOND
|
||||
} from './mobile-session-route-helpers'
|
||||
import type { Terminal, TerminalGestureInputQueue } from './mobile-session-route-types'
|
||||
import type { TerminalGestureInputQueue } from './mobile-session-route-types'
|
||||
import type { TerminalRecord } from './mobile-terminal-records'
|
||||
import type { MobileSessionFileActionsModel } from './use-mobile-session-file-actions'
|
||||
|
||||
export function useMobileSessionTerminalInput(scope: MobileSessionFileActionsModel) {
|
||||
@@ -228,7 +229,7 @@ export function useMobileSessionTerminalInput(scope: MobileSessionFileActionsMod
|
||||
})
|
||||
}, [])
|
||||
|
||||
async function handleClearTerminal(target: Terminal) {
|
||||
async function handleClearTerminal(target: TerminalRecord) {
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
pruneTerminalKeyboardMetrics,
|
||||
resolveRetainedTerminalHandles
|
||||
} from './mobile-terminal-prune-decision'
|
||||
import type { Terminal } from './mobile-session-route-types'
|
||||
import type { TerminalRecord } from './mobile-terminal-records'
|
||||
import type { MobileSessionTerminalStreamDisplayModel } from './use-mobile-session-terminal-stream-display'
|
||||
import { MobileTerminalInventoryRequest } from './mobile-terminal-inventory-request'
|
||||
import type { MobileTerminalInventoryRefreshOptions } from './use-mobile-terminal-inventory-recovery'
|
||||
@@ -61,7 +61,7 @@ export function useMobileSessionTerminalList(scope: MobileSessionTerminalStreamD
|
||||
if (!isCurrent() || !response.ok) {
|
||||
return false
|
||||
}
|
||||
const result = (response as RpcSuccess).result as { terminals: Terminal[] }
|
||||
const result = (response as RpcSuccess).result as { terminals: TerminalRecord[] }
|
||||
if (result.terminals.length === 0 && !allowsEmpty()) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { MobileSpeechSetup } from '../dictation/mobile-dictation-setup'
|
||||
import type { RuntimeSpeechSetupState } from '../../../src/shared/runtime-types'
|
||||
|
||||
export interface VoiceSettingsOperations {
|
||||
load(): Promise<MobileSpeechSetup>
|
||||
load(): Promise<RuntimeSpeechSetupState>
|
||||
configure(params: {
|
||||
enabled?: boolean
|
||||
modelId?: string
|
||||
dictationMode?: 'toggle' | 'hold'
|
||||
}): Promise<MobileSpeechSetup>
|
||||
}): Promise<RuntimeSpeechSetupState>
|
||||
download(modelId: string): Promise<void>
|
||||
delete(modelId: string): Promise<MobileSpeechSetup>
|
||||
delete(modelId: string): Promise<RuntimeSpeechSetupState>
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RuntimeSpeechSetupState } from '../../../src/shared/runtime-types'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { ActivityIndicator, Pressable, ScrollView, Switch, Text, View } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
@@ -8,11 +9,7 @@ import { colors, spacing } from '../theme/mobile-theme'
|
||||
import { BottomDrawer } from '../components/BottomDrawer'
|
||||
import { VoiceModelList } from '../components/VoiceModelList'
|
||||
import { useDictationSetupPoller } from '../dictation/use-dictation-setup-poller'
|
||||
import {
|
||||
isModelInFlight,
|
||||
type MobileSpeechModel,
|
||||
type MobileSpeechSetup
|
||||
} from '../dictation/mobile-dictation-setup'
|
||||
import { isModelInFlight, type MobileSpeechModel } from '../dictation/mobile-dictation-setup'
|
||||
|
||||
const POLL_INTERVAL_MS = 1500
|
||||
|
||||
@@ -33,7 +30,7 @@ export default function VoiceSettingsScreen({
|
||||
onBack: () => void
|
||||
}): React.JSX.Element {
|
||||
const insets = useSafeAreaInsets()
|
||||
const [setup, setSetup] = useState<MobileSpeechSetup | null>(null)
|
||||
const [setup, setSetup] = useState<RuntimeSpeechSetupState | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busyAction, setBusyAction] = useState<ModelBusyAction | null>(null)
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildMobileBranchCompareSection,
|
||||
canOpenMobileBranchCompareDiff,
|
||||
formatMobileBranchCompareSummary,
|
||||
type MobileGitBranchCompareResult
|
||||
formatMobileBranchCompareSummary
|
||||
} from './mobile-branch-compare'
|
||||
|
||||
describe('mobile branch compare helpers', () => {
|
||||
it('keeps the mobile branch compare type in lockstep with the runtime contract', () => {
|
||||
expectTypeOf<MobileGitBranchCompareResult>().toEqualTypeOf<GitBranchCompareResult>()
|
||||
})
|
||||
|
||||
it('sorts committed branch entries by path', () => {
|
||||
const section = buildMobileBranchCompareSection([
|
||||
{ path: 'zeta.ts', status: 'modified' },
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import type {
|
||||
GitBranchChangeEntry,
|
||||
GitBranchCompareResult,
|
||||
GitBranchCompareSummary
|
||||
} from '../../../src/shared/git-diff-compare-types'
|
||||
|
||||
export type MobileGitBranchChangeEntry = GitBranchChangeEntry
|
||||
export type MobileGitBranchCompareSummary = GitBranchCompareSummary
|
||||
export type MobileGitBranchCompareResult = GitBranchCompareResult
|
||||
export type MobileBranchCompareSection<TEntry extends GitBranchChangeEntry = GitBranchChangeEntry> =
|
||||
{
|
||||
title: 'Committed on Branch'
|
||||
data: TEntry[]
|
||||
}
|
||||
|
||||
export type MobileBranchCompareSection<
|
||||
TEntry extends MobileGitBranchChangeEntry = MobileGitBranchChangeEntry
|
||||
> = {
|
||||
title: 'Committed on Branch'
|
||||
data: TEntry[]
|
||||
}
|
||||
|
||||
export function buildMobileBranchCompareSection<TEntry extends MobileGitBranchChangeEntry>(
|
||||
export function buildMobileBranchCompareSection<TEntry extends GitBranchChangeEntry>(
|
||||
entries: readonly TEntry[]
|
||||
): MobileBranchCompareSection<TEntry> | null {
|
||||
if (entries.length === 0) {
|
||||
@@ -32,9 +26,7 @@ export function buildMobileBranchCompareSection<TEntry extends MobileGitBranchCh
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMobileBranchCompareSummary(
|
||||
summary: MobileGitBranchCompareSummary
|
||||
): string | null {
|
||||
export function formatMobileBranchCompareSummary(summary: GitBranchCompareSummary): string | null {
|
||||
if (summary.status !== 'ready') {
|
||||
return summary.errorMessage ?? null
|
||||
}
|
||||
@@ -46,6 +38,6 @@ export function formatMobileBranchCompareSummary(
|
||||
return parts.join(' - ')
|
||||
}
|
||||
|
||||
export function canOpenMobileBranchCompareDiff(summary: MobileGitBranchCompareSummary): boolean {
|
||||
export function canOpenMobileBranchCompareDiff(summary: GitBranchCompareSummary): boolean {
|
||||
return summary.status === 'ready' && Boolean(summary.headOid && summary.mergeBase)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MobileGitBranchChangeEntry } from './mobile-branch-compare'
|
||||
import type { GitBranchChangeEntry } from '../../../src/shared/git-diff-compare-types'
|
||||
|
||||
export function formatMobileBranchEntryMeta(entry: MobileGitBranchChangeEntry): string | null {
|
||||
export function formatMobileBranchEntryMeta(entry: GitBranchChangeEntry): string | null {
|
||||
const stats =
|
||||
entry.added !== undefined || entry.removed !== undefined
|
||||
? `+${entry.added ?? 0} -${entry.removed ?? 0}`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MobileGitStatusEntry } from './mobile-git-status'
|
||||
import type { GitStatusEntry } from '../../../src/shared/git-status-types'
|
||||
|
||||
export {
|
||||
COMMIT_FAILURE_SUMMARY_SCAN_CODE_UNITS,
|
||||
@@ -10,14 +10,14 @@ export {
|
||||
export type MobileCommitFailureRecovery = {
|
||||
error: string
|
||||
commitMessage: string
|
||||
stagedEntries: Pick<MobileGitStatusEntry, 'path' | 'status' | 'area'>[]
|
||||
stagedEntries: Pick<GitStatusEntry, 'path' | 'status' | 'area'>[]
|
||||
}
|
||||
|
||||
export type RecordMobileCommitFailure = (failure: MobileCommitFailureRecovery | null) => void
|
||||
|
||||
export function getMobileCommitFailureStagedEntries(
|
||||
entries: readonly MobileGitStatusEntry[] | undefined
|
||||
): Pick<MobileGitStatusEntry, 'path' | 'status' | 'area'>[] {
|
||||
entries: readonly GitStatusEntry[] | undefined
|
||||
): Pick<GitStatusEntry, 'path' | 'status' | 'area'>[] {
|
||||
return (entries ?? [])
|
||||
.filter((entry) => entry.area === 'staged')
|
||||
.map((entry) => ({ path: entry.path, status: entry.status, area: entry.area }))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { bindDeferredRpcOperation, defineRpcOperation } from '../transport/rpc-operation'
|
||||
import type { RpcCompatibleReader } from '../transport/rpc-operation-contract'
|
||||
import {
|
||||
@@ -5,7 +6,6 @@ import {
|
||||
rpcUncheckedPayloadReader
|
||||
} from '../transport/rpc-reader-payload'
|
||||
import { readMobileGitStatusResult } from '../session/mobile-diff-review-rpc'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
|
||||
// Source-control reads. Every one of these replies used to be re-typed with a cast at the call
|
||||
// site; the reader below is now the only place that says what the payload is.
|
||||
@@ -32,7 +32,7 @@ export const gitStatusHostPayloadRead = bindDeferredRpcOperation(
|
||||
const gitStatusProjectionReader: RpcCompatibleReader<
|
||||
unknown,
|
||||
'normalized-status',
|
||||
MobileGitStatusResult | null
|
||||
GitStatusResult | null
|
||||
> = (raw) => ({
|
||||
compatible: true,
|
||||
variant: 'normalized-status',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { GitStatusEntry } from '../../../src/shared/git-status-types'
|
||||
import {
|
||||
buildMobileSourceControlSections,
|
||||
canOpenMobileGitStatusEntry,
|
||||
@@ -10,22 +10,16 @@ import {
|
||||
isMobileGitDiscardableEntry,
|
||||
isMobileGitStageableEntry,
|
||||
isMobileGitTransientRefreshError,
|
||||
isMobileGitUnavailable,
|
||||
type MobileGitStatusEntry,
|
||||
type MobileGitStatusResult
|
||||
isMobileGitUnavailable
|
||||
} from './mobile-git-status'
|
||||
|
||||
const entries: MobileGitStatusEntry[] = [
|
||||
const entries: GitStatusEntry[] = [
|
||||
{ path: 'b.ts', status: 'modified', area: 'staged' },
|
||||
{ path: 'a.ts', status: 'modified', area: 'unstaged' },
|
||||
{ path: 'new.ts', status: 'untracked', area: 'untracked' }
|
||||
]
|
||||
|
||||
describe('mobile source control status helpers', () => {
|
||||
it('keeps the mobile RPC status type in lockstep with the shared git contract', () => {
|
||||
expectTypeOf<MobileGitStatusResult>().toEqualTypeOf<GitStatusResult>()
|
||||
})
|
||||
|
||||
it('builds sections in the mobile source control order', () => {
|
||||
const sections = buildMobileSourceControlSections(entries)
|
||||
|
||||
@@ -44,7 +38,7 @@ describe('mobile source control status helpers', () => {
|
||||
})
|
||||
|
||||
it('keeps unresolved conflicts out of stage actions', () => {
|
||||
const conflictedEntries: MobileGitStatusEntry[] = [
|
||||
const conflictedEntries: GitStatusEntry[] = [
|
||||
{ path: 'ready.ts', status: 'modified', area: 'unstaged' },
|
||||
{
|
||||
path: 'conflicted.ts',
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
import type {
|
||||
GitFileStatus,
|
||||
GitStagingArea,
|
||||
GitStatusEntry,
|
||||
GitStatusResult,
|
||||
GitUpstreamStatus
|
||||
GitStatusEntry
|
||||
} from '../../../src/shared/git-status-types'
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
|
||||
export type MobileGitFileStatus = GitFileStatus
|
||||
export type MobileGitStagingArea = GitStagingArea
|
||||
export type MobileGitStatusEntry = GitStatusEntry
|
||||
export type MobileGitUpstreamStatus = GitUpstreamStatus
|
||||
export type MobileGitStatusResult = GitStatusResult
|
||||
export type MobileSourceControlSection<TEntry extends GitStatusEntry = GitStatusEntry> = {
|
||||
area: GitStagingArea
|
||||
title: string
|
||||
data: TEntry[]
|
||||
}
|
||||
|
||||
export type MobileSourceControlSection<TEntry extends MobileGitStatusEntry = MobileGitStatusEntry> =
|
||||
{
|
||||
area: MobileGitStagingArea
|
||||
title: string
|
||||
data: TEntry[]
|
||||
}
|
||||
const AREA_ORDER: GitStagingArea[] = ['unstaged', 'untracked', 'staged']
|
||||
|
||||
const AREA_ORDER: MobileGitStagingArea[] = ['unstaged', 'untracked', 'staged']
|
||||
|
||||
const AREA_TITLES: Record<MobileGitStagingArea, string> = {
|
||||
const AREA_TITLES: Record<GitStagingArea, string> = {
|
||||
unstaged: 'Changes',
|
||||
untracked: 'Untracked Files',
|
||||
staged: 'Staged Changes'
|
||||
}
|
||||
|
||||
export const MOBILE_GIT_STATUS_LABELS: Record<MobileGitFileStatus, string> = {
|
||||
export const MOBILE_GIT_STATUS_LABELS: Record<GitFileStatus, string> = {
|
||||
modified: 'M',
|
||||
added: 'A',
|
||||
deleted: 'D',
|
||||
@@ -37,7 +28,7 @@ export const MOBILE_GIT_STATUS_LABELS: Record<MobileGitFileStatus, string> = {
|
||||
copied: 'C'
|
||||
}
|
||||
|
||||
function getConflictSortRank(entry: MobileGitStatusEntry): number {
|
||||
function getConflictSortRank(entry: GitStatusEntry): number {
|
||||
if (entry.conflictStatus === 'unresolved') {
|
||||
return 0
|
||||
}
|
||||
@@ -47,7 +38,7 @@ function getConflictSortRank(entry: MobileGitStatusEntry): number {
|
||||
return 2
|
||||
}
|
||||
|
||||
export function buildMobileSourceControlSections<TEntry extends MobileGitStatusEntry>(
|
||||
export function buildMobileSourceControlSections<TEntry extends GitStatusEntry>(
|
||||
entries: readonly TEntry[]
|
||||
): MobileSourceControlSection<TEntry>[] {
|
||||
const sections = AREA_ORDER.map((area) => ({
|
||||
@@ -67,36 +58,36 @@ export function buildMobileSourceControlSections<TEntry extends MobileGitStatusE
|
||||
return sections
|
||||
}
|
||||
|
||||
export function countStagedEntries(entries: readonly MobileGitStatusEntry[]): number {
|
||||
export function countStagedEntries(entries: readonly GitStatusEntry[]): number {
|
||||
return entries.filter((entry) => entry.area === 'staged').length
|
||||
}
|
||||
|
||||
export function countUnstagedEntries(entries: readonly MobileGitStatusEntry[]): number {
|
||||
export function countUnstagedEntries(entries: readonly GitStatusEntry[]): number {
|
||||
return entries.filter((entry) => entry.area === 'unstaged' || entry.area === 'untracked').length
|
||||
}
|
||||
|
||||
export function getStageablePaths(entries: readonly MobileGitStatusEntry[]): string[] {
|
||||
export function getStageablePaths(entries: readonly GitStatusEntry[]): string[] {
|
||||
return entries.filter(isMobileGitStageableEntry).map((entry) => entry.path)
|
||||
}
|
||||
|
||||
export function getUnstageablePaths(entries: readonly MobileGitStatusEntry[]): string[] {
|
||||
export function getUnstageablePaths(entries: readonly GitStatusEntry[]): string[] {
|
||||
return entries.filter((entry) => entry.area === 'staged').map((entry) => entry.path)
|
||||
}
|
||||
|
||||
export function isMobileGitStageableEntry(entry: MobileGitStatusEntry): boolean {
|
||||
export function isMobileGitStageableEntry(entry: GitStatusEntry): boolean {
|
||||
return (
|
||||
(entry.area === 'unstaged' || entry.area === 'untracked') &&
|
||||
entry.conflictStatus !== 'unresolved'
|
||||
)
|
||||
}
|
||||
|
||||
export function isMobileGitDiscardableEntry(entry: MobileGitStatusEntry): boolean {
|
||||
export function isMobileGitDiscardableEntry(entry: GitStatusEntry): boolean {
|
||||
return entry.conflictStatus !== 'unresolved' && entry.conflictStatus !== 'resolved_locally'
|
||||
}
|
||||
|
||||
// Why: unresolved conflicts are not a stable file to open. Deletions are —
|
||||
// git.diff still returns the pre-delete side (text or image via modifiedDeleted).
|
||||
export function canOpenMobileGitStatusEntry(entry: MobileGitStatusEntry): boolean {
|
||||
export function canOpenMobileGitStatusEntry(entry: GitStatusEntry): boolean {
|
||||
return entry.conflictStatus !== 'unresolved'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import {
|
||||
createMobilePr,
|
||||
getMobilePrCreateBlockMessage,
|
||||
getMobilePrCreateSuccessWarning,
|
||||
shouldPushBeforeMobilePrCreate,
|
||||
type MobilePrPrefill
|
||||
shouldPushBeforeMobilePrCreate
|
||||
} from './mobile-pr-create'
|
||||
import type { MobileHostedReviewPrefill } from './mobile-hosted-review-service'
|
||||
import {
|
||||
prepareMobileHostedReviewCreateIntent,
|
||||
type MobileHostedReviewCreateIntentProgress
|
||||
@@ -15,7 +15,7 @@ import type { MobileSourceControlRpcSender } from './mobile-source-control-rpc-s
|
||||
type RunInput = {
|
||||
branch: string
|
||||
title: string
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
commitMessage?: string
|
||||
onProgress?: (progress: MobileHostedReviewCreateIntentProgress) => void
|
||||
}
|
||||
@@ -25,15 +25,15 @@ export type MobileHostedReviewCreateIntentRunOutcome =
|
||||
ok: true
|
||||
url: string
|
||||
warning?: string
|
||||
prefill: MobilePrPrefill
|
||||
status: MobileGitStatusResult | null
|
||||
prefill: MobileHostedReviewPrefill
|
||||
status: GitStatusResult | null
|
||||
committed: boolean
|
||||
}
|
||||
| {
|
||||
ok: false
|
||||
error: string
|
||||
committed?: boolean
|
||||
status?: MobileGitStatusResult | null
|
||||
status?: GitStatusResult | null
|
||||
commitMessage?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { requestMobileCommitMessage } from './mobile-commit-message-ai'
|
||||
import { getStageablePaths, type MobileGitStatusResult } from './mobile-git-status'
|
||||
import { getStageablePaths } from './mobile-git-status'
|
||||
import { getMobilePrEligibilityReadiness } from './mobile-open-pr-prefill'
|
||||
import { resolveMobilePrPrefill, type MobilePrPrefill } from './mobile-pr-create'
|
||||
import { resolveMobilePrPrefill } from './mobile-pr-create'
|
||||
import type { MobileHostedReviewPrefill } from './mobile-hosted-review-service'
|
||||
import {
|
||||
commitMobileHostedReviewStagedChanges,
|
||||
mobileHostedReviewBranchStillMatches,
|
||||
@@ -24,15 +26,15 @@ type MobileHostedReviewCreateIntentFailure = {
|
||||
ok: false
|
||||
error: string
|
||||
committed?: boolean
|
||||
status?: MobileGitStatusResult | null
|
||||
status?: GitStatusResult | null
|
||||
commitMessage?: string
|
||||
}
|
||||
|
||||
export type MobileHostedReviewCreateIntentOutcome =
|
||||
| {
|
||||
ok: true
|
||||
prefill: MobilePrPrefill
|
||||
status: MobileGitStatusResult | null
|
||||
prefill: MobileHostedReviewPrefill
|
||||
status: GitStatusResult | null
|
||||
committed: boolean
|
||||
}
|
||||
| MobileHostedReviewCreateIntentFailure
|
||||
@@ -40,7 +42,7 @@ export type MobileHostedReviewCreateIntentOutcome =
|
||||
type PrepareInput = {
|
||||
branch: string
|
||||
title: string
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
commitMessage?: string
|
||||
onProgress?: (progress: MobileHostedReviewCreateIntentProgress) => void
|
||||
}
|
||||
@@ -66,7 +68,7 @@ export function mobileHostedReviewCreateIntentProgressMessage(
|
||||
}
|
||||
}
|
||||
|
||||
function hasUnresolvedConflicts(status: MobileGitStatusResult | null): boolean {
|
||||
function hasUnresolvedConflicts(status: GitStatusResult | null): boolean {
|
||||
return status?.entries.some((entry) => entry.conflictStatus === 'unresolved') === true
|
||||
}
|
||||
|
||||
@@ -75,8 +77,8 @@ async function resolvePrefillFromStatus(
|
||||
worktreeId: string,
|
||||
branch: string,
|
||||
title: string,
|
||||
status: MobileGitStatusResult | null
|
||||
): Promise<MobilePrPrefill> {
|
||||
status: GitStatusResult | null
|
||||
): Promise<MobileHostedReviewPrefill> {
|
||||
return resolveMobilePrPrefill(client, worktreeId, {
|
||||
branch,
|
||||
title,
|
||||
@@ -88,9 +90,9 @@ async function ensureLocalChangesCommitted(
|
||||
client: MobileSourceControlRpcSender,
|
||||
worktreeId: string,
|
||||
input: PrepareInput,
|
||||
currentStatus: MobileGitStatusResult | null
|
||||
currentStatus: GitStatusResult | null
|
||||
): Promise<
|
||||
| { ok: true; status: MobileGitStatusResult | null; committed: boolean }
|
||||
| { ok: true; status: GitStatusResult | null; committed: boolean }
|
||||
| MobileHostedReviewCreateIntentFailure
|
||||
> {
|
||||
if ((currentStatus?.entries.length ?? 0) === 0) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import type { RpcSendParams } from '../transport/rpc-params-contract'
|
||||
import {
|
||||
hostReplyErrorTextOrFallback,
|
||||
@@ -6,11 +7,10 @@ import {
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
import { gitBulkStageRun, gitCommitRun, gitPushRun } from './mobile-git-mutation-operations'
|
||||
import { gitStatusProjectionRead } from './mobile-git-read-operations'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
import type { MobileSourceControlRpcSender } from './mobile-source-control-rpc-sender'
|
||||
|
||||
export type MobileHostedReviewStatusReadResult =
|
||||
| { ok: true; status: MobileGitStatusResult | null }
|
||||
| { ok: true; status: GitStatusResult | null }
|
||||
| { ok: false; error: string }
|
||||
|
||||
export type MobileHostedReviewMutationResult = { ok: true } | { ok: false; error: string }
|
||||
@@ -32,7 +32,7 @@ export async function readMobileHostedReviewGitStatus(
|
||||
|
||||
export function mobileHostedReviewBranchStillMatches(
|
||||
inputBranch: string,
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
): boolean {
|
||||
const branch = status?.branch
|
||||
return Boolean(branch && (branch === inputBranch || branch === `refs/heads/${inputBranch}`))
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import type { MobileHostedReviewCreateIntentProgress } from './mobile-hosted-review-create-intent'
|
||||
import type { MobilePrPrefill } from './mobile-pr-create'
|
||||
import type { MobileHostedReviewPrefill } from './mobile-hosted-review-service'
|
||||
import { pushMobileHostedReviewBranch } from './mobile-hosted-review-git-preparation'
|
||||
import type { MobileSourceControlRpcSender } from './mobile-source-control-rpc-sender'
|
||||
|
||||
type RemotePrerequisiteInput = {
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
onProgress?: (progress: MobileHostedReviewCreateIntentProgress) => void
|
||||
}
|
||||
|
||||
export async function applyMobileHostedReviewRemotePrerequisite(
|
||||
client: MobileSourceControlRpcSender,
|
||||
worktreeId: string,
|
||||
prefill: MobilePrPrefill,
|
||||
prefill: MobileHostedReviewPrefill,
|
||||
input: RemotePrerequisiteInput
|
||||
): Promise<{ ok: true; ran: boolean } | { ok: false; error: string }> {
|
||||
const worktree = `id:${worktreeId}`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { getMobilePrEligibilityReadiness, readFreshGitStatus } from './mobile-open-pr-prefill'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
|
||||
const fallback = { branch: 'old', entries: [] } as unknown as MobileGitStatusResult
|
||||
const fallback = { branch: 'old', entries: [] } as unknown as GitStatusResult
|
||||
|
||||
describe('readFreshGitStatus', () => {
|
||||
it('returns the freshly-read status when parseable', async () => {
|
||||
@@ -43,7 +43,7 @@ describe('getMobilePrEligibilityReadiness', () => {
|
||||
const status = {
|
||||
entries: [{ path: 'a.ts' }],
|
||||
upstreamStatus: { hasUpstream: true, ahead: 2, behind: 1 }
|
||||
} as unknown as MobileGitStatusResult
|
||||
} as unknown as GitStatusResult
|
||||
|
||||
expect(getMobilePrEligibilityReadiness(status)).toEqual({
|
||||
hasUncommittedChanges: true,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { readMobileGitStatusResult } from '../session/mobile-diff-review-rpc'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
|
||||
// Refresh after a push when possible so readiness reflects the new upstream state.
|
||||
export async function readFreshGitStatus(
|
||||
worktreeId: string,
|
||||
fallback: MobileGitStatusResult | null,
|
||||
fallback: GitStatusResult | null,
|
||||
sendGitRequest: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
||||
): Promise<MobileGitStatusResult | null> {
|
||||
): Promise<GitStatusResult | null> {
|
||||
try {
|
||||
const fresh = await sendGitRequest<unknown>('git.status', { worktree: `id:${worktreeId}` })
|
||||
return readMobileGitStatusResult(fresh) ?? fallback
|
||||
@@ -15,7 +15,7 @@ export async function readFreshGitStatus(
|
||||
}
|
||||
}
|
||||
|
||||
export function getMobilePrEligibilityReadiness(status: MobileGitStatusResult | null): {
|
||||
export function getMobilePrEligibilityReadiness(status: GitStatusResult | null): {
|
||||
hasUncommittedChanges?: boolean
|
||||
hasUpstream?: boolean
|
||||
ahead?: number
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { GitStatusEntry } from '../../../src/shared/git-status-types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { buildMobileDiffReviewQueue } from '../session/mobile-diff-review-queue'
|
||||
import { buildMobileBranchCompareSection } from './mobile-branch-compare'
|
||||
import { buildMobileSourceControlSections, type MobileGitStatusEntry } from './mobile-git-status'
|
||||
import { buildMobileSourceControlSections } from './mobile-git-status'
|
||||
|
||||
const paths = [
|
||||
'file10.ts',
|
||||
@@ -37,7 +38,7 @@ const reviewInput = {
|
||||
reviewState: { version: 1 as const, files: {} }
|
||||
}
|
||||
const comparePath = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true })
|
||||
const conflictRank = (entry: MobileGitStatusEntry) =>
|
||||
const conflictRank = (entry: GitStatusEntry) =>
|
||||
entry.conflictStatus === 'unresolved' ? 0 : entry.conflictStatus === 'resolved_locally' ? 1 : 2
|
||||
|
||||
describe('mobile path sort collation', () => {
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
getMobilePrCreateBlockMessage,
|
||||
mobileRepoSelectorFromWorktreeId,
|
||||
resolveMobilePrPrefill,
|
||||
shouldPushBeforeMobilePrCreate,
|
||||
type MobilePrPrefill
|
||||
shouldPushBeforeMobilePrCreate
|
||||
} from './mobile-pr-create'
|
||||
import type { MobileHostedReviewPrefill } from './mobile-hosted-review-service'
|
||||
|
||||
function ok(result: unknown): RpcSuccess {
|
||||
return { id: 'r', ok: true, result, _meta: { runtimeId: 'rt' } }
|
||||
@@ -236,7 +236,8 @@ describe('mobile create form gating parity', () => {
|
||||
title: 'Add feature',
|
||||
body: '',
|
||||
canCreate: false,
|
||||
blockedReason: 'future_desktop_reason' as unknown as MobilePrPrefill['blockedReason']
|
||||
blockedReason:
|
||||
'future_desktop_reason' as unknown as MobileHostedReviewPrefill['blockedReason']
|
||||
})
|
||||
).toBe('This branch is not ready for a pull request yet.')
|
||||
})
|
||||
|
||||
@@ -5,17 +5,10 @@ import {
|
||||
mobileRepoSelectorFromWorktreeId,
|
||||
resolveMobileHostedReviewPrefill,
|
||||
shouldPushBeforeMobileHostedReviewCreate,
|
||||
type MobileHostedReviewCreateInput,
|
||||
type MobileHostedReviewCreateOutcome,
|
||||
type MobileHostedReviewEligibilityInput,
|
||||
type MobileHostedReviewPrefill
|
||||
} from './mobile-hosted-review-service'
|
||||
|
||||
export type MobilePrEligibilityInput = MobileHostedReviewEligibilityInput
|
||||
export type MobilePrPrefill = MobileHostedReviewPrefill
|
||||
export type MobilePrCreateInput = MobileHostedReviewCreateInput
|
||||
export type MobilePrCreateOutcome = MobileHostedReviewCreateOutcome
|
||||
|
||||
export {
|
||||
buildMobileHostedReviewCreateParams as buildMobilePrCreateParams,
|
||||
createMobileHostedReview as createMobilePr,
|
||||
@@ -25,8 +18,8 @@ export {
|
||||
}
|
||||
|
||||
export function getMobilePrCreateSuccessWarning(
|
||||
outcome: Extract<MobilePrCreateOutcome, { ok: true }>,
|
||||
provider: MobilePrPrefill['provider']
|
||||
outcome: Extract<MobileHostedReviewCreateOutcome, { ok: true }>,
|
||||
provider: MobileHostedReviewPrefill['provider']
|
||||
): string | undefined {
|
||||
const copy = hostedReviewCopy(provider)
|
||||
if (outcome.existing) {
|
||||
@@ -40,7 +33,7 @@ export function getMobilePrCreateSuccessWarning(
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getMobilePrCreateBlockMessage(prefill: MobilePrPrefill): string | null {
|
||||
export function getMobilePrCreateBlockMessage(prefill: MobileHostedReviewPrefill): string | null {
|
||||
const copy = hostedReviewCopy(prefill.provider)
|
||||
if (prefill.canCreate !== false || shouldPushBeforeMobileHostedReviewCreate(prefill)) {
|
||||
// Fail closed: only an accepted no-review lookup (`not_found`) may open
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MobileGitStagingArea } from './mobile-git-status'
|
||||
import type { GitStagingArea } from '../../../src/shared/git-status-types'
|
||||
|
||||
export type MobileReviewRouteArea = MobileGitStagingArea | 'branch'
|
||||
export type MobileReviewRouteArea = GitStagingArea | 'branch'
|
||||
|
||||
export type MobileReviewRouteTarget = {
|
||||
hostId: string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GitUpstreamStatus } from '../../../src/shared/git-status-types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { MobileGitUpstreamStatus } from './mobile-git-status'
|
||||
import {
|
||||
buildMobileSourceControlActions,
|
||||
type MobileSourceControlActionArgs
|
||||
@@ -30,7 +30,7 @@ function args(
|
||||
return {
|
||||
commitMessage: 'msg',
|
||||
stagedCount: 1,
|
||||
upstream: { hasUpstream: true, ahead: 0, behind: 0 } as MobileGitUpstreamStatus,
|
||||
upstream: { hasUpstream: true, ahead: 0, behind: 0 } as GitUpstreamStatus,
|
||||
upstreamKnown: true,
|
||||
busyAction: null,
|
||||
openingPath: null,
|
||||
@@ -67,14 +67,14 @@ describe('buildMobileSourceControlActions', () => {
|
||||
|
||||
it('disables fast-forward when ahead of upstream (would lose local commits)', () => {
|
||||
const actions = buildMobileSourceControlActions(
|
||||
args({ upstream: { hasUpstream: true, ahead: 2, behind: 3 } as MobileGitUpstreamStatus })
|
||||
args({ upstream: { hasUpstream: true, ahead: 2, behind: 3 } as GitUpstreamStatus })
|
||||
)
|
||||
expect(action(actions, 'Fast-forward')?.disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('enables fast-forward when behind and not ahead', () => {
|
||||
const actions = buildMobileSourceControlActions(
|
||||
args({ upstream: { hasUpstream: true, ahead: 0, behind: 3 } as MobileGitUpstreamStatus })
|
||||
args({ upstream: { hasUpstream: true, ahead: 0, behind: 3 } as GitUpstreamStatus })
|
||||
)
|
||||
expect(action(actions, 'Fast-forward')?.disabled).toBe(false)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MobileGitUpstreamStatus } from './mobile-git-status'
|
||||
import type { GitUpstreamStatus } from '../../../src/shared/git-status-types'
|
||||
|
||||
// Icon identifier resolved to a lucide component by the screen. Kept as a string
|
||||
// here so this module stays free of the native lucide import and unit-testable.
|
||||
@@ -27,7 +27,7 @@ export type MobileSourceControlAction = {
|
||||
export type MobileSourceControlActionArgs = {
|
||||
commitMessage: string
|
||||
stagedCount: number
|
||||
upstream: MobileGitUpstreamStatus | null
|
||||
upstream: GitUpstreamStatus | null
|
||||
upstreamKnown: boolean
|
||||
busyAction: string | null
|
||||
openingPath: string | null
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
buildMobileSourceControlPrimaryAction,
|
||||
type MobileSourceControlPrimaryActionArgs,
|
||||
type MobileSourceControlPrimaryActionHandlers
|
||||
} from './mobile-source-control-primary-action'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
|
||||
function handlers(): MobileSourceControlPrimaryActionHandlers {
|
||||
return {
|
||||
@@ -15,7 +15,7 @@ function handlers(): MobileSourceControlPrimaryActionHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
function status(overrides: Partial<MobileGitStatusResult> = {}): MobileGitStatusResult {
|
||||
function status(overrides: Partial<GitStatusResult> = {}): GitStatusResult {
|
||||
return {
|
||||
entries: [],
|
||||
conflictOperation: 'unknown',
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import {
|
||||
resolveSourceControlCommitAreaPrimaryActionDecision,
|
||||
type SourceControlCommitAreaPrimaryActionDecision,
|
||||
type SourceControlRemoteOpKind
|
||||
} from '../../../src/shared/source-control-primary-action-decision'
|
||||
import type { MobileGitBranchCompareResult } from './mobile-branch-compare'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
|
||||
type GitStep = { method: string; params?: Record<string, unknown> }
|
||||
|
||||
type MobileSourceControlPrimaryActionKind = SourceControlCommitAreaPrimaryActionDecision['kind']
|
||||
type MobileSourceControlPrimaryActionDecision = SourceControlCommitAreaPrimaryActionDecision
|
||||
type MobileSourceControlRemoteOpKind = SourceControlRemoteOpKind
|
||||
|
||||
export type MobileSourceControlPrimaryAction = {
|
||||
kind: MobileSourceControlPrimaryActionKind
|
||||
@@ -31,7 +29,7 @@ export type MobileSourceControlPrimaryActionHandlers = {
|
||||
}
|
||||
|
||||
export type MobileSourceControlPrimaryActionArgs = {
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
hasUnresolvedConflicts: boolean
|
||||
stageablePaths: readonly string[]
|
||||
stagedCount: number
|
||||
@@ -40,7 +38,7 @@ export type MobileSourceControlPrimaryActionArgs = {
|
||||
busyAction: string | null
|
||||
openingPath: string | null
|
||||
openingBranchPath: string | null
|
||||
branchCompareResult: MobileGitBranchCompareResult | null
|
||||
branchCompareResult: GitBranchCompareResult | null
|
||||
handlers: MobileSourceControlPrimaryActionHandlers
|
||||
}
|
||||
|
||||
@@ -88,9 +86,7 @@ function isMobileRemoteOperationActive(busyAction: string | null): boolean {
|
||||
return getInFlightRemoteOpKind(busyAction) !== null
|
||||
}
|
||||
|
||||
function getInFlightRemoteOpKind(
|
||||
busyAction: string | null
|
||||
): MobileSourceControlRemoteOpKind | null {
|
||||
function getInFlightRemoteOpKind(busyAction: string | null): SourceControlRemoteOpKind | null {
|
||||
switch (busyAction) {
|
||||
case 'push':
|
||||
case 'commit-push':
|
||||
@@ -127,7 +123,9 @@ function getMobileBranchCommitsAhead(
|
||||
return upstream?.hasUpstream ? upstream.ahead : undefined
|
||||
}
|
||||
|
||||
function getMobilePrimaryActionLabel(decision: MobileSourceControlPrimaryActionDecision): string {
|
||||
function getMobilePrimaryActionLabel(
|
||||
decision: SourceControlCommitAreaPrimaryActionDecision
|
||||
): string {
|
||||
if (decision.requiresForceWithLease) {
|
||||
return 'Force Push'
|
||||
}
|
||||
@@ -147,7 +145,9 @@ function getMobilePrimaryActionLabel(decision: MobileSourceControlPrimaryActionD
|
||||
}
|
||||
}
|
||||
|
||||
function getMobilePrimaryActionHint(decision: MobileSourceControlPrimaryActionDecision): string {
|
||||
function getMobilePrimaryActionHint(
|
||||
decision: SourceControlCommitAreaPrimaryActionDecision
|
||||
): string {
|
||||
switch (decision.titleIntent) {
|
||||
case 'commit_in_progress':
|
||||
return 'Commit in progress.'
|
||||
@@ -194,7 +194,7 @@ function getMobilePrimaryActionHint(decision: MobileSourceControlPrimaryActionDe
|
||||
}
|
||||
|
||||
function isLoadingDecision(
|
||||
decision: MobileSourceControlPrimaryActionDecision,
|
||||
decision: SourceControlCommitAreaPrimaryActionDecision,
|
||||
busyAction: string | null
|
||||
): boolean {
|
||||
switch (decision.kind) {
|
||||
@@ -219,7 +219,7 @@ function isLoadingDecision(
|
||||
}
|
||||
|
||||
async function runMobilePrimaryAction(
|
||||
decision: MobileSourceControlPrimaryActionDecision,
|
||||
decision: SourceControlCommitAreaPrimaryActionDecision,
|
||||
handlers: MobileSourceControlPrimaryActionHandlers
|
||||
): Promise<void> {
|
||||
switch (decision.kind) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type {
|
||||
GitFileStatus,
|
||||
GitStatusEntry,
|
||||
GitStatusResult
|
||||
} from '../../../src/shared/git-status-types'
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowDownUp,
|
||||
@@ -14,23 +19,18 @@ import { colors } from '../theme/mobile-theme'
|
||||
import type { MobileSourceControlActionIcon } from './mobile-source-control-actions'
|
||||
import type { MobileDiffLine } from '../session/mobile-diff-lines'
|
||||
import type { MobileHighlightedDiffLine } from '../session/mobile-file-syntax'
|
||||
import type {
|
||||
MobileGitBranchChangeEntry,
|
||||
MobileGitBranchCompareResult,
|
||||
MobileGitBranchCompareSummary
|
||||
} from './mobile-branch-compare'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import type { GitBranchChangeEntry } from '../../../src/shared/git-diff-compare-types'
|
||||
import type { GitBranchCompareSummary } from '../../../src/shared/git-diff-compare-types'
|
||||
import {
|
||||
canOpenMobileGitStatusEntry,
|
||||
isMobileGitDiscardableEntry,
|
||||
isMobileGitStageableEntry,
|
||||
type MobileGitFileStatus,
|
||||
type MobileGitStatusEntry,
|
||||
type MobileGitStatusResult
|
||||
isMobileGitStageableEntry
|
||||
} from './mobile-git-status'
|
||||
|
||||
export type ScreenState =
|
||||
| { kind: 'loading' }
|
||||
| { kind: 'ready'; status: MobileGitStatusResult }
|
||||
| { kind: 'ready'; status: GitStatusResult }
|
||||
| { kind: 'unavailable'; message: string }
|
||||
| { kind: 'error'; message: string }
|
||||
|
||||
@@ -49,7 +49,7 @@ export type StatusLoadInFlight = {
|
||||
export type GitRequestError = Error & { code?: string }
|
||||
export type GitCommitResult = { success: boolean; error?: string }
|
||||
|
||||
export type MobileGitStatusEntryView = MobileGitStatusEntry & {
|
||||
export type MobileGitStatusEntryView = GitStatusEntry & {
|
||||
canDiscard: boolean
|
||||
canOpen: boolean
|
||||
canStage: boolean
|
||||
@@ -61,7 +61,7 @@ export type MobileGitStatusEntryView = MobileGitStatusEntry & {
|
||||
// Decorate raw status entries with the row-level capability/action-id fields the
|
||||
// file list needs. Opener guards must use the same canOpen rule.
|
||||
export function buildMobileGitStatusEntryViews(
|
||||
entries: readonly MobileGitStatusEntry[]
|
||||
entries: readonly GitStatusEntry[]
|
||||
): MobileGitStatusEntryView[] {
|
||||
return entries.map((entry) => ({
|
||||
...entry,
|
||||
@@ -77,23 +77,23 @@ export function buildMobileGitStatusEntryViews(
|
||||
export type MobileBranchCompareState =
|
||||
| { kind: 'idle' }
|
||||
| { kind: 'loading' }
|
||||
| { kind: 'ready'; result: MobileGitBranchCompareResult }
|
||||
| { kind: 'ready'; result: GitBranchCompareResult }
|
||||
| { kind: 'error'; message: string }
|
||||
|
||||
export type MobileBranchEntryView = MobileGitBranchChangeEntry & {
|
||||
export type MobileBranchEntryView = GitBranchChangeEntry & {
|
||||
canOpen: boolean
|
||||
}
|
||||
|
||||
export type MobileBranchDiffPreviewState =
|
||||
| { kind: 'loading'; entry: MobileGitBranchChangeEntry }
|
||||
| { kind: 'loading'; entry: GitBranchChangeEntry }
|
||||
| {
|
||||
kind: 'ready'
|
||||
entry: MobileGitBranchChangeEntry
|
||||
summary: MobileGitBranchCompareSummary
|
||||
entry: GitBranchChangeEntry
|
||||
summary: GitBranchCompareSummary
|
||||
lines: MobileHighlightedDiffLine<MobileDiffLine>[]
|
||||
truncated: boolean
|
||||
}
|
||||
| { kind: 'error'; entry: MobileGitBranchChangeEntry; message: string }
|
||||
| { kind: 'error'; entry: GitBranchChangeEntry; message: string }
|
||||
|
||||
export type GitDiffTextResult = {
|
||||
kind: 'text'
|
||||
@@ -134,7 +134,7 @@ export function formatBranchLabel(branch: string | undefined, head: string | und
|
||||
return branch || head?.slice(0, 7) || 'No branch'
|
||||
}
|
||||
|
||||
export function statusColor(status: MobileGitFileStatus): string {
|
||||
export function statusColor(status: GitFileStatus): string {
|
||||
switch (status) {
|
||||
case 'added':
|
||||
case 'copied':
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { useCallback, type MutableRefObject } from 'react'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { triggerError } from '../platform/haptics'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
import type { LoadStatusOptions } from './mobile-source-control-screen-state'
|
||||
import {
|
||||
getMobileCommitFailureStagedEntries,
|
||||
@@ -24,7 +24,7 @@ type LoadStatus = (options?: LoadStatusOptions) => Promise<boolean>
|
||||
type Params = {
|
||||
client: RpcClient | null
|
||||
worktreeId: string
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
branchLabel: string
|
||||
commitMessage: string
|
||||
stagedEntries: MobileCommitFailureRecovery['stagedEntries']
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type { GitStatusResult, GitUpstreamStatus } from '../../../src/shared/git-status-types'
|
||||
import { useCallback } from 'react'
|
||||
import type { ConnectionState, RpcSuccess } from '../transport/types'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import {
|
||||
isMobileGitUnavailable,
|
||||
type MobileGitStatusResult,
|
||||
type MobileGitUpstreamStatus
|
||||
} from './mobile-git-status'
|
||||
import { isMobileGitUnavailable } from './mobile-git-status'
|
||||
import type { GitCommitResult, GitRequestError } from './mobile-source-control-screen-state'
|
||||
|
||||
type Params = {
|
||||
@@ -49,16 +46,16 @@ export function useMobileGitRequests({ client, connState, worktreeId }: Params)
|
||||
[sendGitRequest]
|
||||
)
|
||||
|
||||
const readUpstreamStatusForSync = useCallback(async (): Promise<MobileGitUpstreamStatus> => {
|
||||
const readUpstreamStatusForSync = useCallback(async (): Promise<GitUpstreamStatus> => {
|
||||
try {
|
||||
return await sendGitRequest<MobileGitUpstreamStatus>('git.upstreamStatus')
|
||||
return await sendGitRequest<GitUpstreamStatus>('git.upstreamStatus')
|
||||
} catch (err) {
|
||||
const code = err instanceof Error ? (err as GitRequestError).code : undefined
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
if (!isMobileGitUnavailable(code, message)) {
|
||||
throw err
|
||||
}
|
||||
const status = await sendGitRequest<MobileGitStatusResult>('git.status')
|
||||
const status = await sendGitRequest<GitStatusResult>('git.status')
|
||||
if (!status.upstreamStatus) {
|
||||
throw new Error('Branch status unavailable')
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { useMemo } from 'react'
|
||||
import { buildMobileCreatePrAction } from './mobile-create-pr-action'
|
||||
import { useMobileHostedReviewEligibility } from './use-mobile-hosted-review-eligibility'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
|
||||
type Params = {
|
||||
client: Parameters<typeof useMobileHostedReviewEligibility>[0]['client']
|
||||
connState: Parameters<typeof useMobileHostedReviewEligibility>[0]['connState']
|
||||
hostId: string
|
||||
worktreeId: string
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
hasUncommittedChanges: boolean
|
||||
busyAction: string | null
|
||||
createPr: (pushFirst: boolean) => void
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
@@ -8,10 +9,9 @@ import { gitBranchCompareRead, gitStatusHostPayloadRead } from './mobile-git-rea
|
||||
import {
|
||||
isMobileGitTransientRefreshError,
|
||||
isMobileGitUnavailableReply,
|
||||
readMobileGitRefusal,
|
||||
type MobileGitStatusResult
|
||||
readMobileGitRefusal
|
||||
} from './mobile-git-status'
|
||||
import type { MobileGitBranchCompareResult } from './mobile-branch-compare'
|
||||
import type { GitBranchCompareResult } from '../../../src/shared/git-diff-compare-types'
|
||||
import {
|
||||
SELECTOR_RETRY_COUNT,
|
||||
SELECTOR_RETRY_DELAY_MS,
|
||||
@@ -147,7 +147,7 @@ export function useMobileSourceControlLoaders(params: Params): MobileSourceContr
|
||||
setBranchCompareState({
|
||||
kind: 'ready',
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary.
|
||||
result: compared as MobileGitBranchCompareResult
|
||||
result: compared as GitBranchCompareResult
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
@@ -215,7 +215,7 @@ export function useMobileSourceControlLoaders(params: Params): MobileSourceContr
|
||||
const refusal = readMobileGitRefusal(reply)
|
||||
if (!refusal) {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary.
|
||||
const result = gitStatusHostPayloadRead.interpret(reply) as MobileGitStatusResult
|
||||
const result = gitStatusHostPayloadRead.interpret(reply) as GitStatusResult
|
||||
setScreenState({ kind: 'ready', status: result })
|
||||
void loadBranchCompare({ preserveReadyOnFailure: true })
|
||||
if (options?.clearActionErrorOnSuccess !== false) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GitStatusEntry } from '../../../src/shared/git-status-types'
|
||||
import { useCallback, useRef, useState, type MutableRefObject } from 'react'
|
||||
import { useRouter } from 'expo-router'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
@@ -9,16 +10,10 @@ import {
|
||||
highlightMobileDiffLines,
|
||||
resolveMobileSyntaxLanguage
|
||||
} from '../session/mobile-file-syntax'
|
||||
import {
|
||||
canOpenMobileBranchCompareDiff,
|
||||
type MobileGitBranchChangeEntry
|
||||
} from './mobile-branch-compare'
|
||||
import { canOpenMobileBranchCompareDiff } from './mobile-branch-compare'
|
||||
import type { GitBranchChangeEntry } from '../../../src/shared/git-diff-compare-types'
|
||||
import { gitBranchDiffRead } from './mobile-git-read-operations'
|
||||
import {
|
||||
canOpenMobileGitStatusEntry,
|
||||
isMobileGitUnavailableReply,
|
||||
type MobileGitStatusEntry
|
||||
} from './mobile-git-status'
|
||||
import { canOpenMobileGitStatusEntry, isMobileGitUnavailableReply } from './mobile-git-status'
|
||||
import { sourceFileDiffOpenRun, sourceFileOpenRun } from './mobile-source-file-open-operations'
|
||||
import { buildMobileReviewFileRoute } from './mobile-review-route'
|
||||
import { revealMobileSourceControlSessionDiff } from './reveal-mobile-source-control-session-diff'
|
||||
@@ -77,7 +72,7 @@ export function useMobileSourceControlOpeners(params: Params) {
|
||||
const openingBranchPathRef = useRef<string | null>(null)
|
||||
|
||||
const openFile = useCallback(
|
||||
async (entry: MobileGitStatusEntry) => {
|
||||
async (entry: GitStatusEntry) => {
|
||||
// Deletions are openable (pre-delete text/image via git.diff); only block
|
||||
// unresolved conflicts, matching canOpenMobileGitStatusEntry / row UI.
|
||||
if (!canOpenMobileGitStatusEntry(entry)) {
|
||||
@@ -202,7 +197,7 @@ export function useMobileSourceControlOpeners(params: Params) {
|
||||
)
|
||||
|
||||
const openBranchDiff = useCallback(
|
||||
async (entry: MobileGitBranchChangeEntry) => {
|
||||
async (entry: GitBranchChangeEntry) => {
|
||||
if (openingBranchPathRef.current || openingPathRef.current || busyActionRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GitStatusResult } from '../../../src/shared/git-status-types'
|
||||
import { useCallback, type MutableRefObject } from 'react'
|
||||
import { useRouter } from 'expo-router'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
@@ -7,7 +8,6 @@ import { useMobileSourceControlCommitRunners } from './use-mobile-source-control
|
||||
import { useMobileSourceControlActionSheetRunners } from './use-mobile-source-control-action-sheet-runners'
|
||||
import { useMobileCreatePrRunner } from './use-mobile-create-pr-runner'
|
||||
import type { RuntimeGitLocalBranches } from '../../../src/shared/runtime-types'
|
||||
import type { MobileGitStatusResult } from './mobile-git-status'
|
||||
import type { LoadStatusOptions } from './mobile-source-control-screen-state'
|
||||
import type {
|
||||
MobileCommitFailureRecovery,
|
||||
@@ -21,7 +21,7 @@ type Params = {
|
||||
client: RpcClient | null
|
||||
hostId: string
|
||||
worktreeId: string
|
||||
status: MobileGitStatusResult | null
|
||||
status: GitStatusResult | null
|
||||
branchLabel: string
|
||||
commitMessage: string
|
||||
stagedEntries: MobileCommitFailureRecovery['stagedEntries']
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GitStatusEntry } from '../../../src/shared/git-status-types'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useHostClient, useForceReconnect } from '../transport/client-context'
|
||||
@@ -21,8 +22,7 @@ import {
|
||||
countStagedEntries,
|
||||
countUnstagedEntries,
|
||||
getStageablePaths,
|
||||
getUnstageablePaths,
|
||||
type MobileGitStatusEntry
|
||||
getUnstageablePaths
|
||||
} from './mobile-git-status'
|
||||
import { getMobileCommitFailureStagedEntries } from './mobile-commit-failure-recovery'
|
||||
import { useMobileSourceControlCommitFailure } from './use-mobile-source-control-commit-failure'
|
||||
@@ -32,8 +32,6 @@ import {
|
||||
type MobileBranchEntryView
|
||||
} from './mobile-source-control-screen-state'
|
||||
|
||||
type MobileGitLocalBranches = RuntimeGitLocalBranches
|
||||
|
||||
export type MobileSourceControlStateParams = {
|
||||
hostId: string
|
||||
worktreeId: string
|
||||
@@ -67,10 +65,10 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara
|
||||
const [commitMessage, setCommitMessage] = useState('')
|
||||
const [generatingMessage, setGeneratingMessage] = useState(false)
|
||||
const [showBranchPicker, setShowBranchPicker] = useState(false)
|
||||
const [localBranches, setLocalBranches] = useState<MobileGitLocalBranches | null>(null)
|
||||
const [localBranches, setLocalBranches] = useState<RuntimeGitLocalBranches | null>(null)
|
||||
const [createdPrUrl, setCreatedPrUrl] = useState<string | null>(null)
|
||||
const [createdPrWarning, setCreatedPrWarning] = useState<string | null>(null)
|
||||
const [discardTarget, setDiscardTarget] = useState<MobileGitStatusEntry | null>(null)
|
||||
const [discardTarget, setDiscardTarget] = useState<GitStatusEntry | null>(null)
|
||||
const [showActionSheet, setShowActionSheet] = useState(false)
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
const keyboardLift = useMobileSourceControlKeyboardLift()
|
||||
|
||||
@@ -2,11 +2,8 @@ import type { TuiAgent } from '../../../src/shared/tui-agent'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktree-create-retry'
|
||||
import type { WorktreeCreateIdempotencyProbe } from './worktree-create-idempotency-policy'
|
||||
import {
|
||||
agentLaunchCreateFields,
|
||||
type WorkspaceCreateParams,
|
||||
type WorkspaceCreateSetupDecision
|
||||
} from './workspace-create-params'
|
||||
import { agentLaunchCreateFields, type WorkspaceCreateParams } from './workspace-create-params'
|
||||
import type { SetupDecision } from '../../../src/shared/worktree/create-types'
|
||||
|
||||
// The blank/named create path, extracted from NewWorktreeModal so the modal keeps
|
||||
// only the UI-coupled setup-trust flow. Assembles worktree.create params and
|
||||
@@ -17,7 +14,7 @@ export async function createBlankWorkspace(args: {
|
||||
baseName: string
|
||||
createdWithAgentId: TuiAgent | undefined
|
||||
comment: string | undefined
|
||||
setupDecision: WorkspaceCreateSetupDecision
|
||||
setupDecision: SetupDecision
|
||||
/** True when `baseName` is a generated creature name rather than one the user typed; only then
|
||||
* may the host retire it. */
|
||||
nameWasGenerated: boolean
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
MobileLinkedWorkItem,
|
||||
SmartNameSelection
|
||||
} from './mobile-composer-source-types'
|
||||
import type { WorkspaceCreateGitPushTarget } from './workspace-create-params'
|
||||
import type { GitPushTarget } from '../../../src/shared/worktree/types'
|
||||
|
||||
export function buildGitHubLinkedWorkItem(item: {
|
||||
type: 'issue' | 'pr'
|
||||
@@ -85,7 +85,7 @@ export function resolveComposerCreateSelection(args: {
|
||||
base: {
|
||||
baseBranch?: string
|
||||
compareBaseRef?: string
|
||||
pushTarget?: WorkspaceCreateGitPushTarget
|
||||
pushTarget?: GitPushTarget
|
||||
branchNameOverride?: string
|
||||
}
|
||||
branch: { refName: string; localBranchName: string } | null
|
||||
|
||||
@@ -6,10 +6,8 @@ export type GitHubCheckLike = {
|
||||
conclusion?: string | null
|
||||
}
|
||||
|
||||
export type GitHubCheckSummary = ProviderCheckSummary
|
||||
|
||||
// Why: reuse the desktop classifier verbatim — a second copy is what let mobile call `skipped`
|
||||
// unresolved while desktop called the same PR green.
|
||||
export function buildGitHubCheckSummary(checks: GitHubCheckLike[]): GitHubCheckSummary {
|
||||
export function buildGitHubCheckSummary(checks: GitHubCheckLike[]): ProviderCheckSummary {
|
||||
return summarizeProviderChecks(checks)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { GitHubProjectIdentity } from '../../../src/shared/github/project-identity'
|
||||
|
||||
export type GitHubProjectOwnerType = GitHubProjectIdentity['ownerType']
|
||||
export type GitHubProjectRef = GitHubProjectIdentity
|
||||
export type GitHubProjectSettings = {
|
||||
pinned: GitHubProjectRef[]
|
||||
recent: Array<GitHubProjectRef & { lastOpenedAt: string }>
|
||||
pinned: GitHubProjectIdentity[]
|
||||
recent: Array<GitHubProjectIdentity & { lastOpenedAt: string }>
|
||||
lastViewByProject: Record<string, { viewId: string }>
|
||||
activeProject: GitHubProjectRef | null
|
||||
activeProject: GitHubProjectIdentity | null
|
||||
}
|
||||
export type GitHubProjectSummary = GitHubProjectRef & {
|
||||
export type GitHubProjectSummary = GitHubProjectIdentity & {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
|
||||
@@ -3,14 +3,14 @@ import type {
|
||||
WorkspaceSourceLinkedItem,
|
||||
WorkspaceSourceSelection
|
||||
} from '../../../src/shared/new-workspace/workspace-source'
|
||||
import type { WorkspaceCreateGitPushTarget } from './workspace-create-params'
|
||||
import type { GitPushTarget } from '../../../src/shared/worktree/types'
|
||||
|
||||
export type { SmartNameMode }
|
||||
|
||||
export type ComposerBaseState = {
|
||||
baseBranch?: string
|
||||
compareBaseRef?: string
|
||||
pushTarget?: WorkspaceCreateGitPushTarget
|
||||
pushTarget?: GitPushTarget
|
||||
branchNameOverride?: string
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export type MobileComposerCreateSelection =
|
||||
item: MobileLinkedWorkItem
|
||||
baseBranch?: string
|
||||
compareBaseRef?: string
|
||||
pushTarget?: WorkspaceCreateGitPushTarget
|
||||
pushTarget?: GitPushTarget
|
||||
branchNameOverride?: string
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { LinearIssue } from './mobile-tasks-provider-detail-types'
|
||||
import type { LinearMobileIssue } from './mobile-tasks-provider-detail-types'
|
||||
import {
|
||||
sortLinearIssues,
|
||||
groupLinearIssues,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
vi.mock('./mobile-tasks-dependencies', () => import('../theme/mobile-theme'))
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
const issues: LinearIssue[] = Array.from({ length: 60 }, (_, i) => ({
|
||||
const issues: LinearMobileIssue[] = Array.from({ length: 60 }, (_, i) => ({
|
||||
id: `${i}`,
|
||||
identifier: ['ENG-10', 'ENG-2', 'Ä-1', 'Å-1', 'é-2', 'e\u0301-2', 'İ-3'][i % 7],
|
||||
title: 'Task',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { LinearIssue } from './mobile-tasks-provider-detail-types'
|
||||
import type { LinearMobileIssue } from './mobile-tasks-provider-detail-types'
|
||||
import type { LinearOrderBy } from './mobile-tasks-view-state-types'
|
||||
import { groupLinearIssues, sortLinearIssues } from './mobile-tasks-reviewer-linear'
|
||||
import { taskTime } from './mobile-tasks-item-mapping'
|
||||
@@ -8,7 +8,7 @@ import { getLinearPriorityRank } from './mobile-tasks-hosted-review'
|
||||
vi.mock('./mobile-tasks-dependencies', () => import('../theme/mobile-theme'))
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
const issues: LinearIssue[] = Array.from({ length: 60 }, (_, i) => ({
|
||||
const issues: LinearMobileIssue[] = Array.from({ length: 60 }, (_, i) => ({
|
||||
id: `${i}`,
|
||||
identifier: ['ENG-10', 'ENG-2', 'Ä-1', 'Å-1', 'é-2', 'e\u0301-2', 'İ-3'][i % 7],
|
||||
title: 'Task',
|
||||
@@ -21,7 +21,10 @@ const issues: LinearIssue[] = Array.from({ length: 60 }, (_, i) => ({
|
||||
state: { name: i % 2 ? 'Todo' : 'Done', type: 'started', color: '' },
|
||||
team: { id: `${i % 3}`, name: `Team ${i % 3}`, key: 'ENG' }
|
||||
}))
|
||||
function originalSort(input: readonly LinearIssue[], mode: LinearOrderBy): LinearIssue[] {
|
||||
function originalSort(
|
||||
input: readonly LinearMobileIssue[],
|
||||
mode: LinearOrderBy
|
||||
): LinearMobileIssue[] {
|
||||
return [...input].sort((a, b) => {
|
||||
if (mode === 'updated') {
|
||||
return taskTime(b.updatedAt) - taskTime(a.updatedAt)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { HostStackNavigationState } from '../navigation/host-stack-navigation'
|
||||
import {
|
||||
coordinateMobileTasksNavigation,
|
||||
mobileTasksHostRoute,
|
||||
navigateToMobileTasks,
|
||||
type MobileTasksNavigationState
|
||||
navigateToMobileTasks
|
||||
} from './mobile-task-navigation'
|
||||
|
||||
function navigationHarness(initialState: MobileTasksNavigationState) {
|
||||
function navigationHarness(initialState: HostStackNavigationState) {
|
||||
let stateListener = () => {}
|
||||
let state = initialState
|
||||
const unsubscribeState = vi.fn()
|
||||
@@ -20,7 +20,7 @@ function navigationHarness(initialState: MobileTasksNavigationState) {
|
||||
}
|
||||
return {
|
||||
navigation,
|
||||
setState(nextState: MobileTasksNavigationState) {
|
||||
setState(nextState: HostStackNavigationState) {
|
||||
state = nextState
|
||||
stateListener()
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
navigateToHostStackRoute,
|
||||
type HostStackHostRoute,
|
||||
type HostStackNavigationController,
|
||||
type HostStackNavigationState,
|
||||
type HostStackRootNavigation,
|
||||
type HostStackRouteTarget,
|
||||
type HostStackRouter,
|
||||
@@ -12,14 +11,7 @@ import {
|
||||
} from '../navigation/host-stack-navigation'
|
||||
import type { TaskProvider } from './mobile-task-providers'
|
||||
|
||||
export type MobileTasksHostRoute = HostStackHostRoute
|
||||
export type MobileTasksNavigationState = HostStackNavigationState
|
||||
export type MobileTasksRootNavigation = HostStackRootNavigation
|
||||
export type MobileTasksRouter = HostStackRouter
|
||||
export type MobileTasksNavigationController = HostStackNavigationController
|
||||
export type PendingMobileTasksNavigation = PendingHostStackNavigation
|
||||
|
||||
export function mobileTasksHostRoute(hostId: string): MobileTasksHostRoute {
|
||||
export function mobileTasksHostRoute(hostId: string): HostStackHostRoute {
|
||||
return hostStackHostRoute(hostId)
|
||||
}
|
||||
|
||||
@@ -34,11 +26,11 @@ export function mobileTasksRouteTarget(
|
||||
}
|
||||
|
||||
export function navigateToMobileTasks(
|
||||
navigation: MobileTasksRootNavigation,
|
||||
router: MobileTasksRouter,
|
||||
navigation: HostStackRootNavigation,
|
||||
router: HostStackRouter,
|
||||
hostId: string,
|
||||
provider?: TaskProvider
|
||||
): MobileTasksNavigationController {
|
||||
): HostStackNavigationController {
|
||||
return navigateToHostStackRoute(
|
||||
navigation,
|
||||
router,
|
||||
@@ -48,12 +40,12 @@ export function navigateToMobileTasks(
|
||||
}
|
||||
|
||||
export function coordinateMobileTasksNavigation(
|
||||
current: PendingMobileTasksNavigation | null,
|
||||
navigation: MobileTasksRootNavigation,
|
||||
router: MobileTasksRouter,
|
||||
current: PendingHostStackNavigation | null,
|
||||
navigation: HostStackRootNavigation,
|
||||
router: HostStackRouter,
|
||||
hostId: string,
|
||||
provider?: TaskProvider
|
||||
): PendingMobileTasksNavigation {
|
||||
): PendingHostStackNavigation {
|
||||
return coordinateHostStackNavigation(
|
||||
current,
|
||||
navigation,
|
||||
|
||||
@@ -89,11 +89,11 @@ export { parseGitHubProjectInput as parseProjectInput } from './github-project-r
|
||||
export type {
|
||||
GitHubProjectOwnerType,
|
||||
GitHubProjectPartialFailure,
|
||||
GitHubProjectRef,
|
||||
GitHubProjectSettings,
|
||||
GitHubProjectSummary,
|
||||
GitHubProjectViewSummary
|
||||
} from './github-project-reference'
|
||||
export type { GitHubProjectIdentity } from '../../../src/shared/github/project-identity'
|
||||
export {
|
||||
extractGitHubIssueSourceFallback,
|
||||
extractGitHubIssueSourceError
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
GitHubWorkItem,
|
||||
GitLabTodo,
|
||||
GitLabWorkItem,
|
||||
LinearIssue,
|
||||
LinearMobileIssue,
|
||||
RepoSummary
|
||||
} from './mobile-tasks-provider-detail-types'
|
||||
|
||||
@@ -288,7 +288,7 @@ export async function mapWithConcurrency<T, R>(
|
||||
return results
|
||||
}
|
||||
|
||||
export function createLinearTask(issue: LinearIssue): TaskItem {
|
||||
export function createLinearTask(issue: LinearMobileIssue): TaskItem {
|
||||
return {
|
||||
key: `linear:${issue.workspaceId ?? 'workspace'}:${issue.id}`,
|
||||
provider: 'linear',
|
||||
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
TaskSort
|
||||
} from './mobile-tasks-view-state-types'
|
||||
import type { ActionableTaskItem } from './mobile-tasks-project-workspace-types'
|
||||
import type { DetailComment, LinearIssue } from './mobile-tasks-provider-detail-types'
|
||||
import type { DetailComment, LinearMobileIssue } from './mobile-tasks-provider-detail-types'
|
||||
|
||||
export const PROVIDER_OPTIONS: PickerOption<TaskProvider>[] = [
|
||||
{
|
||||
@@ -180,12 +180,12 @@ export type LinearIssueSection = {
|
||||
key: string
|
||||
label: string
|
||||
color: string
|
||||
issues: LinearIssue[]
|
||||
issues: LinearMobileIssue[]
|
||||
}
|
||||
|
||||
export type LinearListEntry =
|
||||
| { type: 'section'; section: LinearIssueSection }
|
||||
| { type: 'issue'; issue: LinearIssue }
|
||||
| { type: 'issue'; issue: LinearMobileIssue }
|
||||
|
||||
export const PROJECT_VIEW_DEFAULT_SORT = '__view_default__'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
GitHubWorkItem,
|
||||
GitLabWorkItem,
|
||||
GitLabTodo,
|
||||
LinearIssue,
|
||||
LinearMobileIssue,
|
||||
SetupDecision
|
||||
} from './mobile-tasks-provider-detail-types'
|
||||
import type { WorkspaceAgentChoice, SparsePreset } from './mobile-tasks-dependencies'
|
||||
@@ -43,7 +43,7 @@ export type TaskItem =
|
||||
subtitle: string
|
||||
status: string
|
||||
updatedAt: string
|
||||
source: LinearIssue
|
||||
source: LinearMobileIssue
|
||||
}
|
||||
|
||||
export type ActionableTaskItem = Exclude<TaskItem, { provider: 'gitlabTodo' }>
|
||||
|
||||
@@ -151,7 +151,7 @@ export type LinearIssueChild = {
|
||||
url: string
|
||||
}
|
||||
|
||||
export type LinearIssue = LinearMobileIssue
|
||||
export type { LinearMobileIssue }
|
||||
|
||||
export type LinearState = {
|
||||
id: string
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
GitHubAssignableUser,
|
||||
GitHubPRReviewSummary,
|
||||
GitHubRepoSources,
|
||||
LinearIssue,
|
||||
LinearMobileIssue,
|
||||
LinearTeam
|
||||
} from './mobile-tasks-provider-detail-types'
|
||||
|
||||
@@ -84,9 +84,9 @@ export function issueSourceSlug(source: GitHubOwnerRepo | null | undefined): str
|
||||
}
|
||||
|
||||
export function sortLinearIssues(
|
||||
issues: readonly LinearIssue[],
|
||||
issues: readonly LinearMobileIssue[],
|
||||
orderBy: LinearOrderBy
|
||||
): LinearIssue[] {
|
||||
): LinearMobileIssue[] {
|
||||
if (issues.length < 2) {
|
||||
return [...issues]
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export function sortLinearIssues(
|
||||
}
|
||||
|
||||
export function getLinearIssueGroup(
|
||||
issue: LinearIssue,
|
||||
issue: LinearMobileIssue,
|
||||
groupBy: LinearGroupBy
|
||||
): {
|
||||
key: string
|
||||
@@ -135,7 +135,7 @@ export function getLinearIssueGroup(
|
||||
}
|
||||
|
||||
export function groupLinearIssues(
|
||||
issues: LinearIssue[],
|
||||
issues: LinearMobileIssue[],
|
||||
groupBy: LinearGroupBy,
|
||||
orderBy: LinearOrderBy
|
||||
): LinearIssueSection[] {
|
||||
@@ -144,14 +144,14 @@ export function groupLinearIssues(
|
||||
|
||||
/** The caller must sort issues by its selected order before grouping. */
|
||||
export function groupSortedLinearIssues(
|
||||
issues: readonly LinearIssue[],
|
||||
issues: readonly LinearMobileIssue[],
|
||||
groupBy: LinearGroupBy
|
||||
): LinearIssueSection[] {
|
||||
return groupOrderedLinearIssues([...issues], groupBy)
|
||||
}
|
||||
|
||||
function groupOrderedLinearIssues(
|
||||
sorted: LinearIssue[],
|
||||
sorted: LinearMobileIssue[],
|
||||
groupBy: LinearGroupBy
|
||||
): LinearIssueSection[] {
|
||||
if (groupBy === 'none') {
|
||||
@@ -159,7 +159,7 @@ function groupOrderedLinearIssues(
|
||||
}
|
||||
const sections = new Map<
|
||||
string,
|
||||
{ key: string; label: string; color: string; issues: LinearIssue[] }
|
||||
{ key: string; label: string; color: string; issues: LinearMobileIssue[] }
|
||||
>()
|
||||
for (const issue of sorted) {
|
||||
const group = getLinearIssueGroup(issue, groupBy)
|
||||
@@ -174,7 +174,7 @@ function groupOrderedLinearIssues(
|
||||
}
|
||||
|
||||
export function linearIssueSecondaryParts(
|
||||
issue: LinearIssue,
|
||||
issue: LinearMobileIssue,
|
||||
displayProperties: ReadonlySet<LinearDisplayProperty>
|
||||
): string[] {
|
||||
const parts = [issue.identifier]
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
TuiAgent,
|
||||
TaskProvider,
|
||||
GitHubProjectSettings,
|
||||
GitHubProjectRef
|
||||
GitHubProjectIdentity
|
||||
} from './mobile-tasks-dependencies'
|
||||
import type { GitHubProjectSortDirection } from '../../../src/shared/github/project-types'
|
||||
|
||||
@@ -159,7 +159,7 @@ export type GitHubProjectRow = {
|
||||
}
|
||||
|
||||
export type GitHubProjectTable = {
|
||||
project: GitHubProjectRef & {
|
||||
project: GitHubProjectIdentity & {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
agentLaunchCreateFields,
|
||||
buildTaskWorkspaceCreateParams,
|
||||
type WorkspaceCreateParams,
|
||||
type WorkspaceCreateSetupDecision,
|
||||
type WorkspaceCreateTaskItem
|
||||
} from './workspace-create-params'
|
||||
import type { SetupDecision } from '../../../src/shared/worktree/create-types'
|
||||
import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktree-create-retry'
|
||||
import type { WorktreeCreateIdempotencyProbe } from './worktree-create-idempotency-policy'
|
||||
|
||||
@@ -26,7 +26,7 @@ export type CreateWorkspaceFromComposerArgs = {
|
||||
client: RpcClient
|
||||
selection: MobileComposerCreateSelection
|
||||
targetRepoId: string
|
||||
setupDecision: WorkspaceCreateSetupDecision
|
||||
setupDecision: SetupDecision
|
||||
agent: WorkspaceCreateAgentBundle
|
||||
workspaceName: string | undefined
|
||||
nameIsAutoManaged?: boolean
|
||||
@@ -89,7 +89,7 @@ async function createWorkItemWorkspace(args: {
|
||||
client: RpcClient
|
||||
selection: Extract<MobileComposerCreateSelection, { kind: 'work-item' }>
|
||||
targetRepoId: string
|
||||
setupDecision: WorkspaceCreateSetupDecision
|
||||
setupDecision: SetupDecision
|
||||
agent: WorkspaceCreateAgentBundle
|
||||
workspaceName: string | undefined
|
||||
nameIsAutoManaged?: boolean
|
||||
@@ -148,7 +148,7 @@ async function createBranchWorkspace(args: {
|
||||
client: RpcClient
|
||||
selection: Extract<MobileComposerCreateSelection, { kind: 'branch' }>
|
||||
targetRepoId: string
|
||||
setupDecision: WorkspaceCreateSetupDecision
|
||||
setupDecision: SetupDecision
|
||||
agent: WorkspaceCreateAgentBundle
|
||||
workspaceName: string | undefined
|
||||
nameIsAutoManaged?: boolean
|
||||
@@ -235,7 +235,7 @@ async function createNewBranchWorkspace(args: {
|
||||
client: RpcClient
|
||||
selection: Extract<MobileComposerCreateSelection, { kind: 'new-branch' }>
|
||||
targetRepoId: string
|
||||
setupDecision: WorkspaceCreateSetupDecision
|
||||
setupDecision: SetupDecision
|
||||
agent: WorkspaceCreateAgentBundle
|
||||
workspaceName: string | undefined
|
||||
nameIsAutoManaged?: boolean
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type GitHubDetailCheck,
|
||||
type GitHubDetailFile,
|
||||
type GitHubPRReviewSummary,
|
||||
type LinearIssue,
|
||||
type LinearMobileIssue,
|
||||
type TaskItem,
|
||||
createLinearTask,
|
||||
isSuccess
|
||||
@@ -207,7 +207,7 @@ export function useMobileTasksItemDetailLoading(model: ItemDetailMetadataEffects
|
||||
if (!isSuccess(issueResponse)) {
|
||||
throw new Error(issueResponse.error.message)
|
||||
}
|
||||
const issue = issueResponse.result as LinearIssue | null
|
||||
const issue = issueResponse.result as LinearMobileIssue | null
|
||||
const comments = isSuccess(commentsResponse)
|
||||
? ((commentsResponse.result as DetailComment[]) ?? [])
|
||||
: []
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { GithubReplyMergeActionsModel } from './use-mobile-tasks-github-rep
|
||||
import { useCallback } from './mobile-tasks-dependencies'
|
||||
import {
|
||||
type DetailComment,
|
||||
type LinearIssue,
|
||||
type LinearMobileIssue,
|
||||
type LinearIssueChild,
|
||||
type TaskItem,
|
||||
createLinearTask,
|
||||
@@ -87,7 +87,7 @@ export function useMobileTasksLinearItemActions(model: GithubReplyMergeActionsMo
|
||||
if (!isSuccess(response)) {
|
||||
throw new Error(response.error.message)
|
||||
}
|
||||
const issue = response.result as LinearIssue | null
|
||||
const issue = response.result as LinearMobileIssue | null
|
||||
if (!issue) {
|
||||
throw new Error('Sub-issue not found')
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TaskPaginationActionsModel } from './use-mobile-tasks-task-paginat
|
||||
import {
|
||||
type GitHubProjectOwnerType,
|
||||
type GitHubProjectPartialFailure,
|
||||
type GitHubProjectRef,
|
||||
type GitHubProjectIdentity,
|
||||
type GitHubProjectSettings,
|
||||
type GitHubProjectSummary,
|
||||
type GitHubProjectViewSummary,
|
||||
@@ -69,7 +69,7 @@ export function useMobileTasksProjectLoadingActions(model: TaskPaginationActions
|
||||
}, [client, connState, tasksSupported])
|
||||
|
||||
const loadGitHubProjectViews = useCallback(
|
||||
async (project: GitHubProjectRef): Promise<GitHubProjectViewSummary[]> => {
|
||||
async (project: GitHubProjectIdentity): Promise<GitHubProjectViewSummary[]> => {
|
||||
if (!client || connState !== 'connected' || !tasksSupported || !taskStateHydrated) {
|
||||
return []
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export function useMobileTasksProjectLoadingActions(model: TaskPaginationActions
|
||||
)
|
||||
|
||||
const commitGitHubProjectView = useCallback(
|
||||
(project: GitHubProjectRef, viewId: string): void => {
|
||||
(project: GitHubProjectIdentity, viewId: string): void => {
|
||||
const projectKey = githubProjectKey(project)
|
||||
const nextSettings: GitHubProjectSettings = {
|
||||
...githubProjectSettings,
|
||||
@@ -186,7 +186,10 @@ export function useMobileTasksProjectLoadingActions(model: TaskPaginationActions
|
||||
)
|
||||
|
||||
const selectGitHubProject = useCallback(
|
||||
async (project: GitHubProjectRef, options: { viewNumber?: number } = {}): Promise<void> => {
|
||||
async (
|
||||
project: GitHubProjectIdentity,
|
||||
options: { viewNumber?: number } = {}
|
||||
): Promise<void> => {
|
||||
if (!tasksSupported || !taskStateHydrated) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type {
|
||||
LinearGroupBy,
|
||||
LinearIssue,
|
||||
LinearMobileIssue,
|
||||
LinearIssueSection,
|
||||
LinearOrderBy,
|
||||
LinearViewMode,
|
||||
@@ -35,11 +35,15 @@ vi.mock('./mobile-tasks-legacy-foundation', async () => {
|
||||
return {
|
||||
...options,
|
||||
...linear,
|
||||
groupLinearIssues: (issues: LinearIssue[], groupBy: LinearGroupBy, orderBy: LinearOrderBy) => {
|
||||
groupLinearIssues: (
|
||||
issues: LinearMobileIssue[],
|
||||
groupBy: LinearGroupBy,
|
||||
orderBy: LinearOrderBy
|
||||
) => {
|
||||
groupingInputSizes.push(issues.length)
|
||||
return linear.groupLinearIssues(issues, groupBy, orderBy)
|
||||
},
|
||||
groupSortedLinearIssues: (issues: readonly LinearIssue[], groupBy: LinearGroupBy) => {
|
||||
groupSortedLinearIssues: (issues: readonly LinearMobileIssue[], groupBy: LinearGroupBy) => {
|
||||
groupingInputSizes.push(issues.length)
|
||||
return linear.groupSortedLinearIssues(issues, groupBy)
|
||||
}
|
||||
@@ -70,7 +74,7 @@ const ORDERINGS: LinearOrderBy[] = ['priority', 'updated', 'identifier']
|
||||
|
||||
/** Deterministic issues: every field is a pure function of the index, so grouping,
|
||||
* ordering and comparison counts repeat exactly across runs. */
|
||||
function makeIssue(index: number): LinearIssue {
|
||||
function makeIssue(index: number): LinearMobileIssue {
|
||||
const state = STATES[(index * 3) % STATES.length]!
|
||||
const team = TEAMS[(index * 5) % TEAMS.length]!
|
||||
return {
|
||||
@@ -190,7 +194,7 @@ function current(): Projection {
|
||||
|
||||
/** The pre-change board memo, kept verbatim as the parity and count oracle. */
|
||||
function legacyProjection(input: ProbeInput): {
|
||||
issuesForView: LinearIssue[]
|
||||
issuesForView: LinearMobileIssue[]
|
||||
listSections: LinearIssueSection[]
|
||||
boardSections: LinearIssueSection[]
|
||||
} {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { RouteAndItemStateModel } from './use-mobile-tasks-route-and-item-s
|
||||
import {
|
||||
type BaseRefSearchResult,
|
||||
type GitHubProjectPartialFailure,
|
||||
type GitHubProjectRef,
|
||||
type GitHubProjectIdentity,
|
||||
type GitHubProjectSettings,
|
||||
type GitHubProjectSummary,
|
||||
type GitHubProjectViewSummary,
|
||||
@@ -120,7 +120,7 @@ export function useMobileTasksWorkspaceAndProjectState(model: RouteAndItemStateM
|
||||
const [showGitHubProjectSortPicker, setShowGitHubProjectSortPicker] = useState(false)
|
||||
const [showGitHubProjectFieldsPicker, setShowGitHubProjectFieldsPicker] = useState(false)
|
||||
const [pendingGitHubProjectViewSelection, setPendingGitHubProjectViewSelection] =
|
||||
useState<GitHubProjectRef | null>(null)
|
||||
useState<GitHubProjectIdentity | null>(null)
|
||||
const [projectRowItem, setProjectRowItem] = useState<GitHubProjectRow | null>(null)
|
||||
const [projectRowDetail, setProjectRowDetail] = useState<DetailPayload | null>(null)
|
||||
const [projectRowDetailLoading, setProjectRowDetailLoading] = useState(false)
|
||||
|
||||
@@ -9,13 +9,9 @@ import { getWorkspaceSourceName } from '../../../src/shared/new-workspace/worksp
|
||||
import { resolveMobileWorkspaceCreateName } from './mobile-workspace-name'
|
||||
import type { WorkspaceAgentChoice } from './workspace-agent-selection'
|
||||
|
||||
export type WorkspaceCreateSetupDecision = SetupDecision
|
||||
export type WorkspaceCreateSparseCheckout = CreateSparseCheckoutRequest
|
||||
export type WorkspaceCreateGitPushTarget = GitPushTarget
|
||||
|
||||
export type WorkspaceCreateHostedStartPoint = {
|
||||
baseBranch: string
|
||||
pushTarget?: WorkspaceCreateGitPushTarget
|
||||
pushTarget?: GitPushTarget
|
||||
}
|
||||
|
||||
type WorkspaceCreateGitHubItem = {
|
||||
@@ -78,15 +74,15 @@ export function agentLaunchCreateFields(agentId: TuiAgent | undefined): {
|
||||
export function buildTaskWorkspaceCreateParams(args: {
|
||||
item: WorkspaceCreateTaskItem
|
||||
targetRepoId: string
|
||||
setupDecision: WorkspaceCreateSetupDecision
|
||||
setupDecision: SetupDecision
|
||||
agent?: WorkspaceAgentChoice
|
||||
workspaceName?: string
|
||||
note?: string
|
||||
baseBranch?: string
|
||||
compareBaseRef?: string
|
||||
branchNameOverride?: string
|
||||
pushTarget?: WorkspaceCreateGitPushTarget
|
||||
sparseCheckout?: WorkspaceCreateSparseCheckout
|
||||
pushTarget?: GitPushTarget
|
||||
sparseCheckout?: CreateSparseCheckoutRequest
|
||||
hostedStartPoint?: WorkspaceCreateHostedStartPoint
|
||||
nameIsAutoManaged?: boolean
|
||||
}): WorkspaceCreateParams {
|
||||
|
||||
@@ -16,168 +16,16 @@ import { dispatchTerminalWebViewNotification } from './terminal-webview-notifica
|
||||
import { routeTerminalQueryReply } from './terminal-webview-query-reply-routing'
|
||||
import { createTerminalWriteCoalescer } from './terminal-write-coalescer'
|
||||
|
||||
type Props = TerminalWebViewProps
|
||||
|
||||
export type { TerminalWebViewHandle } from './terminal-webview-contract'
|
||||
|
||||
export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function TerminalWebView(
|
||||
{
|
||||
style,
|
||||
terminalTheme,
|
||||
textScale = 1,
|
||||
onWebReady,
|
||||
onEngineError,
|
||||
onSelectionMode,
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
onModesChanged,
|
||||
onKeyboardAvoidanceMetrics,
|
||||
onHaptic,
|
||||
onTerminalInput,
|
||||
onTerminalQueryReply,
|
||||
onTerminalTap,
|
||||
onFileTap,
|
||||
onOpenUrl,
|
||||
onTextScaleChange
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const webViewRef = useRef<WebView>(null)
|
||||
const isWebReadyRef = useRef(false)
|
||||
const pendingMessages = useMemo(() => createTerminalWebViewPendingMessages(), [])
|
||||
const messageIdRef = useRef(0)
|
||||
const pendingPingIdRef = useRef<number | null>(null)
|
||||
const terminalThemeKey = useMemo(() => JSON.stringify(terminalTheme ?? null), [terminalTheme])
|
||||
const measureResolveRef = useRef<
|
||||
((result: { cols: number; rows: number } | null) => void) | null
|
||||
>(null)
|
||||
// Why: each init() call posts 'init' to the WebView and arms a fresh
|
||||
// ready promise. WebView's init() rAF chain ends with a 'ready' notify
|
||||
// that resolves it. measureFitDimensions awaits this so it doesn't
|
||||
// race ahead of term.open() / renderService population.
|
||||
const readyPromiseRef = useRef<Promise<void> | null>(null)
|
||||
const readyResolveRef = useRef<(() => void) | null>(null)
|
||||
const { clearEngineError, engineError, reportEngineError, reportNativeEngineError } =
|
||||
useTerminalWebViewEngineErrorState(onEngineError)
|
||||
const { armWebReadyWatchdog, clearWebReadyWatchdog } = useTerminalWebReadyWatchdog(
|
||||
isWebReadyRef,
|
||||
reportEngineError
|
||||
)
|
||||
|
||||
const sendToWebView = useCallback((msg: TerminalWebViewCommand) => {
|
||||
messageIdRef.current += 1
|
||||
const id = messageIdRef.current
|
||||
webViewRef.current?.postMessage(JSON.stringify({ ...msg, id }))
|
||||
return id
|
||||
}, [])
|
||||
|
||||
const flushPendingMessages = useCallback(() => {
|
||||
pendingMessages.flush(sendToWebView)
|
||||
}, [pendingMessages, sendToWebView])
|
||||
|
||||
const postMessage = useCallback(
|
||||
(msg: TerminalWebViewCommand) => {
|
||||
if (!isWebReadyRef.current) {
|
||||
pendingMessages.queue(msg)
|
||||
return
|
||||
}
|
||||
sendToWebView(msg)
|
||||
},
|
||||
[pendingMessages, sendToWebView]
|
||||
)
|
||||
|
||||
// Why: a busy PTY delivers ~200 stream frames/s; coalescing here collapses the
|
||||
// per-frame bridge + WebKit IPC + paint cost that runs the phone hot (#9302).
|
||||
const writeCoalescer = useMemo(
|
||||
() => createTerminalWriteCoalescer((data) => postMessage({ type: 'write', data })),
|
||||
[postMessage]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
writeCoalescer.clear()
|
||||
}
|
||||
}, [writeCoalescer])
|
||||
|
||||
const confirmWebReady = useCallback(
|
||||
(notifyParent: boolean) => {
|
||||
pendingPingIdRef.current = null
|
||||
isWebReadyRef.current = true
|
||||
clearWebReadyWatchdog()
|
||||
clearEngineError()
|
||||
if (notifyParent) {
|
||||
onWebReady?.()
|
||||
}
|
||||
// Why: reload clears queued commands, so readiness must always restore the
|
||||
// native-selected theme even when its value did not change in React.
|
||||
sendToWebView({ type: 'set-theme', terminalTheme })
|
||||
flushPendingMessages()
|
||||
},
|
||||
[
|
||||
clearEngineError,
|
||||
clearWebReadyWatchdog,
|
||||
flushPendingMessages,
|
||||
export const TerminalWebView = forwardRef<TerminalWebViewHandle, TerminalWebViewProps>(
|
||||
function TerminalWebView(
|
||||
{
|
||||
style,
|
||||
terminalTheme,
|
||||
textScale = 1,
|
||||
onWebReady,
|
||||
sendToWebView,
|
||||
terminalTheme
|
||||
]
|
||||
)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: WebViewMessageEvent) => {
|
||||
let msg: Record<string, unknown>
|
||||
try {
|
||||
msg = JSON.parse(event.nativeEvent.data) as Record<string, unknown>
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
routeTerminalQueryReply(msg, onTerminalQueryReply)
|
||||
|
||||
if (msg.type === 'web-ready') {
|
||||
confirmWebReady(true)
|
||||
} else if (
|
||||
msg.type === 'pong' &&
|
||||
typeof msg.pingId === 'number' &&
|
||||
msg.pingId === pendingPingIdRef.current
|
||||
) {
|
||||
confirmWebReady(false)
|
||||
} else if (msg.type === 'ready') {
|
||||
// Why: the WebView's init() rAF chain has run — term is open,
|
||||
// renderService is populated, first paint has happened. Resolve
|
||||
// any pending awaitReady() so a queued measure can now safely
|
||||
// read cell dims.
|
||||
const resolve = readyResolveRef.current
|
||||
readyResolveRef.current = null
|
||||
readyPromiseRef.current = null
|
||||
resolve?.()
|
||||
} else if (msg.type === 'measure-result') {
|
||||
const resolve = measureResolveRef.current
|
||||
measureResolveRef.current = null
|
||||
if (resolve) {
|
||||
const cols = typeof msg.cols === 'number' ? msg.cols : null
|
||||
const rows = typeof msg.rows === 'number' ? msg.rows : null
|
||||
resolve(cols && rows && cols >= 20 && rows >= 8 ? { cols, rows } : null)
|
||||
}
|
||||
} else {
|
||||
dispatchTerminalWebViewNotification(msg, {
|
||||
reportEngineError,
|
||||
onSelectionMode,
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
onModesChanged,
|
||||
onKeyboardAvoidanceMetrics,
|
||||
onHaptic,
|
||||
onTerminalInput,
|
||||
onTerminalTap,
|
||||
onFileTap,
|
||||
onOpenUrl,
|
||||
onTextScaleChange
|
||||
})
|
||||
}
|
||||
},
|
||||
[
|
||||
confirmWebReady,
|
||||
reportEngineError,
|
||||
onEngineError,
|
||||
onSelectionMode,
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
@@ -190,207 +38,359 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
|
||||
onFileTap,
|
||||
onOpenUrl,
|
||||
onTextScaleChange
|
||||
]
|
||||
)
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const webViewRef = useRef<WebView>(null)
|
||||
const isWebReadyRef = useRef(false)
|
||||
const pendingMessages = useMemo(() => createTerminalWebViewPendingMessages(), [])
|
||||
const messageIdRef = useRef(0)
|
||||
const pendingPingIdRef = useRef<number | null>(null)
|
||||
const terminalThemeKey = useMemo(() => JSON.stringify(terminalTheme ?? null), [terminalTheme])
|
||||
const measureResolveRef = useRef<
|
||||
((result: { cols: number; rows: number } | null) => void) | null
|
||||
>(null)
|
||||
// Why: each init() call posts 'init' to the WebView and arms a fresh
|
||||
// ready promise. WebView's init() rAF chain ends with a 'ready' notify
|
||||
// that resolves it. measureFitDimensions awaits this so it doesn't
|
||||
// race ahead of term.open() / renderService population.
|
||||
const readyPromiseRef = useRef<Promise<void> | null>(null)
|
||||
const readyResolveRef = useRef<(() => void) | null>(null)
|
||||
const { clearEngineError, engineError, reportEngineError, reportNativeEngineError } =
|
||||
useTerminalWebViewEngineErrorState(onEngineError)
|
||||
const { armWebReadyWatchdog, clearWebReadyWatchdog } = useTerminalWebReadyWatchdog(
|
||||
isWebReadyRef,
|
||||
reportEngineError
|
||||
)
|
||||
|
||||
const handleLoadStart = useCallback(() => {
|
||||
isWebReadyRef.current = false
|
||||
pendingPingIdRef.current = null
|
||||
armWebReadyWatchdog()
|
||||
// Why: messages queued for a previous WebView generation are stale after a reload;
|
||||
// dropping them avoids replaying terminal chunks before the next init snapshot.
|
||||
pendingMessages.clear()
|
||||
writeCoalescer.clear()
|
||||
}, [armWebReadyWatchdog, pendingMessages, writeCoalescer])
|
||||
const sendToWebView = useCallback((msg: TerminalWebViewCommand) => {
|
||||
messageIdRef.current += 1
|
||||
const id = messageIdRef.current
|
||||
webViewRef.current?.postMessage(JSON.stringify({ ...msg, id }))
|
||||
return id
|
||||
}, [])
|
||||
|
||||
const handleReload = useCallback(() => {
|
||||
clearEngineError()
|
||||
webViewRef.current?.reload()
|
||||
}, [clearEngineError])
|
||||
const flushPendingMessages = useCallback(() => {
|
||||
pendingMessages.flush(sendToWebView)
|
||||
}, [pendingMessages, sendToWebView])
|
||||
|
||||
const handleContentProcessDidTerminate = useCallback(() => {
|
||||
// Why: WKWebView content-process loss is recoverable; stale commands belong
|
||||
// to the dead document and the replacement must prove readiness before replay.
|
||||
isWebReadyRef.current = false
|
||||
pendingPingIdRef.current = null
|
||||
pendingMessages.clear()
|
||||
writeCoalescer.clear()
|
||||
clearEngineError()
|
||||
armWebReadyWatchdog()
|
||||
webViewRef.current?.reload()
|
||||
}, [armWebReadyWatchdog, clearEngineError, pendingMessages, writeCoalescer])
|
||||
|
||||
useEffect(() => {
|
||||
postMessage({ type: 'set-theme', terminalTheme })
|
||||
}, [postMessage, terminalThemeKey, terminalTheme])
|
||||
|
||||
// Why: live-apply text-size changes to an already-mounted terminal (the pane
|
||||
// stays alive while the user visits Settings), so no terminal reload is needed.
|
||||
useEffect(() => {
|
||||
postMessage({ type: 'set-font-scale', fontScale: textScale })
|
||||
}, [postMessage, textScale])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
prepareForForegroundRecovery() {
|
||||
if (Platform.OS !== 'ios') {
|
||||
const postMessage = useCallback(
|
||||
(msg: TerminalWebViewCommand) => {
|
||||
if (!isWebReadyRef.current) {
|
||||
pendingMessages.queue(msg)
|
||||
return
|
||||
}
|
||||
// Why: direct ping is the only command allowed through while readiness is
|
||||
// invalid; init/write commands queue until this exact document answers.
|
||||
isWebReadyRef.current = false
|
||||
armWebReadyWatchdog()
|
||||
pendingPingIdRef.current = sendToWebView({ type: 'ping' })
|
||||
sendToWebView(msg)
|
||||
},
|
||||
write(data: string) {
|
||||
writeCoalescer.write(data)
|
||||
[pendingMessages, sendToWebView]
|
||||
)
|
||||
|
||||
// Why: a busy PTY delivers ~200 stream frames/s; coalescing here collapses the
|
||||
// per-frame bridge + WebKit IPC + paint cost that runs the phone hot (#9302).
|
||||
const writeCoalescer = useMemo(
|
||||
() => createTerminalWriteCoalescer((data) => postMessage({ type: 'write', data })),
|
||||
[postMessage]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
writeCoalescer.clear()
|
||||
}
|
||||
}, [writeCoalescer])
|
||||
|
||||
const confirmWebReady = useCallback(
|
||||
(notifyParent: boolean) => {
|
||||
pendingPingIdRef.current = null
|
||||
isWebReadyRef.current = true
|
||||
clearWebReadyWatchdog()
|
||||
clearEngineError()
|
||||
if (notifyParent) {
|
||||
onWebReady?.()
|
||||
}
|
||||
// Why: reload clears queued commands, so readiness must always restore the
|
||||
// native-selected theme even when its value did not change in React.
|
||||
sendToWebView({ type: 'set-theme', terminalTheme })
|
||||
flushPendingMessages()
|
||||
},
|
||||
init(
|
||||
cols: number,
|
||||
rows: number,
|
||||
initialData?: string,
|
||||
preserveScroll?: boolean,
|
||||
oscLinks?: TerminalOscLinkRange[]
|
||||
) {
|
||||
// Why: arm a fresh ready promise BEFORE posting init. The WebView
|
||||
// resolves it via the 'ready' notify at the end of its rAF chain.
|
||||
// Resolve any prior in-flight ready first so awaiters from the
|
||||
// previous generation don't sit on the 3s setTimeout fallback —
|
||||
// each leaked timer + closure pinned an awaiting measure caller
|
||||
// for the full 3s under rapid re-init (orientation change,
|
||||
// multiple resubscribes), delaying cold-start fit chains.
|
||||
const priorResolve = readyResolveRef.current
|
||||
if (priorResolve) {
|
||||
[
|
||||
clearEngineError,
|
||||
clearWebReadyWatchdog,
|
||||
flushPendingMessages,
|
||||
onWebReady,
|
||||
sendToWebView,
|
||||
terminalTheme
|
||||
]
|
||||
)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: WebViewMessageEvent) => {
|
||||
let msg: Record<string, unknown>
|
||||
try {
|
||||
msg = JSON.parse(event.nativeEvent.data) as Record<string, unknown>
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
routeTerminalQueryReply(msg, onTerminalQueryReply)
|
||||
|
||||
if (msg.type === 'web-ready') {
|
||||
confirmWebReady(true)
|
||||
} else if (
|
||||
msg.type === 'pong' &&
|
||||
typeof msg.pingId === 'number' &&
|
||||
msg.pingId === pendingPingIdRef.current
|
||||
) {
|
||||
confirmWebReady(false)
|
||||
} else if (msg.type === 'ready') {
|
||||
// Why: the WebView's init() rAF chain has run — term is open,
|
||||
// renderService is populated, first paint has happened. Resolve
|
||||
// any pending awaitReady() so a queued measure can now safely
|
||||
// read cell dims.
|
||||
const resolve = readyResolveRef.current
|
||||
readyResolveRef.current = null
|
||||
readyPromiseRef.current = null
|
||||
priorResolve()
|
||||
}
|
||||
readyPromiseRef.current = new Promise<void>((resolve) => {
|
||||
readyResolveRef.current = resolve
|
||||
})
|
||||
// Why: pending chunks are pre-snapshot data; the init snapshot supersedes
|
||||
// them, and writing them after init would corrupt the fresh buffer.
|
||||
writeCoalescer.clear()
|
||||
postMessage({
|
||||
type: 'init',
|
||||
cols,
|
||||
rows,
|
||||
initialData,
|
||||
oscLinks,
|
||||
terminalTheme,
|
||||
fontScale: textScale,
|
||||
preserveScroll
|
||||
})
|
||||
},
|
||||
resize(cols: number, rows: number) {
|
||||
// Why: resize/reflow must observe all prior writes or bytes reorder.
|
||||
writeCoalescer.flushNow()
|
||||
postMessage({ type: 'resize', cols, rows })
|
||||
},
|
||||
reflow(cols: number, rows: number) {
|
||||
writeCoalescer.flushNow()
|
||||
postMessage({ type: 'reflow', cols, rows })
|
||||
},
|
||||
clear() {
|
||||
writeCoalescer.clear()
|
||||
postMessage({ type: 'clear' })
|
||||
},
|
||||
measureFitDimensions(
|
||||
containerHeight?: number
|
||||
): Promise<{ cols: number; rows: number } | null> {
|
||||
if (!isWebReadyRef.current) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
measureResolveRef.current?.(null)
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
const finish = (result: { cols: number; rows: number } | null) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
if (measureResolveRef.current === finish) {
|
||||
measureResolveRef.current = null
|
||||
}
|
||||
resolve(result)
|
||||
resolve?.()
|
||||
} else if (msg.type === 'measure-result') {
|
||||
const resolve = measureResolveRef.current
|
||||
measureResolveRef.current = null
|
||||
if (resolve) {
|
||||
const cols = typeof msg.cols === 'number' ? msg.cols : null
|
||||
const rows = typeof msg.rows === 'number' ? msg.rows : null
|
||||
resolve(cols && rows && cols >= 20 && rows >= 8 ? { cols, rows } : null)
|
||||
}
|
||||
measureResolveRef.current = finish
|
||||
sendToWebView({ type: 'measure', containerHeight })
|
||||
// Why: if the WebView doesn't respond within 2s (e.g., xterm
|
||||
// failed to load), resolve null so the caller can disable
|
||||
// Fit to Phone rather than hanging indefinitely.
|
||||
timeout = setTimeout(() => {
|
||||
if (measureResolveRef.current === finish) {
|
||||
finish(null)
|
||||
}
|
||||
}, 2000)
|
||||
})
|
||||
},
|
||||
resetZoom() {
|
||||
postMessage({ type: 'reset-zoom' })
|
||||
},
|
||||
cancelSelect() {
|
||||
postMessage({ type: 'cancel-select' })
|
||||
},
|
||||
doSelectAll() {
|
||||
postMessage({ type: 'do-select-all' })
|
||||
},
|
||||
async awaitReady(): Promise<void> {
|
||||
// Why: returns the in-flight ready promise (set by init); resolves
|
||||
// immediately if no init is pending. Capped at 3s so a stuck
|
||||
// WebView doesn't hang the caller.
|
||||
const p = readyPromiseRef.current
|
||||
if (!p) {
|
||||
return
|
||||
} else {
|
||||
dispatchTerminalWebViewNotification(msg, {
|
||||
reportEngineError,
|
||||
onSelectionMode,
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
onModesChanged,
|
||||
onKeyboardAvoidanceMetrics,
|
||||
onHaptic,
|
||||
onTerminalInput,
|
||||
onTerminalTap,
|
||||
onFileTap,
|
||||
onOpenUrl,
|
||||
onTextScaleChange
|
||||
})
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
let settled = false
|
||||
const timeout = setTimeout(() => {
|
||||
settled = true
|
||||
resolve()
|
||||
}, 3000)
|
||||
void p.finally(() => {
|
||||
if (!settled) {
|
||||
clearTimeout(timeout)
|
||||
},
|
||||
[
|
||||
confirmWebReady,
|
||||
reportEngineError,
|
||||
onSelectionMode,
|
||||
onSelectionCopy,
|
||||
onSelectionEvicted,
|
||||
onModesChanged,
|
||||
onKeyboardAvoidanceMetrics,
|
||||
onHaptic,
|
||||
onTerminalInput,
|
||||
onTerminalQueryReply,
|
||||
onTerminalTap,
|
||||
onFileTap,
|
||||
onOpenUrl,
|
||||
onTextScaleChange
|
||||
]
|
||||
)
|
||||
|
||||
const handleLoadStart = useCallback(() => {
|
||||
isWebReadyRef.current = false
|
||||
pendingPingIdRef.current = null
|
||||
armWebReadyWatchdog()
|
||||
// Why: messages queued for a previous WebView generation are stale after a reload;
|
||||
// dropping them avoids replaying terminal chunks before the next init snapshot.
|
||||
pendingMessages.clear()
|
||||
writeCoalescer.clear()
|
||||
}, [armWebReadyWatchdog, pendingMessages, writeCoalescer])
|
||||
|
||||
const handleReload = useCallback(() => {
|
||||
clearEngineError()
|
||||
webViewRef.current?.reload()
|
||||
}, [clearEngineError])
|
||||
|
||||
const handleContentProcessDidTerminate = useCallback(() => {
|
||||
// Why: WKWebView content-process loss is recoverable; stale commands belong
|
||||
// to the dead document and the replacement must prove readiness before replay.
|
||||
isWebReadyRef.current = false
|
||||
pendingPingIdRef.current = null
|
||||
pendingMessages.clear()
|
||||
writeCoalescer.clear()
|
||||
clearEngineError()
|
||||
armWebReadyWatchdog()
|
||||
webViewRef.current?.reload()
|
||||
}, [armWebReadyWatchdog, clearEngineError, pendingMessages, writeCoalescer])
|
||||
|
||||
useEffect(() => {
|
||||
postMessage({ type: 'set-theme', terminalTheme })
|
||||
}, [postMessage, terminalThemeKey, terminalTheme])
|
||||
|
||||
// Why: live-apply text-size changes to an already-mounted terminal (the pane
|
||||
// stays alive while the user visits Settings), so no terminal reload is needed.
|
||||
useEffect(() => {
|
||||
postMessage({ type: 'set-font-scale', fontScale: textScale })
|
||||
}, [postMessage, textScale])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
prepareForForegroundRecovery() {
|
||||
if (Platform.OS !== 'ios') {
|
||||
return
|
||||
}
|
||||
// Why: direct ping is the only command allowed through while readiness is
|
||||
// invalid; init/write commands queue until this exact document answers.
|
||||
isWebReadyRef.current = false
|
||||
armWebReadyWatchdog()
|
||||
pendingPingIdRef.current = sendToWebView({ type: 'ping' })
|
||||
},
|
||||
write(data: string) {
|
||||
writeCoalescer.write(data)
|
||||
},
|
||||
init(
|
||||
cols: number,
|
||||
rows: number,
|
||||
initialData?: string,
|
||||
preserveScroll?: boolean,
|
||||
oscLinks?: TerminalOscLinkRange[]
|
||||
) {
|
||||
// Why: arm a fresh ready promise BEFORE posting init. The WebView
|
||||
// resolves it via the 'ready' notify at the end of its rAF chain.
|
||||
// Resolve any prior in-flight ready first so awaiters from the
|
||||
// previous generation don't sit on the 3s setTimeout fallback —
|
||||
// each leaked timer + closure pinned an awaiting measure caller
|
||||
// for the full 3s under rapid re-init (orientation change,
|
||||
// multiple resubscribes), delaying cold-start fit chains.
|
||||
const priorResolve = readyResolveRef.current
|
||||
if (priorResolve) {
|
||||
readyResolveRef.current = null
|
||||
readyPromiseRef.current = null
|
||||
priorResolve()
|
||||
}
|
||||
readyPromiseRef.current = new Promise<void>((resolve) => {
|
||||
readyResolveRef.current = resolve
|
||||
})
|
||||
// Why: pending chunks are pre-snapshot data; the init snapshot supersedes
|
||||
// them, and writing them after init would corrupt the fresh buffer.
|
||||
writeCoalescer.clear()
|
||||
postMessage({
|
||||
type: 'init',
|
||||
cols,
|
||||
rows,
|
||||
initialData,
|
||||
oscLinks,
|
||||
terminalTheme,
|
||||
fontScale: textScale,
|
||||
preserveScroll
|
||||
})
|
||||
},
|
||||
resize(cols: number, rows: number) {
|
||||
// Why: resize/reflow must observe all prior writes or bytes reorder.
|
||||
writeCoalescer.flushNow()
|
||||
postMessage({ type: 'resize', cols, rows })
|
||||
},
|
||||
reflow(cols: number, rows: number) {
|
||||
writeCoalescer.flushNow()
|
||||
postMessage({ type: 'reflow', cols, rows })
|
||||
},
|
||||
clear() {
|
||||
writeCoalescer.clear()
|
||||
postMessage({ type: 'clear' })
|
||||
},
|
||||
measureFitDimensions(
|
||||
containerHeight?: number
|
||||
): Promise<{ cols: number; rows: number } | null> {
|
||||
if (!isWebReadyRef.current) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
measureResolveRef.current?.(null)
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
const finish = (result: { cols: number; rows: number } | null) => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
if (measureResolveRef.current === finish) {
|
||||
measureResolveRef.current = null
|
||||
}
|
||||
resolve(result)
|
||||
}
|
||||
measureResolveRef.current = finish
|
||||
sendToWebView({ type: 'measure', containerHeight })
|
||||
// Why: if the WebView doesn't respond within 2s (e.g., xterm
|
||||
// failed to load), resolve null so the caller can disable
|
||||
// Fit to Phone rather than hanging indefinitely.
|
||||
timeout = setTimeout(() => {
|
||||
if (measureResolveRef.current === finish) {
|
||||
finish(null)
|
||||
}
|
||||
}, 2000)
|
||||
})
|
||||
},
|
||||
resetZoom() {
|
||||
postMessage({ type: 'reset-zoom' })
|
||||
},
|
||||
cancelSelect() {
|
||||
postMessage({ type: 'cancel-select' })
|
||||
},
|
||||
doSelectAll() {
|
||||
postMessage({ type: 'do-select-all' })
|
||||
},
|
||||
async awaitReady(): Promise<void> {
|
||||
// Why: returns the in-flight ready promise (set by init); resolves
|
||||
// immediately if no init is pending. Capped at 3s so a stuck
|
||||
// WebView doesn't hang the caller.
|
||||
const p = readyPromiseRef.current
|
||||
if (!p) {
|
||||
return
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
let settled = false
|
||||
const timeout = setTimeout(() => {
|
||||
settled = true
|
||||
resolve()
|
||||
}
|
||||
}, 3000)
|
||||
void p.finally(() => {
|
||||
if (!settled) {
|
||||
clearTimeout(timeout)
|
||||
settled = true
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}),
|
||||
[armWebReadyWatchdog, postMessage, sendToWebView, terminalTheme, textScale, writeCoalescer]
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[TERMINAL_WEBVIEW_FRAME_STYLES.container, style]}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={XTERM_WEBVIEW_SOURCE}
|
||||
style={TERMINAL_WEBVIEW_FRAME_STYLES.webview}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
scrollEnabled={false}
|
||||
// Why: Android parent gesture containers can intercept vertical drags
|
||||
// before the injected xterm scroll router sees them.
|
||||
nestedScrollEnabled
|
||||
scalesPageToFit={false}
|
||||
// Why: Android WebView defaults textZoom to the system font scale, inflating
|
||||
// xterm's DOM glyphs past its canvas-measured cell grid (#4579). iOS ignores it.
|
||||
textZoom={100}
|
||||
onLoadStart={handleLoadStart}
|
||||
onMessage={handleMessage}
|
||||
onError={(event) => reportNativeEngineError('Terminal WebView load failed', event)}
|
||||
onHttpError={(event) => reportNativeEngineError('Terminal WebView HTTP error', event)}
|
||||
onRenderProcessGone={(event) =>
|
||||
reportNativeEngineError('Terminal WebView render process ended', event)
|
||||
}
|
||||
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
||||
/>
|
||||
{engineError ? (
|
||||
<TerminalWebViewEngineErrorOverlay message={engineError} onReload={handleReload} />
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}),
|
||||
[armWebReadyWatchdog, postMessage, sendToWebView, terminalTheme, textScale, writeCoalescer]
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[TERMINAL_WEBVIEW_FRAME_STYLES.container, style]}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={XTERM_WEBVIEW_SOURCE}
|
||||
style={TERMINAL_WEBVIEW_FRAME_STYLES.webview}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
scrollEnabled={false}
|
||||
// Why: Android parent gesture containers can intercept vertical drags
|
||||
// before the injected xterm scroll router sees them.
|
||||
nestedScrollEnabled
|
||||
scalesPageToFit={false}
|
||||
// Why: Android WebView defaults textZoom to the system font scale, inflating
|
||||
// xterm's DOM glyphs past its canvas-measured cell grid (#4579). iOS ignores it.
|
||||
textZoom={100}
|
||||
onLoadStart={handleLoadStart}
|
||||
onMessage={handleMessage}
|
||||
onError={(event) => reportNativeEngineError('Terminal WebView load failed', event)}
|
||||
onHttpError={(event) => reportNativeEngineError('Terminal WebView HTTP error', event)}
|
||||
onRenderProcessGone={(event) =>
|
||||
reportNativeEngineError('Terminal WebView render process ended', event)
|
||||
}
|
||||
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
||||
/>
|
||||
{engineError ? (
|
||||
<TerminalWebViewEngineErrorOverlay message={engineError} onReload={handleReload} />
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TappedFilePath } from './terminal-path-tap'
|
||||
import type { ParsedFileLinkLocation } from '../../../src/shared/file-link-location'
|
||||
import { parsePathWithOptionalLineColumn } from './terminal-path-tap'
|
||||
|
||||
export function resolveTerminalFileUrlTap(uri: string): TappedFilePath | null {
|
||||
export function resolveTerminalFileUrlTap(uri: string): ParsedFileLinkLocation | null {
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(uri)
|
||||
@@ -24,7 +24,7 @@ export function resolveTerminalFileUrlTap(uri: string): TappedFilePath | null {
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveTerminalOscFileTap(uri: string): TappedFilePath | null {
|
||||
export function resolveTerminalOscFileTap(uri: string): ParsedFileLinkLocation | null {
|
||||
return resolveTerminalFileUrlTap(uri) ?? parseOscPathLikeTarget(uri)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ function isLocalFileUriHostname(hostname: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
function parseOscPathLikeTarget(value: string): TappedFilePath | null {
|
||||
function parseOscPathLikeTarget(value: string): ParsedFileLinkLocation | null {
|
||||
if (
|
||||
!/^(?:~[\\/]|[\\/]|\.{1,2}[\\/]|[A-Za-z]:[\\/]|[A-Za-z0-9._-]+[\\/]|(?=[A-Za-z0-9._-]*\.[A-Za-z0-9]))/.test(
|
||||
value
|
||||
@@ -81,7 +81,7 @@ function parseFileUrlLineHash(hash: string): { line: number; column: number | nu
|
||||
return { line, column }
|
||||
}
|
||||
|
||||
function parseFilePathTrailingLineTarget(filePath: string): TappedFilePath | null {
|
||||
function parseFilePathTrailingLineTarget(filePath: string): ParsedFileLinkLocation | null {
|
||||
const match = /^(.*?)(?::(\d+))(?::(\d+))?$/.exec(filePath)
|
||||
if (!match || !match[1] || match[1].endsWith('/') || match[1].endsWith('\\')) {
|
||||
return null
|
||||
|
||||
@@ -84,8 +84,6 @@ export type TerminalLiveInputDefaultResult = {
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
export type TerminalLiveInputPruneResult = TerminalLiveInputDefaultResult
|
||||
|
||||
export function getTerminalLiveSpecialKeyBytes(key: string): string | null {
|
||||
const shortcutKey = TERMINAL_LIVE_SPECIAL_KEY_IDS.get(key)
|
||||
if (!shortcutKey) {
|
||||
@@ -176,7 +174,7 @@ export function pruneTerminalLiveInputHandles(
|
||||
enabledHandles: ReadonlySet<string>,
|
||||
defaultedHandles: ReadonlySet<string>,
|
||||
liveTerminalHandles: ReadonlySet<string>
|
||||
): TerminalLiveInputPruneResult {
|
||||
): TerminalLiveInputDefaultResult {
|
||||
let nextEnabledHandles: Set<string> | null = null
|
||||
let nextDefaultedHandles: Set<string> | null = null
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
type ParsedFileLinkLocation
|
||||
} from '../../../src/shared/file-link-location'
|
||||
|
||||
export type TappedFilePath = ParsedFileLinkLocation
|
||||
|
||||
// Separator-anchored path tokens (absolute, relative, ~/, drive-letter, UNC) OR
|
||||
// a bare filename with an extension (README.md, index.ts), optionally suffixed
|
||||
// with :line or :line:col. Like desktop, we propose candidates and let the host
|
||||
@@ -47,7 +45,7 @@ function trimBoundaryPunctuation(
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePathWithOptionalLineColumn(value: string): TappedFilePath | null {
|
||||
export function parsePathWithOptionalLineColumn(value: string): ParsedFileLinkLocation | null {
|
||||
const parsed = parseFileLinkLocation(value)
|
||||
if (!parsed) {
|
||||
return null
|
||||
@@ -137,7 +135,7 @@ function hasSpacedPathExtension(text: string): boolean {
|
||||
return /\s/.test(trimmed) && /\.[A-Za-z0-9_+-]+(?::\d+)?(?::\d+)?$/.test(trimmed)
|
||||
}
|
||||
|
||||
function matchSpacedFilePathAtColumn(lineText: string, col: number): TappedFilePath | null {
|
||||
function matchSpacedFilePathAtColumn(lineText: string, col: number): ParsedFileLinkLocation | null {
|
||||
SPACED_PATH_REGEX.lastIndex = 0
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = SPACED_PATH_REGEX.exec(lineText)) !== null) {
|
||||
@@ -165,7 +163,10 @@ function matchSpacedFilePathAtColumn(lineText: string, col: number): TappedFileP
|
||||
|
||||
// Returns the file-path span (after punctuation trim) that contains `col`, or
|
||||
// null when the tap isn't on a path.
|
||||
export function matchFilePathAtColumn(lineText: string, col: number): TappedFilePath | null {
|
||||
export function matchFilePathAtColumn(
|
||||
lineText: string,
|
||||
col: number
|
||||
): ParsedFileLinkLocation | null {
|
||||
const spaced = matchSpacedFilePathAtColumn(lineText, col)
|
||||
if (spaced) {
|
||||
return spaced
|
||||
|
||||
@@ -42,8 +42,6 @@ function toNonNegativeInteger(value: unknown): number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0
|
||||
}
|
||||
|
||||
export type MobileTerminalTheme = RuntimeMobileTerminalTheme
|
||||
|
||||
export type TerminalSelectionEvents = {
|
||||
onSelectionMode?: (active: boolean) => void
|
||||
onSelectionCopy?: (text: string) => void
|
||||
@@ -65,7 +63,7 @@ export type TerminalSelectionEvents = {
|
||||
|
||||
export type TerminalWebViewProps = {
|
||||
style?: StyleProp<ViewStyle>
|
||||
terminalTheme?: MobileTerminalTheme
|
||||
terminalTheme?: RuntimeMobileTerminalTheme
|
||||
// Why: baseline zoom multiplier applied on top of fit-to-width scale; raw
|
||||
// xterm fontSize alone cannot drive apparent size because fitting cancels it.
|
||||
textScale?: number
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, Script } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { TappedFilePath } from './terminal-path-tap'
|
||||
import type { ParsedFileLinkLocation } from '../../../src/shared/file-link-location'
|
||||
import { TERMINAL_PATH_TAP_JS } from './terminal-path-tap-injected'
|
||||
import {
|
||||
TERMINAL_HTTP_URL_MAX_LENGTH,
|
||||
@@ -16,7 +16,7 @@ import { XTERM_HTML } from './terminal-webview-html'
|
||||
type FileTapResolverCase = {
|
||||
name: string
|
||||
uri: string
|
||||
expected: TappedFilePath | null
|
||||
expected: ParsedFileLinkLocation | null
|
||||
}
|
||||
|
||||
const FILE_URL_TAP_CASES: FileTapResolverCase[] = [
|
||||
@@ -90,7 +90,7 @@ const OSC_FILE_TAP_CASES: FileTapResolverCase[] = [
|
||||
}
|
||||
]
|
||||
|
||||
type InjectedFileTapResolver = (uri: string) => TappedFilePath | null
|
||||
type InjectedFileTapResolver = (uri: string) => ParsedFileLinkLocation | null
|
||||
|
||||
// Why: the WebView blob hand-translates terminal-file-url-tap.ts into plain JS
|
||||
// with re-escaped regexes; executing it against the same cases as the TS module
|
||||
|
||||
@@ -43,13 +43,11 @@ export {
|
||||
useRefreshHostClient
|
||||
} from './host-client-hooks'
|
||||
|
||||
type StoreEntry = HostClientStoreEntry
|
||||
|
||||
const Ctx = createContext<RpcClientContextValue | null>(null)
|
||||
|
||||
export function RpcClientProvider({ children }: { children: ReactNode }) {
|
||||
// Why: entries in a ref so state changes don't re-render the whole tree; propagation goes through per-host listener Sets.
|
||||
const storeRef = useRef<Map<string, StoreEntry>>(new Map())
|
||||
const storeRef = useRef<Map<string, HostClientStoreEntry>>(new Map())
|
||||
const stateListenersRef = useRef<Map<string, Set<(state: ConnectionState) => void>>>(new Map())
|
||||
const allHostsListenersRef = useRef<Set<() => void>>(new Set())
|
||||
|
||||
@@ -93,7 +91,7 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
|
||||
}, [])
|
||||
|
||||
const openEntry = useCallback(
|
||||
(hostId: string, allowUnowned = false): Promise<StoreEntry | null> => {
|
||||
(hostId: string, allowUnowned = false): Promise<HostClientStoreEntry | null> => {
|
||||
const retryScheduler = retrySchedulerRef.current
|
||||
if (!retryScheduler) {
|
||||
throw new Error('host retry scheduler not initialized')
|
||||
|
||||
@@ -2,11 +2,11 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
mobileHostEditHostRoute,
|
||||
mobileHostEditRouteTarget,
|
||||
navigateToMobileHostEdit,
|
||||
type MobileHostEditNavigationState
|
||||
navigateToMobileHostEdit
|
||||
} from './host-edit-navigation'
|
||||
import type { HostStackNavigationState } from '../navigation/host-stack-navigation'
|
||||
|
||||
function navigationHarness(initialState: MobileHostEditNavigationState) {
|
||||
function navigationHarness(initialState: HostStackNavigationState) {
|
||||
let stateListener = () => {}
|
||||
let state = initialState
|
||||
const unsubscribeState = vi.fn()
|
||||
@@ -20,7 +20,7 @@ function navigationHarness(initialState: MobileHostEditNavigationState) {
|
||||
}
|
||||
return {
|
||||
navigation,
|
||||
setState(nextState: MobileHostEditNavigationState) {
|
||||
setState(nextState: HostStackNavigationState) {
|
||||
state = nextState
|
||||
stateListener()
|
||||
},
|
||||
@@ -30,7 +30,7 @@ function navigationHarness(initialState: MobileHostEditNavigationState) {
|
||||
|
||||
// Edit now waits for the nested host stack, not just the root `h` route, so every committed
|
||||
// state below carries the stack the replacement targets.
|
||||
function committedHostState(hostIdParam: string): MobileHostEditNavigationState {
|
||||
function committedHostState(hostIdParam: string): HostStackNavigationState {
|
||||
return {
|
||||
index: 1,
|
||||
routes: [
|
||||
|
||||
@@ -3,19 +3,12 @@ import {
|
||||
navigateToHostStackRoute,
|
||||
type HostStackHostRoute,
|
||||
type HostStackNavigationController,
|
||||
type HostStackNavigationState,
|
||||
type HostStackRootNavigation,
|
||||
type HostStackRouteTarget,
|
||||
type HostStackRouter
|
||||
} from '../navigation/host-stack-navigation'
|
||||
|
||||
export type MobileHostEditHostRoute = HostStackHostRoute
|
||||
export type MobileHostEditNavigationState = HostStackNavigationState
|
||||
export type MobileHostEditRootNavigation = HostStackRootNavigation
|
||||
export type MobileHostEditRouter = HostStackRouter
|
||||
export type MobileHostEditNavigationController = HostStackNavigationController
|
||||
|
||||
export function mobileHostEditHostRoute(hostId: string): MobileHostEditHostRoute {
|
||||
export function mobileHostEditHostRoute(hostId: string): HostStackHostRoute {
|
||||
return hostStackHostRoute(hostId)
|
||||
}
|
||||
|
||||
@@ -27,9 +20,9 @@ export function mobileHostEditRouteTarget(hostId: string): HostStackRouteTarget
|
||||
}
|
||||
|
||||
export function navigateToMobileHostEdit(
|
||||
navigation: MobileHostEditRootNavigation,
|
||||
router: MobileHostEditRouter,
|
||||
navigation: HostStackRootNavigation,
|
||||
router: HostStackRouter,
|
||||
hostId: string
|
||||
): MobileHostEditNavigationController {
|
||||
): HostStackNavigationController {
|
||||
return navigateToHostStackRoute(navigation, router, hostId, mobileHostEditRouteTarget(hostId))
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ type EmulatorKillResult = {
|
||||
deviceUdid?: string
|
||||
}
|
||||
|
||||
type EmulatorShutdownResult = EmulatorKillResult
|
||||
|
||||
type EmulatorGesturePoint = {
|
||||
edge?: number
|
||||
type: 'begin' | 'move' | 'end'
|
||||
@@ -233,7 +231,7 @@ export const EMULATOR_HANDLERS: Record<string, CommandHandler> = {
|
||||
worktree: target.worktree
|
||||
})
|
||||
printResult(res, json, (r: unknown) => {
|
||||
const result = r as EmulatorShutdownResult
|
||||
const result = r as EmulatorKillResult
|
||||
return `Shut down ${result.deviceUdid || target.device || 'emulator'}`
|
||||
})
|
||||
},
|
||||
|
||||
@@ -18,9 +18,9 @@ import { getSshGitProvider } from '../providers/ssh-git-dispatch'
|
||||
import {
|
||||
probeBranchUpstream,
|
||||
renameCurrentBranch,
|
||||
resolveUniqueBranchName,
|
||||
type GitExec
|
||||
resolveUniqueBranchName
|
||||
} from '../git/branch-rename'
|
||||
import type { GitCommandRunner } from '../../shared/git-effective-upstream'
|
||||
import {
|
||||
generateBranchNameFromContext,
|
||||
resolveTextGenerationParams
|
||||
@@ -180,7 +180,7 @@ async function runAutoRename(
|
||||
if (repo.connectionId && !provider) {
|
||||
return retry('ssh provider unavailable')
|
||||
}
|
||||
const exec: GitExec = provider
|
||||
const exec: GitCommandRunner = provider
|
||||
? (args) => provider.exec(args, worktreePath)
|
||||
: (args) => gitExecFileAsync(args, { cwd: worktreePath })
|
||||
|
||||
|
||||
@@ -19,15 +19,13 @@ export type ReadAiVaultFirstUserPromptArgs = {
|
||||
codexHome?: string | null
|
||||
}
|
||||
|
||||
export type ReadAiVaultFirstUserPromptResult = AiVaultFirstUserPromptResult
|
||||
|
||||
/**
|
||||
* Re-parse one session transcript under full first-prompt capture and return
|
||||
* the untruncated first real user ask for copy/reuse.
|
||||
*/
|
||||
export async function readAiVaultFirstUserPrompt(
|
||||
args: ReadAiVaultFirstUserPromptArgs
|
||||
): Promise<ReadAiVaultFirstUserPromptResult> {
|
||||
): Promise<AiVaultFirstUserPromptResult> {
|
||||
const filePath = args.filePath.trim()
|
||||
if (!filePath || !args.agent) {
|
||||
return { prompt: null }
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { AiVaultListResult, AiVaultSubagentListResult } from '../../shared/ai-vault-types'
|
||||
import type {
|
||||
AiVaultFirstUserPromptResult,
|
||||
AiVaultListResult,
|
||||
AiVaultSubagentListResult
|
||||
} from '../../shared/ai-vault-types'
|
||||
import type {
|
||||
AiVaultSessionTitleRequest,
|
||||
AiVaultSessionTitlesResult
|
||||
} from '../../shared/ai-vault-session-title'
|
||||
import {
|
||||
readAiVaultFirstUserPrompt,
|
||||
type ReadAiVaultFirstUserPromptArgs,
|
||||
type ReadAiVaultFirstUserPromptResult
|
||||
type ReadAiVaultFirstUserPromptArgs
|
||||
} from './session-first-user-prompt-read'
|
||||
import {
|
||||
clearAiVaultServiceRestartCircuit,
|
||||
@@ -72,7 +75,7 @@ export function listAiVaultSubagentSessionsInBackground(
|
||||
|
||||
export function readAiVaultFirstUserPromptInBackground(
|
||||
request: ReadAiVaultFirstUserPromptArgs
|
||||
): Promise<ReadAiVaultFirstUserPromptResult> {
|
||||
): Promise<AiVaultFirstUserPromptResult> {
|
||||
return shouldUseAiVaultServiceProcess()
|
||||
? readAiVaultFirstUserPromptInService(request)
|
||||
: readAiVaultFirstUserPrompt(request)
|
||||
|
||||
@@ -21,14 +21,14 @@ afterEach(() => {
|
||||
tempDirs = []
|
||||
})
|
||||
|
||||
function createTempDb(): { db: Database.Database; path: string } {
|
||||
function createTempDb(): { db: Database; path: string } {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'orca-opencode-bounds-'))
|
||||
tempDirs.push(dir)
|
||||
const path = join(dir, 'opencode.db')
|
||||
return { db: new Database(path), path }
|
||||
}
|
||||
|
||||
function applySchema(db: Database.Database): void {
|
||||
function applySchema(db: Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE session (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -63,7 +63,7 @@ function applySchema(db: Database.Database): void {
|
||||
`)
|
||||
}
|
||||
|
||||
function insertSession(db: Database.Database, id: string, timeUpdated: number): void {
|
||||
function insertSession(db: Database, id: string, timeUpdated: number): void {
|
||||
db.prepare(
|
||||
`INSERT INTO session (id, project_id, directory, title, time_created, time_updated, agent)
|
||||
VALUES (?, 'proj', '/tmp/w', ?, ?, ?, 'build')`
|
||||
@@ -71,7 +71,7 @@ function insertSession(db: Database.Database, id: string, timeUpdated: number):
|
||||
}
|
||||
|
||||
function insertUserMessage(
|
||||
db: Database.Database,
|
||||
db: Database,
|
||||
args: { id: string; sessionId: string; timeCreated: number; text: string }
|
||||
): void {
|
||||
db.prepare(`INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)`).run(
|
||||
@@ -92,7 +92,7 @@ function insertUserMessage(
|
||||
}
|
||||
|
||||
function insertMessageWithPart(
|
||||
db: Database.Database,
|
||||
db: Database,
|
||||
args: {
|
||||
id: string
|
||||
sessionId: string
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user