Files
stablyai--orca/docs/reference/direct-url-or-file-entry.md
2026-07-13 13:05:33 +08:00

147 lines
13 KiB
Markdown

# Direct URL Or File Entry
## Problem
The tab bar `+` menu only offers fixed actions: terminal, browser, new markdown, and open markdown in `src/renderer/src/components/tab-bar/TabBar.tsx`. It has no text entry point for a user who already knows the URL or file path they want.
Quick Open already loads files for the active worktree through `listRuntimeFiles`, watches the active SSH target status, excludes nested linked worktrees, ranks via `prepareQuickOpenFiles`/`rankQuickOpenFiles`, and opens a selected match with `openFile`. That flow is modal and file-only. It does not live in the `+` menu, does not accept URLs, and cannot create a named new file from the typed query.
The runtime file client has the required local/SSH/runtime primitives, with caveats:
- `listRuntimeFiles(context, { rootPath, excludePaths })` returns relative paths only and can fail for auth, missing provider, ripgrep/size, or stale worktree reasons.
- `statRuntimePath(context, absolutePath)` is a one-path existence/type check.
- `createRuntimePath(context, absolutePath, 'file')` creates a single empty file and creates parent directories on the current local, SSH, and runtime-backed paths. Directory creation has separate no-clobber/recursive semantics elsewhere in the file stack, so v1 should create files directly and not expose directory creation as a separate action.
## Goal
Add an entry field at the top of the tab bar `+` menu so users can type:
1. A URL to open an Orca browser tab.
2. An existing file name/path to open an editor tab.
3. A new relative file path to create in the active worktree and open.
The behavior must work from both the titlebar tab strip and split-group tab strips. File operations must route through `RuntimeFileOperationArgs`; browser/editor creation must preserve the target group.
## Non-goals
- Do not replace global Quick Open.
- Do not add persisted launcher state.
- Do not support standalone directory creation.
- Do not open external URLs outside Orca.
- Do not make URL/file detection configurable.
## Design
1. Add a `TabBarCreateEntry` surface inside `TabBar`'s dropdown content above the fixed rows. Use the existing `DropdownMenu` shell, but render the input in a plain form/container, not as a `DropdownMenuItem`; Radix menu item typeahead/selection should not own text input keystrokes. Stop propagation only where required for input typing, and close the dropdown only after a successful submit.
2. Extend `TabBarProps` with a presentation callback that resolves success by returning and failure by throwing:
```ts
onOpenEntry?: (args: { query: string; worktreeId: string; groupId: string }) => Promise<void>
```
`TabBar` owns input text, pending state, focus, and dropdown close behavior. It does not create browser tabs or files. `groupId` should be `groupId ?? worktreeId`, matching the existing `resolvedGroupId` fallback.
3. Add a small hook/helper pair instead of duplicating Quick Open logic:
- `useTabEntryFileList` loads the file list when the menu opens, using the same inputs as `QuickOpen`: active worktree path, `getConnectionId(worktreeId)`, nested worktree exclusions, and active SSH target status. It cancels stale requests on close or key changes.
- A pure classifier/opening helper accepts the query, file-list snapshot, load/error state, worktree metadata, runtime context, and target group.
4. Wire the callback in both owners:
- `Terminal.tsx` titlebar fallback resolves the current active worktree and target group the same way `handleNewTab` / `handleNewBrowserTab` do.
- `useTabGroupWorkspaceModel` passes its explicit `worktreeId` and `groupId`. Do not rely on ambient `activeGroupIdByWorktree`; split-group `+` can be invoked from an unfocused group.
5. Classify submissions in this order:
- Empty after trim: reject inline.
- Explicit URL: accept only `http://` and `https://` URLs with a parseable host.
- Existing file: once the file-list snapshot is ready, normalize query separators for matching, prefer exact relative-path match, then exact basename match, then `rankQuickOpenFiles`. Before opening, `statRuntimePath` the matched absolute path and reject directories/stale missing matches instead of blindly opening stale list entries.
- Host-like URL: only after there is no existing file match, accept strict bare hosts such as `example.com`, `localhost:3000`, or `127.0.0.1:3000`, normalized to `https://...` when no scheme is present. Do not run host-like URL parsing for bare input containing `/` or `\`; `new URL('https://docs/readme.md')` parses, so parsing alone is not a path/file guard. Also reject common source/document filename extensions such as `md`, `ts`, `tsx`, `js`, `jsx`, `json`, `yml`, `yaml`, `toml`, `css`, `html`, and `py` so `README.md` and `src/foo.test.ts` stay file/create candidates.
- New file: only after file listing has completed successfully with no existing-file match and no host-like URL match. Treat the query as a relative worktree path. If listing fails, allow explicit `http://` / `https://` URLs only; keep bare host-like inputs blocked because they cannot be disambiguated from files.
6. Validate new file paths before joining:
- Reject POSIX absolute paths, Windows drive paths, UNC paths, `~`, empty path, trailing slash, control characters, `.` / `..` segments, and empty raw segments such as `a//b`.
- Normalize `\` to `/` only after absolute-path checks, then run segment validation on the normalized path too so traversal like `a\..\b` is still rejected.
- Build the absolute path with `joinPath(worktreePath, relativePath)`, then create with `createRuntimePath(context, absolutePath, 'file')`.
- On `EEXIST` / "exists", immediately `statRuntimePath`; if it is now a file, open it. If it is a directory, show an error. This handles another window/process winning the create race.
7. Open actions:
- URL: in paired web clients, call `createWebRuntimeSessionBrowserTab({ worktreeId, url, targetGroupId: groupId })`; otherwise call `createBrowserTab(worktreeId, url, { activate: true, targetGroupId: groupId, title: url })`. Do not use `openNewBrowserTabInActiveWorkspace`; it only opens the default URL.
- Existing/new file: call `openFile(fileInfo, { preview: false, targetGroupId: groupId })` with `language: detectLanguage(relativePath)`. Include the active runtime environment owner as `runtimeEnvironmentId` through the normal `openFile` fallback; do not suppress runtime ownership.
8. Preserve current fixed actions. Existing `New Terminal`, `New Browser Tab`, `New Markdown`, `Open Markdown...`, and quick-launch rows remain below the entry and keep their current shortcuts/icons.
9. Surface errors with existing toast or compact inline text. Keep the dropdown open and preserve the query on validation/runtime errors. For Quick Open's special ripgrep guidance, either extract its parser/UI deliberately or show the cleaned error string; do not duplicate a private parser inline.
## Data Flow
- User opens the `+` menu.
- `TabBarCreateEntry` focuses the input and `useTabEntryFileList` starts/reuses the menu-local file-list request.
- User types; the menu may show the best existing-file match or "create file" affordance based on the current snapshot.
- Enter calls `onOpenEntry({ query, worktreeId, groupId })`.
- Helper classifies and dispatches:
- URL -> browser tab creation with target group.
- Existing file -> stat matched path -> `openFile(..., { targetGroupId })`.
- New file -> validate -> `createRuntimePath(..., 'file')` -> `openFile(..., { targetGroupId })`.
- Success closes the dropdown. Failure keeps it open.
## Edge Cases
- No active worktree: disable the entry, including URL entry, because Orca browser tabs are worktree-scoped.
- SSH/runtime connection not ready: mirror Quick Open by keying the list request on active target status. Non-URL submissions are disabled while loading/connecting to avoid creating a duplicate before the real list arrives.
- File listing failure: explicit `http://` / `https://` URL submissions still work; file and bare host-like submissions are blocked with the cleaned list error.
- Ambiguous host-like/file names: an existing listed file wins over bare host-like URL normalization. Explicit `http://` or `https://` input is the escape hatch when the user wants a browser tab despite a file-name collision.
- File list stale because another window/process added or removed a file: stat before opening matched files; handle create `EEXIST` by stat-and-open.
- Existing directory match: show an error; do not open as an editor tab.
- Internal spaces in file paths are allowed. Leading/trailing whitespace is trimmed. Control characters are rejected.
- Windows-style separators match existing relative paths after normalization, but Windows absolute and UNC paths are rejected for creation.
- Duplicate Enter while pending is disabled.
- Browser creation in paired web/mobile clients must use `createWebRuntimeSessionBrowserTab`; local desktop uses `createBrowserTab`.
- External file-watch invalidation is not required for v1. The menu-local list reloads on each open and on worktree/connection/status changes; successful create can close the menu without mutating the list.
## Test Plan
- Unit test URL classification: schemes, host-like domains, localhost/IP ports, listed file named `example.com` winning over host-like normalization, `README.md`/`readme.md`, `src/foo.test.ts`, `docs/readme.md`, whitespace, and invalid schemes.
- Unit test path validation: Windows/POSIX absolute paths, UNC, `~`, traversal, empty segments, trailing slash, control characters, spaces, and separator normalization.
- Unit test existing-file selection with `prepareQuickOpenFiles`/`rankQuickOpenFiles`: exact path beats basename, basename beats fuzzy, stale stat failure blocks open, directory stat blocks open.
- Unit/helper test new-file creation with `RuntimeFileOperationArgs`, `createRuntimePath`, `statRuntimePath`, EEXIST stat-and-open, and SSH/runtime connection context.
- Component test `TabBar`: input renders above fixed rows, focuses on open, typing does not close the menu, Enter awaits the callback, success closes, failure preserves text, fixed actions still fire.
- Store/model tests: titlebar and split-group callbacks pass `targetGroupId`; URL creation uses `createWebRuntimeSessionBrowserTab` in web runtime and `createBrowserTab` otherwise.
- Electron validation: open URL, open existing file by exact path/name, create nested new file, invalid traversal/absolute path error, and smoke-test existing menu actions.
## UI Quality Bar
- Follow `docs/STYLEGUIDE.md` and existing `DropdownMenu`/`Input` tokens. Do not add custom colors, shadows, or a modal-like panel inside the menu.
- The input is the first focus target and must not break menu keyboarding.
- The menu remains compact; loading, match preview, create preview, and error rows fit at the current menu width without overlap or awkward height jumps.
- Long typed paths scroll/truncate within the input; fixed rows retain icons, shortcuts, hover states, and dense spacing.
## Review Screenshots
Attach evidence to the PR conversation; do not commit images.
1. `+` menu open in a normal workspace with the entry focused and fixed rows visible.
2. Typed URL state.
3. Typed existing-file query with visible best match.
4. Typed new-file path/create state.
5. Rejected absolute/traversal path error.
6. Adjacent-feature smoke: fixed menu rows still visible and aligned.
## Rollout
1. Add classifier/path-validation helper and tests.
2. Add menu-local file-list hook by extracting the reusable Quick Open loading inputs, not by copying private UI-only parsing.
3. Add `TabBarCreateEntry` and wire it into `TabBar`.
4. Add titlebar and split-group callbacks.
5. Add component/store tests.
6. Run typecheck, lint, targeted tests, UI review, and Electron validation with screenshots.
## Lightweight Eng Review
- Scope: Correctly scoped to the tab bar `+` menu. The implementation must not mutate Quick Open behavior except for extracting reusable search/listing code.
- Architecture/data flow: Good if `TabBar` stays presentational and all worktree/runtime/group decisions live in owners plus a shared helper. The original `onOpenEntry(query, groupId?)` shape was under-specified; include `worktreeId` and a resolved `groupId`.
- Failure modes: Must explicitly handle loading list, failed list, stale list, directory matches, create races, no active worktree, no SSH provider yet, and paired web runtime browser creation.
- Feasibility: Specific URL browser tabs cannot go through `openNewBrowserTabInActiveWorkspace` because that action uses the default URL. `createRuntimePath(..., 'file')` is feasible and creates parent directories for current local/SSH/runtime paths, but do not expose recursive directory creation as a separate v1 behavior.
- Concurrency/invalidation: A menu-local file list is acceptable if every matched file is statted before open and `EEXIST` is handled on create. No global cache is needed.
- Performance/blast radius: Listing on menu open has Quick Open's cost class, but the menu is more casually opened than the modal; cancel stale loads and key requests tightly by worktree path, connection, exclusions, and SSH status.
- Tests: Add pure helper tests first; then component and store/model tests. Electron screenshots are required because Radix menu focus/input behavior is the highest-risk UI part.
- Residual risk: Host-like URL heuristics can surprise users. Keep the heuristic narrow and prefer file matches over host-like normalization whenever the query contains path separators or matches a listed file.