From 0fcd896dd1c3ed2bcc825040484833a88304e10c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 12 Jun 2026 21:34:00 -0700 Subject: [PATCH] Clean host copy in onboarding flows Co-authored-by: Orca --- .../host-context-implementation-checklist.md | 1 + .../src/components/onboarding/RepoStep.tsx | 203 ++++++++---------- .../onboarding/RepoStepNestedImportPanel.tsx | 145 +++++++++++++ .../onboarding/use-onboarding-flow.ts | 4 +- .../src/components/settings/Settings.tsx | 2 +- .../components/sidebar/NonGitFolderDialog.tsx | 2 +- src/renderer/src/hooks/useComposerState.ts | 2 +- src/renderer/src/i18n/locales/en.json | 18 +- src/renderer/src/store/slices/repos.ts | 4 +- 9 files changed, 256 insertions(+), 125 deletions(-) create mode 100644 src/renderer/src/components/onboarding/RepoStepNestedImportPanel.tsx diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index 96973c9a8ec..aec0e9f337c 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -242,3 +242,4 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-12: Routed GitHub issue reads, PR check reads/details, PR comments, review replies, and review-thread resolution through explicit `TaskSourceContext` in the store/preload/main IPC boundary, and source-scoped their comment/check caches; verified with GitHub slice regressions (106 tests), focused oxlint, `git diff --check`, and full `pnpm run typecheck`. - [x] 2026-06-12: Guarded remaining GitLab path-only fallbacks by making `workItemByPath` use the same repoId/source-context selector as the rest of GitLab IPC, passing repoId from URL picker and right-sidebar GitLab review actions, and verifying source-host routing with GitLab IPC regressions plus focused oxlint and full `pnpm run typecheck`. - [x] 2026-06-12: Guarded GitHub label and assignable-user metadata lookups with the same repo/source-context selector as task mutations, so drawer edit metadata cannot bypass source-host validation; verified with GitHub IPC regressions (17 tests), focused oxlint, and full `pnpm run typecheck`. +- [x] 2026-06-12: Continued the Host copy audit through onboarding, remote folder-add, attachment import, Settings SSH copy, and remote file browser catalog text; extracted `RepoStepNestedImportPanel` to keep the touched onboarding component under max-lines, and verified with stale-copy scans, focused onboarding/repos tests, focused oxlint, full `pnpm run typecheck`, and `git diff --check`. diff --git a/src/renderer/src/components/onboarding/RepoStep.tsx b/src/renderer/src/components/onboarding/RepoStep.tsx index decf49da34f..0202ef6a6a6 100644 --- a/src/renderer/src/components/onboarding/RepoStep.tsx +++ b/src/renderer/src/components/onboarding/RepoStep.tsx @@ -1,5 +1,4 @@ import { - ArrowLeft, ArrowRight, CircleStop, FolderOpen, @@ -10,12 +9,10 @@ import { } from 'lucide-react' import type { Dispatch, SetStateAction } from 'react' import { Button } from '@/components/ui/button' -import { NestedRepoChecklist } from '@/components/repo/NestedRepoChecklist' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import type { NestedRepoScanResult } from '../../../../shared/types' -import { NestedRepoScanLimitNotice } from '../repo/NestedRepoScanLimitNotice' -import { getRuntimePathBasename } from '../../../../shared/cross-platform-path' import { translate } from '@/i18n/i18n' +import { RepoStepNestedImportPanel } from './RepoStepNestedImportPanel' type RepoStepProps = { cloneUrl: string @@ -65,92 +62,20 @@ export function RepoStep({ error }: RepoStepProps) { const disabled = Boolean(busyLabel) - const nestedImportDisabled = disabled || nestedScanInProgress if (nestedScan) { - const folderName = getRuntimePathBasename(nestedScan.selectedPath) || nestedScan.selectedPath return ( -
-
-
-
- -
-
-
{translate("auto.components.onboarding.RepoStep.2d20200346", "Import repositories")}
-
- {nestedScanInProgress ? ( - - - - - - {translate("auto.components.onboarding.RepoStep.e8fdb36338", "Scanning repositories. Click to stop.")} - - ) : null} - - {translate("auto.components.onboarding.RepoStep.2e6438dd34", "{{value0}}Found {{value1}} {{value2}} in this folder.", { value0: nestedScanInProgress ? 'Scanning... ' : '', value1: nestedScan.repos.length, value2: nestedScan.repos.length === 1 ? 'repository' : 'repositories' })} - -
-
- {translate("auto.components.onboarding.RepoStep.cecd6593fa", "Scanned folder:")} {folderName} - {nestedScan.selectedPath} -
-
-
- - {nestedScanInProgress || - nestedScan.truncated || - nestedScan.timedOut || - nestedScan.stopped ? ( -
- -
- ) : null} -
- - -
-
- {busyLabel && ( -
- {busyLabel} -
- )} - {error && ( -
- {error} -
- )} -
+ ) } return ( @@ -168,15 +93,27 @@ export function RepoStep({
-
{translate("auto.components.onboarding.RepoStep.8cab104e3c", "Open a server project")}
+
+ {translate( + 'auto.components.onboarding.RepoStep.8cab104e3c', + 'Open a project on this host' + )} +
- {translate("auto.components.onboarding.RepoStep.466108ab89", "Enter a path that exists on the runtime server.")}
+ {translate( + 'auto.components.onboarding.RepoStep.466108ab89', + 'Enter a path that exists on the selected host.' + )} +
- {translate("auto.components.onboarding.RepoStep.3863747c56", "Add Git Project")} + {translate('auto.components.onboarding.RepoStep.3863747c56', 'Add Git Project')} + + {translate('auto.components.onboarding.RepoStep.e8214aa632', 'Open as Folder')} +
) : ( @@ -212,18 +151,31 @@ export function RepoStep({
- {translate("auto.components.onboarding.RepoStep.f4e9c8dcf8", "Browse for a folder")}
+ {translate( + 'auto.components.onboarding.RepoStep.f4e9c8dcf8', + 'Browse for a folder' + )} +
- {translate("auto.components.onboarding.RepoStep.831524961f", "Choose any local directory, git repo or not.")}
+ {translate( + 'auto.components.onboarding.RepoStep.831524961f', + 'Choose any local directory, git repo or not.' + )} +
- {translate("auto.components.onboarding.RepoStep.6558d50c69", "Want to import many repos at once? Select the parent folder.")} + + {translate( + 'auto.components.onboarding.RepoStep.6558d50c69', + 'Want to import many repos at once? Select the parent folder.' + )} +
)} @@ -240,15 +192,24 @@ export function RepoStep({
-
{translate("auto.components.onboarding.RepoStep.132425a3e3", "Clone a repo")}
+
+ {translate('auto.components.onboarding.RepoStep.132425a3e3', 'Clone a repo')} +
- {translate("auto.components.onboarding.RepoStep.288d8444b7", "Paste an HTTPS or SSH URL.")}
+ {translate( + 'auto.components.onboarding.RepoStep.288d8444b7', + 'Paste an HTTPS or SSH URL.' + )} +
onCloneUrlChange(event.target.value)} @@ -258,15 +219,20 @@ export function RepoStep({ className="shrink-0 rounded-lg bg-primary px-5 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-40" disabled={!cloneUrl.trim() || (runtimeActive && !cloneDestination.trim()) || disabled} > - {translate("auto.components.onboarding.RepoStep.7932e95f68", "Clone")} + {translate('auto.components.onboarding.RepoStep.7932e95f68', 'Clone')} +
{runtimeActive && (
+ {translate('auto.components.onboarding.RepoStep.24c7c8696c', 'Clone into host path')} +
- {translate("auto.components.onboarding.RepoStep.7b679207e4", "Workspace")} + {translate('auto.components.onboarding.RepoStep.7b679207e4', 'Workspace')} - {runtimeActive ? translate("auto.components.onboarding.RepoStep.cf23006ba7", "Runtime server") : workspaceDir} + {runtimeActive + ? translate('auto.components.onboarding.RepoStep.cf23006ba7', 'Selected host') + : workspaceDir}
{runtimeActive ? (
- {translate("auto.components.onboarding.RepoStep.c33b190ca3", "Server paths only")} + + {translate('auto.components.onboarding.RepoStep.c33b190ca3', 'Host paths only')} +
) : ( )} @@ -313,8 +288,14 @@ export function RepoStep({ variant="ghost" size="icon-xs" className="group text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:bg-destructive/10 focus-visible:text-destructive focus-visible:ring-destructive/40" - aria-label={translate("auto.components.onboarding.RepoStep.c3d9d44ca2", "Stop scan")} - title={translate("auto.components.onboarding.RepoStep.c7af322fc3", "Stop scanning")} + aria-label={translate( + 'auto.components.onboarding.RepoStep.c3d9d44ca2', + 'Stop scan' + )} + title={translate( + 'auto.components.onboarding.RepoStep.c7af322fc3', + 'Stop scanning' + )} onClick={onStopNestedScan} > @@ -322,7 +303,11 @@ export function RepoStep({ - {translate("auto.components.onboarding.RepoStep.e8fdb36338", "Scanning repositories. Click to stop.")} + {translate( + 'auto.components.onboarding.RepoStep.e8fdb36338', + 'Scanning repositories. Click to stop.' + )} + ) : null}
diff --git a/src/renderer/src/components/onboarding/RepoStepNestedImportPanel.tsx b/src/renderer/src/components/onboarding/RepoStepNestedImportPanel.tsx new file mode 100644 index 00000000000..e7b24e2b71e --- /dev/null +++ b/src/renderer/src/components/onboarding/RepoStepNestedImportPanel.tsx @@ -0,0 +1,145 @@ +import { ArrowLeft, CircleStop, FolderOpen, Loader2 } from 'lucide-react' +import type { Dispatch, SetStateAction } from 'react' +import { Button } from '@/components/ui/button' +import { NestedRepoChecklist } from '@/components/repo/NestedRepoChecklist' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { translate } from '@/i18n/i18n' +import type { NestedRepoScanResult } from '../../../../shared/types' +import { getRuntimePathBasename } from '../../../../shared/cross-platform-path' +import { NestedRepoScanLimitNotice } from '../repo/NestedRepoScanLimitNotice' + +type RepoStepNestedImportPanelProps = { + nestedScan: NestedRepoScanResult + nestedScanInProgress: boolean + nestedSelectedPaths: Set + onNestedSelectedPathsChange: Dispatch>> + onImportNested: () => void + onCancelNested: () => void + onStopNestedScan: () => void + busyLabel: string | null + error: string | null + disabled: boolean +} + +export function RepoStepNestedImportPanel({ + nestedScan, + nestedScanInProgress, + nestedSelectedPaths, + onNestedSelectedPathsChange, + onImportNested, + onCancelNested, + onStopNestedScan, + busyLabel, + error, + disabled +}: RepoStepNestedImportPanelProps) { + const folderName = getRuntimePathBasename(nestedScan.selectedPath) || nestedScan.selectedPath + const nestedImportDisabled = disabled || nestedScanInProgress + return ( +
+
+
+
+ +
+
+
+ {translate('auto.components.onboarding.RepoStep.2d20200346', 'Import repositories')} +
+
+ {nestedScanInProgress ? ( + + + + + + {translate( + 'auto.components.onboarding.RepoStep.e8fdb36338', + 'Scanning repositories. Click to stop.' + )} + + + ) : null} + + {translate( + 'auto.components.onboarding.RepoStep.2e6438dd34', + '{{value0}}Found {{value1}} {{value2}} in this folder.', + { + value0: nestedScanInProgress ? 'Scanning... ' : '', + value1: nestedScan.repos.length, + value2: nestedScan.repos.length === 1 ? 'repository' : 'repositories' + } + )} + +
+
+ {translate('auto.components.onboarding.RepoStep.cecd6593fa', 'Scanned folder:')}{' '} + {folderName} - {nestedScan.selectedPath} +
+
+
+ + {nestedScanInProgress || + nestedScan.truncated || + nestedScan.timedOut || + nestedScan.stopped ? ( +
+ +
+ ) : null} +
+ + +
+
+ {busyLabel ? ( +
+ {busyLabel} +
+ ) : null} + {error ? ( +
+ {error} +
+ ) : null} +
+ ) +} diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow.ts b/src/renderer/src/components/onboarding/use-onboarding-flow.ts index a305456c07c..88d02e23af2 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow.ts @@ -705,7 +705,7 @@ export function useOnboardingFlow( if (settings?.activeRuntimeEnvironmentId?.trim()) { const path = serverPath.trim() if (!path) { - const message = 'Enter a server path.' + const message = 'Enter a path on the selected host.' setError(message) return } @@ -996,7 +996,7 @@ export function useOnboardingFlow( const destination = target.kind === 'environment' ? cloneDestination.trim() : settings.workspaceDir if (!destination) { - const message = 'Enter a server path for the clone destination.' + const message = 'Enter a host path for the clone destination.' setError(message) return } diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index bec514251ab..b9f36963782 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -1388,7 +1388,7 @@ function Settings(): React.JSX.Element { title={translate('auto.components.settings.Settings.9b02492d1f', 'SSH Hosts')} description={translate( 'auto.components.settings.Settings.c2ee313198', - 'Remote SSH hosts for files, terminals, and git.' + 'SSH host access for files, terminals, and git.' )} searchEntries={getSectionSearchEntries('ssh')} > diff --git a/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx b/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx index 1e9ef329da3..d65cd747604 100644 --- a/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx +++ b/src/renderer/src/components/sidebar/NonGitFolderDialog.tsx @@ -73,7 +73,7 @@ const NonGitFolderDialog = React.memo(function NonGitFolderDialog() { ? err.message : translate( 'auto.components.sidebar.NonGitFolderDialog.c49fb13492', - 'Failed to add remote folder' + 'Failed to add folder on this host' ) ) } diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index 1cee618a172..fe012f66a67 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -1578,7 +1578,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS toast.error( translate( 'auto.hooks.useComposerState.3db83fc58a', - 'No remote project path is available for attachments.' + 'No project path is available on this host for attachments.' ) ) return { filePaths: [], folderPaths: [] } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index ad097e0310d..aee0a52a309 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -218,7 +218,7 @@ }, "repos": { "b7e14472ae": "Failed to add folder", - "e649269645": "Use a server path to add projects from a remote runtime.", + "e649269645": "Use Add Project to enter a path on the selected host.", "c6e022ddfc": "Failed to add project", "90d129b48b": "Folder added", "8bb3ad7935": "Project added", @@ -475,7 +475,7 @@ "7eb3f44ff7": "Selected agent is disabled. Choose an enabled agent before creating.", "b2ead86962": "Failed to resolve PR base.", "a9ff236145": "Some attachments could not be uploaded.", - "3db83fc58a": "No remote project path is available for attachments.", + "3db83fc58a": "No project path is available on this host for attachments.", "ba6cb77082": "Failed to connect to project." }, "useGlobalFileDrop": { @@ -3233,7 +3233,7 @@ "e52454b7f6": "Open as Folder", "05b33a17a9": "Cancel", "8fba4b8cbb": "This folder isn't a Git repository. You'll have the editor, terminal, and search, but Git-based features won't be available.", - "c49fb13492": "Failed to add remote folder" + "c49fb13492": "Failed to add folder on this host" }, "OrcaYamlTrustDialog": { "f3e2b868fb": "Run hooks", @@ -3279,7 +3279,7 @@ "9e060f5815": "Select folder", "f8b1deb1a4": "Cancel", "51001182e3": "Empty directory", - "971d85cc84": "Opens as a remote project · {{value0}}", + "971d85cc84": "Opens as a project on this host · {{value0}}", "00c4235c10": "No matches for '{{value0}}'" }, "RemoveFolderDialog": { @@ -8576,9 +8576,9 @@ "RepoStep": { "e8fdb36338": "Scanning repositories. Click to stop.", "b7c4da0504": "SSH? Set hosts up in Settings", - "c33b190ca3": "Server paths only", + "c33b190ca3": "Host paths only", "7b679207e4": "Workspace", - "24c7c8696c": "Clone into server path", + "24c7c8696c": "Clone into host path", "7932e95f68": "Clone", "955134915e": "git@github.com:org/repo.git", "288d8444b7": "Paste an HTTPS or SSH URL.", @@ -8589,14 +8589,14 @@ "e8214aa632": "Open as Folder", "3863747c56": "Add Git Project", "2ebbc26343": "/home/user/project", - "466108ab89": "Enter a path that exists on the runtime server.", - "8cab104e3c": "Open a server project", + "466108ab89": "Enter a path that exists on the selected host.", + "8cab104e3c": "Open a project on this host", "2d20200346": "Import repositories", "27ca610db1": "Back", "cecd6593fa": "Scanned folder:", "c7af322fc3": "Stop scanning", "c3d9d44ca2": "Stop scan", - "cf23006ba7": "Runtime server", + "cf23006ba7": "Selected host", "7ec3f48820": "/home/user", "2e6438dd34": "{{value0}}Found {{value1}} {{value2}} in this folder." }, diff --git a/src/renderer/src/store/slices/repos.ts b/src/renderer/src/store/slices/repos.ts index a3c19914db9..c3a3e8b1ee9 100644 --- a/src/renderer/src/store/slices/repos.ts +++ b/src/renderer/src/store/slices/repos.ts @@ -1391,11 +1391,11 @@ export const createRepoSlice: StateCreator = (set, const target = getActiveRuntimeTarget(get().settings) if (target.kind !== 'local') { // Why: OS folder pickers return client-local paths. Remote environments - // need an explicit server path, which the Add Project dialog handles. + // need an explicit host path, which the Add Project dialog handles. toast.error( translate( 'auto.store.slices.repos.e649269645', - 'Use a server path to add projects from a remote runtime.' + 'Use Add Project to enter a path on the selected host.' ) ) return null