mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 00:44:49 +00:00
feat(app): make Explorer a first-class pane host (#3826)
This commit is contained in:
@@ -62,6 +62,10 @@ The button is `<Button>` (`packages/app/src/components/ui/button.tsx`). It has f
|
||||
|
||||
`ghost` is structural and non-committal — no border, no fill. Back arrows, header toggles, "Load more" footers (`packages/app/src/screens/sessions-screen.tsx:54-63`), more-affordances. Ghost is used when the affordance is part of the chrome, not a decision.
|
||||
|
||||
Header and toolbar controls use `interactionHighlight` for hovered, pressed, open, and selected
|
||||
backgrounds. It is a translucent semantic fill so the same control works over the main surface and
|
||||
the sidebar. Apply it as `backgroundColor`; setting `opacity` on the control also fades its content.
|
||||
|
||||
`destructive` is filled with `destructive`. It only appears inside a confirm. The button on the page is `outline`; the destructive button is the confirm button inside the dialog.
|
||||
|
||||
Sizes: `xs` for ultra-tight inline triggers. `sm` for any button sitting in a row. `md` is the page default. `lg` is reserved for large standalone CTAs.
|
||||
@@ -155,6 +159,8 @@ On a narrow desktop route, app navigation yields to the rendered content topolog
|
||||
|
||||
Electron window controls are top-corner obstructions, not a compact-layout condition. Rendered surfaces declare which top corners they physically occupy; only those corners receive clearance. Full-window overlays redeclare both corners. A focused split pane owns both corners; if focus restoration temporarily exposes the full split tree, the split boundary reserves one top strip instead of assigning a control rectangle to an arbitrarily narrow leaf. The 720px desktop breakpoint preserves the default 320px sidebar and target 400px center width when the Explorer is closed; it is product policy, not an obstruction gate.
|
||||
|
||||
Windows and Linux controls are fixed window chrome, outside scrolling header content. A tab rail that reaches them ends at their obstruction and shows the shared overflow fade. On macOS, the Explorer toggle occupies a fixed top-right window slot so opening and closing Explorer does not move the pointer target.
|
||||
|
||||
A new list+detail feature copies the settings shell. A new workspace-shaped feature copies the workspace shell. Inventing a third shape happens in design review, not in a PR.
|
||||
|
||||
---
|
||||
|
||||
+13
-2
@@ -242,13 +242,13 @@ focused invalidation capture; React Native's generated stacks make that mode hig
|
||||
Set `PASEO_PROFILE_TRACE_FOCUS=1` to include focus targets, durations, and JavaScript call stacks in
|
||||
the scenario report. This mode wraps `HTMLElement.focus`, so use it only for diagnosis.
|
||||
|
||||
For the desktop Side panel toggle, run the app against the root checkout's daemon and use:
|
||||
For the desktop Explorer sidebar toggle, run the app against the root checkout's daemon and use:
|
||||
|
||||
```bash
|
||||
npm run profile:explorer-toggle --workspace=@getpaseo/app
|
||||
```
|
||||
|
||||
The harness verifies port `6768`, opens the Paseo workspace, creates and warms the Side panel pane,
|
||||
The harness verifies port `6768`, opens the Paseo workspace, creates and warms the Explorer pane,
|
||||
records an idle control, then measures settled and 50 ms burst Cmd+E toggles. It reports
|
||||
input-to-DOM and input-to-paint latency, React commits, mounts, unmounts, and DOM mutations. Set
|
||||
`PASEO_PROFILE_TRACE_PATH=/tmp/explorer-toggle.trace.json` or
|
||||
@@ -278,6 +278,17 @@ That report includes input and composer height changes plus React work grouped i
|
||||
stream, and ancestor/root scopes. Ancestor/root timings include descendant work because the Profiler
|
||||
boundaries are nested. A printable key after an empty newline should not change either height.
|
||||
|
||||
### Preview Windows and Linux window controls on macOS
|
||||
|
||||
Desktop development can replace native macOS traffic lights with Paseo's custom controls:
|
||||
|
||||
```bash
|
||||
PASEO_DESKTOP_WINDOW_CONTROLS=windows npm run dev:desktop
|
||||
PASEO_DESKTOP_WINDOW_CONTROLS=linux npm run dev:desktop
|
||||
```
|
||||
|
||||
The override is rejected in packaged builds. Restart the desktop process when changing it.
|
||||
|
||||
### Desktop macOS compositor watchdog
|
||||
|
||||
macOS display sleep can leave Chromium's GPU-process display link — the vsync
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Explorer sidebar and side pane
|
||||
|
||||
The Explorer sidebar and the side pane share panel implementations, but they have different shell
|
||||
contracts.
|
||||
|
||||
| Surface | Purpose | Lifecycle |
|
||||
| ---------------- | ---------------------------- | ------------------------------------------ |
|
||||
| Explorer sidebar | Files and Changes navigation | Cmd+E shows or hides the dedicated dock |
|
||||
| Side pane | Ordinary workspace content | Created and closed like any workspace pane |
|
||||
|
||||
## Panel host contract
|
||||
|
||||
Every desktop panel registers its supported `PaneHost` values and presentation. Launchers derive
|
||||
fixed-target labels and icons from that registration, filter by host, and never substitute one
|
||||
panel type for another. Tab moves reject unsupported destinations, and placement resolves only to
|
||||
a compatible pane.
|
||||
|
||||
Files and Changes are the Explorer defaults and its singleton navigation views. Other compatible
|
||||
tabs, including agents, terminals, files, and diffs, can move between Explorer and main panes.
|
||||
Keep panel implementations independent of either shell. `WorkspacePanelHost` owns mounting and
|
||||
retention, while each shell owns its tabs, focus, dragging, resizing, and shortcuts.
|
||||
|
||||
## Explorer sidebar
|
||||
|
||||
`packages/app/src/workspace-tabs/explorer-sidebar.ts` owns show, hide, toggle, and view selection.
|
||||
On desktop, the shell is rendered outside the workspace split canvas so it divides the full
|
||||
workspace, including the header. It has its own persisted width and resize handle. Main-pane splits
|
||||
never read or modify that width.
|
||||
|
||||
The persisted layout still contains the Explorer pane so tabs survive reloads. The renderer removes
|
||||
that pane from the workspace split tree and docks it separately. Persisted identifiers retain the
|
||||
literal `"explorer"` pane id and `explorerPaneIdByWorkspace` key for compatibility.
|
||||
|
||||
The tab rail has no inline add or close controls. Its context menu opens a New Tab launcher and
|
||||
toggles the singleton Files and Changes views. Individual tab menus close instances or move
|
||||
compatible tabs to main. Explorer tabs can be reordered, but the dock cannot be split. Selecting
|
||||
an Explorer tab does not change workspace focus.
|
||||
|
||||
Cmd+E selects Changes for Git workspaces and Files otherwise. Compact layouts use the combined
|
||||
full-screen Explorer overlay and close it after a file opens. Wide native layouts without pane
|
||||
splits use the same combined content in a resizable inline dock; opening a file leaves that dock
|
||||
visible. Both presentations keep their selection in the panel store and reuse the layout store's
|
||||
per-workspace Explorer width. They do not create a second Explorer lifecycle.
|
||||
|
||||
## Side pane
|
||||
|
||||
`packages/app/src/workspace-tabs/open-beside.ts` owns content opened beside the user's work. The
|
||||
layout store remembers one ordinary pane per workspace. The first side open creates a full-height
|
||||
right split around the workspace root; later side opens reuse it.
|
||||
|
||||
Closing the pane or moving away its final tab removes it normally and clears the remembered id. A
|
||||
later side open creates a new pane. There is no hidden side-pane lifecycle.
|
||||
|
||||
Placement intent still controls existing tabs:
|
||||
|
||||
| Mode | New target | Existing target |
|
||||
| --------- | --------------------------- | --------------------------------- |
|
||||
| `pane` | opens in the requested pane | moves to the requested pane |
|
||||
| `prefer` | opens in the requested pane | stays where the user placed it |
|
||||
| `focused` | opens in the focused pane | focuses it where it already lives |
|
||||
| `ambient` | opens in a compatible pane | focuses it where it already lives |
|
||||
|
||||
Explicit **Open to Side** uses `pane`. Implicit opens use `prefer`, so a preference affects only a
|
||||
new target and never yanks an existing tab out of a user-selected pane.
|
||||
|
||||
## Routing preferences
|
||||
|
||||
Desktop **Settings → Layout → Open in side pane** has independent switches for Explorer Files,
|
||||
Explorer Changes, chat files, diff files, subagents, pull requests, and Changes links. All switches
|
||||
default off. Mobile ignores them.
|
||||
|
||||
Panels request an implicit open through the narrow `openPreferredTarget(target, source)` pane
|
||||
contract. Entry points outside panels use `openPreferredWorkspaceTarget`. Do not branch on a
|
||||
specific shell inside a panel.
|
||||
+2
-1
@@ -19,7 +19,8 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here
|
||||
- **MR** — GitLab merge request. UI label for GitLab change requests only; do not use MR for GitHub/Gitea/Forgejo.
|
||||
- **Worktree** — Paseo-managed git worktree (`~/.paseo/worktrees/{name}`); also a `workspaceKind` value. User-facing creation treats it as the `worktree` workspace isolation choice. Code and `paseo.json` retain worktree terminology for git lifecycle implementation. Forbidden: "Checkout" as a product synonym.
|
||||
- **Repository / Remote** — Internal Git observations. They may produce mutable kind, branch, and project-grouping metadata but never the host-local project ID, root, display name, or workspace membership. No UI label.
|
||||
- **Side panel** — The companion surface beside the user's work, holding Changes, Files, the pull request, and tabs an agent opens alongside its output. UI: "Side panel". Code: `packages/app/src/workspace-tabs/side-panel.ts`; the persisted pane id and layout key keep the pre-rename `explorer` spelling. See [side-panel.md](side-panel.md). Forbidden: "Explorer" as the name of the surface — **File Explorer** is only the Files view inside it.
|
||||
- **Explorer sidebar** — The dedicated navigation dock containing the Files and Changes trees. UI: "Explorer sidebar". Code: `packages/app/src/workspace-tabs/explorer-sidebar.ts`; persisted pane identifiers retain the `explorer` spelling. See [explorer-sidebar.md](explorer-sidebar.md).
|
||||
- **Side pane** — An ordinary right-side workspace pane used for content opened beside the user's work. It closes and participates in workspace splits like every other main-host pane. See [explorer-sidebar.md](explorer-sidebar.md#side-pane). Forbidden: "Side panel".
|
||||
- **Directory-backed surface** — A right-sidebar surface whose content is determined by the workspace's `cwd`, so two workspaces on the same directory see identical content: git diff/status, forge change-request info, file preview/explorer contents. Keyed by `(serverId, cwd)`, never `workspaceId`. See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned).
|
||||
- **Workspace-owned state** — Per-workspace state that never leaks to a same-`cwd` sibling: tabs, agents, terminals, panes, title, plus review drafts, diff-mode overrides, composer attachments, and file-explorer open/expand state. Keyed by `workspaceId` (`cwd` only as a fallback for old payloads). See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned).
|
||||
- **Workspace status bucket** — Aggregate activity signal for a workspace row. Same-`cwd` workspaces intentionally share agent and terminal status buckets, while tab, agent, and terminal visibility remains scoped by `workspaceId`.
|
||||
|
||||
@@ -165,6 +165,10 @@ RPC. Snapshot DTOs are deeply readonly and frozen at runtime so plugin code cann
|
||||
app state or a memoized selection. Panels use one persisted
|
||||
`plugin` workspace-tab target, so reload, disable, removal, and restoration resolve through the
|
||||
current installed-plugin catalog. A missing contribution renders unavailable inside the tab.
|
||||
Panels declare `locations: ["workspace", "explorer"]` to opt into Explorer hosting; omission means
|
||||
workspace only. Location controls hosting, not context. An agent panel target keeps its `agentId`
|
||||
when moved between hosts. Explorer configuration can create workspace-context panels and remove
|
||||
existing agent-context instances, but it cannot create an agent panel without an agent-aware command.
|
||||
|
||||
Command Center callbacks use the selected host's existing `PaseoApi` for normal Paseo operations.
|
||||
They use typed plugin RPC only for plugin-specific backend work. Navigation is limited to the
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# Side panel
|
||||
|
||||
The companion surface beside the user's work: Changes, Files, the pull request, and
|
||||
anything an agent asks to show alongside its output.
|
||||
|
||||
Everything that opens, reveals, hides, or asks about it goes through
|
||||
`packages/app/src/workspace-tabs/side-panel.ts`. Nothing else branches on which
|
||||
surface the current layout uses.
|
||||
|
||||
## Three renderings, two facts
|
||||
|
||||
| Layout | Rendering |
|
||||
| ---------------------- | ----------------------------------------------- |
|
||||
| compact | slide-over overlay, owned by the panel store |
|
||||
| non-compact + splits | dedicated right pane, owned by the layout store |
|
||||
| non-compact, no splits | ordinary tabs in the focused pane |
|
||||
|
||||
The third case is the one that bites. A native tablet renders one pane at a time and
|
||||
the tab row only lists the focused pane's tabs, so anything routed into a separate
|
||||
side panel pane there disappears with no way back.
|
||||
|
||||
## Placement
|
||||
|
||||
Where a tab lands is an intent the caller declares, not something the store infers
|
||||
from a `paneId` that happens to be in scope. `WorkspaceTabPlacement`
|
||||
(`packages/app/src/stores/workspace-layout-actions.ts`) has four modes, and the
|
||||
difference between them only shows up when the tab is already open somewhere:
|
||||
|
||||
| Mode | New tab | Tab already open |
|
||||
| --------- | ----------------- | ---------------------------- |
|
||||
| `pane` | opens in the pane | **moves** to the pane |
|
||||
| `prefer` | opens in the pane | stays where the user left it |
|
||||
| `focused` | focused pane | focuses it in place |
|
||||
| `ambient` | focused pane | focuses it in place |
|
||||
|
||||
`pane` is a pane-local affordance — that pane's `+` menu, its empty-state launcher.
|
||||
The user is placing the tab and expects it to arrive.
|
||||
|
||||
`prefer` is an implicit supporting open — an agent's file link or a failed workspace
|
||||
setup. It has an opinion about new tabs only. Yanking a tab out of the pane the user
|
||||
deliberately moved it to is the failure this mode exists to prevent.
|
||||
|
||||
`ambient` differs from `focused` in one rule: reconciliation opens tabs with nobody
|
||||
behind the click, and the side panel can hold focus from an earlier reveal, so an
|
||||
ambient open of something the user works _in_ (an agent, a terminal, a browser) is
|
||||
pushed out of the side panel. A `focused` open honours the focused pane whatever it
|
||||
is, because the user is standing in it.
|
||||
|
||||
Deduplication lives in `openTabInLayoutFocused`, so it obeys the placement intent for
|
||||
every caller. Do not re-place a tab after opening it.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
The side panel lives in the ordinary split tree, so it is a pane like any other until
|
||||
it empties or the user dismisses it:
|
||||
|
||||
- **Close pane** on the side panel **hides** it; its tabs are waiting on the next reveal.
|
||||
- **Close pane** on any other pane **removes** it.
|
||||
- Closing the Side panel's final tab hides the pane. Revealing it returns the empty launcher.
|
||||
This is close-only: claiming or dragging that tab to another pane leaves the empty Side panel
|
||||
visible.
|
||||
- Nothing takes the last visible pane away. `closePane` will not remove it, and
|
||||
neither `closePane` nor `hideSidePanel` will hide it — including the header toggle
|
||||
reaching `hideSidePanel` after every other pane is gone. The gate is one check in
|
||||
`setPaneHiddenInLayout`; do not add a second one upstream.
|
||||
|
||||
`closePane` in the layout store owns that distinction. React renders the button and
|
||||
does not decide what it means.
|
||||
|
||||
Dragging a pane's last tab into another pane collapses the source pane — except the
|
||||
side panel, which the user summons and expects to find again, split size and all.
|
||||
|
||||
## What the side panel never does
|
||||
|
||||
It does not seed itself. Revealing it opens an empty pane with the launcher; the user
|
||||
picks what goes in. Detecting a pull request makes the launcher's card available and
|
||||
opens nothing. A failed workspace setup adds a background tab to the main pane.
|
||||
Running and successful setup never seed tabs.
|
||||
|
||||
## Routing preference
|
||||
|
||||
**Settings → General → Open supporting tabs in the Side panel**, on by default,
|
||||
persisted in client app settings as `openSupportingTabsInSidePanel`. Off means new
|
||||
supporting tabs land in the focused pane instead. Mobile ignores it. Explicit actions
|
||||
— the header toggle, a pane's `+` menu, the empty-state launcher — are never subject
|
||||
to it. The Changes keyboard shortcut and the diff-stat pill above an agent composer are
|
||||
supporting opens: they follow the preference and focus an existing Changes tab wherever
|
||||
the user left it. When Changes is already visible in the Side panel, the diff-stat pill
|
||||
hides the panel without closing the tab. A different visible Side panel tab switches to
|
||||
Changes instead.
|
||||
|
||||
## The name
|
||||
|
||||
The product surface is the **Side panel**. **File Explorer** is still the name of the
|
||||
Files view inside it. Persisted identifiers keep their pre-rename spelling: the pane
|
||||
id is the literal string `"explorer"` (`SIDE_PANEL_PANE_ID`) and the persisted layout
|
||||
key is `explorerPaneIdByWorkspace`. The persisted state schema is strict, so renaming
|
||||
either would fail every saved blob and wipe the layout it describes.
|
||||
Reference in New Issue
Block a user