Share the Finder UI between the PWA and the ClassicStack SPA - #1
Merged
Conversation
…inder UI. FinderWindow now talks RemoteEndpoint/Catalog instead of NBP and AFP client types, package.json exports the shared surface, and the TashTalk app implements the new host. Co-authored-by: Cursor <cursoragent@cursor.com>
…is clean. Co-authored-by: Cursor <cursoragent@cursor.com>
… Info. The Go SPA reuses this chrome; operators need the same on-the-fly archive and AppleDouble zip actions without the TashTalk Advanced menu. Co-authored-by: Cursor <cursoragent@cursor.com>
…ble. Finder Expand and resource decompress now go through pluggable codecs, so a later package split can ship a third-party StuffIt expander or rez decoder without forking the UI. Co-authored-by: Cursor <cursoragent@cursor.com>
Callers can section locations and stamp protocol pills, so ClassicStack can label shares AFP/SMB/NCP/EDFS and group LAN clients by AppleTalk, SMB, NetWare, and EtherDFS. Co-authored-by: Cursor <cursoragent@cursor.com>
The same dialog can persist mappings in browser storage or through a host API, so ClassicStack can edit the server’s Netatalk extmap without forking the UI. Co-authored-by: Cursor <cursoragent@cursor.com>
Local shares no longer show a nested volume or eject, and a heading refresh can rediscover just that service. Co-authored-by: Cursor <cursoragent@cursor.com>
Leaving Finder for another screen dismisses the prompt instead of leaving connect hanging. Co-authored-by: Cursor <cursoragent@cursor.com>
Auto-mounted volumes never called reload, so the window stayed empty, and local shares showed id:name in the path bar. Co-authored-by: Cursor <cursoragent@cursor.com>
…a Settings window. Split the AFP-over-WebSerial wiring out of main.ts into a new src/finder/ module (api.ts, api-catalog.ts, afp-finder-api.ts, afp-finder-host.ts, bind-catalog.ts, catalog-copy.ts, progress.ts, types.ts) behind a protocol-neutral FinderAPI/CatalogWithBackend interface, and publish it plus the protocol/services/transport layers as package.json subpath exports. ClassicStack's Go control-panel SPA implements the same FinderAPI over HTTP, so the two apps now share one Finder UI and one copy/move/expand engine instead of forking it. Let the sidebar hold more than one live connection at once: endpoints are now either catalogs themselves (a ClassicStack share, a FUSE/WinFsp mount) or servers that list volumes as children, tracked per-endpoint (loggedInEndpoints/knownVolumes/openedVolumeKeys) instead of assuming a single remote session. Disconnect (log out of a server) and Eject (unmount one volume) are now separate actions, drag-and-drop targets key off data-share-key instead of positional volume indexes, and browser-history navigation can reconnect to whichever share a restored URL names. The login dialog is now protocol-aware (AFP/SMB/NCP) instead of hardcoding AFP UAM copy and the 8-char AFP password limit. Add a Settings window (settings-window.ts, settings-panel.ts) that gathers the prefs that used to live loose in the Advanced menu — hidden files, auto-expand, Finder icon reads, zip export style, extension editor, import/export preferences, reset environment, and Netboot — into one sectioned dialog, and factor menu open/close/escape/click-outside handling out of app-menubar.ts into a shared menu-bar-track.ts. Give the AFP client real multi-volume sessions: FPCopyFile support for server-side copies, and closeVolume() to release one volume (DT ref + desktop-info caches) without logging out, replacing the old assumption that only one volume is ever open. finder-window detects when source and destination share a backend (isCatalogWithBackend) and routes copies through the native copyFrom/expandNode paths instead of a client-side byte shuffle. Also: gate folder Icon\r lookups on the HAS_CUSTOM_ICON Finder flag instead of just the presence of a findChild callback, so folders using the default glyph are never probed; make the default Finder view (icon/list/column) a persisted preference; and rebrand the README from "ClassicStackWeb" to "ClassicStack-Web" with a pointer to the full ClassicStack project. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…resource tools Publishes the Finder host surface the ClassicStack-go SPA already imports but that had never left this working tree, which is why its CI could not resolve NodeRef, CatalogCapabilities, or classicstack-web/ui/settings-icons. - fs/catalog-caps: NodeRef, VolumeIdentity and per-backend CatalogCapabilities (AFP, SMB, NCP, EtherDFS, local share) plus the ref key helpers, re-exported from finder/index as the shared public contract. - fs/capability-catalog, fs/volume-chrome: capability-aware catalog wrapper and volume chrome/URI helpers. - fs/zip-export, fs/winicon: zip export and PE/NE icon extraction. - ui/finder-file-menu, ui/finder-view-menu, ui/menu-shortcut: Finder File and View menus split out of the window with shared shortcut rendering. - ui/settings-icons, ui/win-resource-explorer: Settings window iconography and the Windows resource explorer. - icons/classic, icons/ui: artwork backing the above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening a share always failed with FPOpenVol UserNotAuth (-5023). A pcap shows the client sending FPLogout + CloseSession, opening a fresh ASP session, then issuing FPOpenVol on it with no FPLogin in between. connectServerWithLogin has a fast path for endpoints already in loggedInEndpoints that calls beginRemote purely to refresh the server name and volume list, then mounts the volume without logging in again. That holds for the Go SPA host, where the session lives server-side and beginRemote is a cheap query. On the web PWA the session lives in the browser, so beginRemote's unconditional close() threw the login away. - afp-finder-host: beginRemote reuses a live authenticated session for the same endpoint instead of tearing it down, and reports the volumes it already knows rather than a hardcoded empty list. - finder-window: only overwrite knownVolumes when beginRemote returns a non-empty list, so the cached volume list survives a fast-path connect. - finder-window: a failed mount now drops the cached login and falls through to the sign-in loop, so a session the server timed out no longer leaves the share unopenable until the user disconnects by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch turns ClassicStack-web from a standalone PWA into the shared Finder UI for two different applications: this PWA, and the admin SPA served by ClassicStack (Go). One Finder window, one set of dialogs, one catalog model — two very different backends underneath.
How the sharing works
ClassicStack consumes this repo as a git submodule and aliases it straight to source — there is no npm publish and no intermediate build artifact:
package.jsonexportsdeclares the public surface (~60 entry points:ui/*,finder/*,fs/*,protocol/*,services/*,transport/*). Both apps typecheck against the same TypeScript sources, so a break in the contract surfaces in CI on both sides rather than at runtime.The seam:
FinderHostThe Finder window knows nothing about AFP, HTTP, or Web Serial. Everything backend-shaped goes through one protocol-neutral interface (
src/ui/finder-host.ts) — discover, login, mount, catalog:graph TD subgraph shared["ClassicStack-web/src — shared source"] FW["ui/finder-window<br/>dialogs · menus · Get Info<br/>resource explorers"] FH{{"ui/finder-host<br/>FinderHost contract"}} CAT["fs/catalog-caps<br/>NodeRef · VolumeIdentity<br/>CatalogCapabilities"] FW --> FH FW --> CAT end FH -.implemented by.-> AH["AfpFinderHost<br/>(this PWA)"] FH -.implemented by.-> GH["GoFinderHost<br/>(ClassicStack SPA)"] AH --> AFP["Web Serial → TashTalk → LocalTalk<br/>in-browser AFP client<br/><b>session lives in the browser</b>"] GH --> HTTP["HTTP → Go server<br/>AFP · SMB · NCP · EtherDFS<br/><b>session lives server-side</b>"]AfpFinderHost)GoFinderHost)fetch→ Go control APIRemoteVfsover the AFP clientHttpCatalogover the control APIThe SPA imports ~27 modules across this seam today —
ui/finder-window,ui/finder-host, the login / alert / name-conflict dialogs, the File and View menus, Get Info, the Mac and Windows resource explorers, the extension editor,finder/api-catalog,finder/types,fs/catalog-caps,fs/extension-map,util/prefs.What's in the branch
FinderHostso the Go SPA can reuse the Finder UI, with hosts owning sidebar groups, badges, context menus, and per-group network scans — the window renders, the host decides.NodeRef,VolumeIdentityand per-backendCatalogCapabilities(AFP, SMB, NCP, EtherDFS, local share), re-exported fromfinder/indexas the shared public contract. This unblocks ClassicStack's CI, which already importedNodeRef,CatalogCapabilities, andclassicstack-web/ui/settings-iconsbefore they had ever left this working tree.The regression this also fixes
Sharing one Finder across two session models is exactly where the last commit's bug came from. Opening a share always failed with
FPOpenVol UserNotAuth (-5023). A LocalTalk pcap shows the client throwing away its own login:FPLogin— succeedsFPGetSrvrParms— volumes listedFPLogout← client discards the authenticated sessionCloseSessionOpenSession— fresh, unauthenticatedFPOpenVol BlueSCSI Mac Plus-5023 user not authenticatedconnectServerWithLoginhas a fast path for endpoints already inloggedInEndpoints: it callsbeginRemotepurely to refresh the server name and volume list, then mounts the volume without logging in again. That is correct againstGoFinderHost, where the session lives on the server andbeginRemoteis a cheap query. AgainstAfpFinderHostthe session lives in the browser, sobeginRemote's unconditionalclose()logged it out — opening any share after signing in always failed.Fixes:
afp-finder-host:beginRemotereuses a live authenticated session for the same endpoint instead of tearing it down, and reports the volumes it already knows rather than a hardcoded empty list.beginRemoteis now idempotent on both hosts, which is what the shared caller assumed all along.finder-window: only overwriteknownVolumeswhenbeginRemotereturns a non-empty list — the hardcoded[]was wiping the cached volume list on every fast-path connect, leaving the sidebar showing just the one volume being opened.finder-window: a failed mount now drops the cached login and falls through to the sign-in loop, so a session the server timed out no longer leaves the share unopenable until the user disconnects by hand.afp-finder-host.test.tscovering session reuse, title-vs-id endpoint matching, and both reconnect cases.The general lesson for this seam: anything the shared Finder calls speculatively to refresh state has to be safe to call on a host that keeps real, stateful connections behind it.
Testing
npx tsc --noEmitclean;npx vitest run— 55 files, 384 passed, 1 skipped.The
-5023path was reproduced against a real Mac over LocalTalk and verified against the pcap; the rest of the branch is covered by the existing suite.Follow-up after merge
ClassicStack/third_party/README.mdcurrently pins the submodule tofeature/shared-finder-host, andmake spafalls back to a sibling../ClassicStack-webcheckout or clonesWEB_REF(defaultfeature/shared-finder-host). Both want to move tomainonce this lands.🤖 Generated with Claude Code