Skip to content

Share the Finder UI between the PWA and the ClassicStack SPA - #1

Merged
pgodwin merged 12 commits into
mainfrom
feature/shared-finder-host
Aug 21, 2026
Merged

Share the Finder UI between the PWA and the ClassicStack SPA#1
pgodwin merged 12 commits into
mainfrom
feature/shared-finder-host

Conversation

@pgodwin

@pgodwin pgodwin commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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:

# ClassicStack/third_party/README.md
git submodule add https://github.com/ObsoleteMadness/ClassicStack-web.git third_party/classicstack-web
// ClassicStack/adapter/control/http/ui/vite.config.ts
'classicstack-web': path.join(webRoot, 'src'),
// ClassicStack/adapter/control/http/ui/tsconfig.json
"classicstack-web/*": ["../../../../third_party/classicstack-web/src/*"]

package.json exports declares 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: FinderHost

The 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>"]
Loading
PWA (AfpFinderHost) SPA (GoFinderHost)
Transport Web Serial → TashTalk → LocalTalk fetch → Go control API
Protocol stack In-browser: DDP/ATP/ASP/AFP Server-side: AFP, SMB, NCP, EtherDFS
Session state In the browser tab On the server
Catalog RemoteVfs over the AFP client HttpCatalog over the control API

The 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

  • Protocol-neutral FinderHost so 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.
  • Catalog capabilitiesNodeRef, VolumeIdentity and per-backend CatalogCapabilities (AFP, SMB, NCP, EtherDFS, local share), re-exported from finder/index as the shared public contract. This unblocks ClassicStack's CI, which already imported NodeRef, CatalogCapabilities, and classicstack-web/ui/settings-icons before they had ever left this working tree.
  • Multiple open servers at once, plus a Settings window.
  • Finder menus — File and View split out of the window with shared shortcut rendering.
  • Windows resource tools — PE/NE icon extraction and a Windows resource explorer, alongside the existing Mac resource fork explorer.
  • Zip export, a pluggable extension-editor store, and a Macintosh codec registry so SIT and rez stay replaceable.

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:

pkt
56 FPLogin — succeeds
61 FPGetSrvrParms — volumes listed
73 FPLogout ← client discards the authenticated session
78 CloseSession
87 OpenSession — fresh, unauthenticated
92 FPOpenVol BlueSCSI Mac Plus
97 -5023 user not authenticated

connectServerWithLogin has a fast path for endpoints already in loggedInEndpoints: it calls beginRemote purely to refresh the server name and volume list, then mounts the volume without logging in again. That is correct against GoFinderHost, where the session lives on the server and beginRemote is a cheap query. Against AfpFinderHost the session lives in the browser, so beginRemote's unconditional close() logged it out — opening any share after signing in always failed.

Fixes:

  • 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. beginRemote is now idempotent on both hosts, which is what the shared caller assumed all along.
  • finder-window: only overwrite knownVolumes when beginRemote returns 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.
  • New afp-finder-host.test.ts covering 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 --noEmit clean; npx vitest run — 55 files, 384 passed, 1 skipped.

The -5023 path 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.md currently pins the submodule to feature/shared-finder-host, and make spa falls back to a sibling ../ClassicStack-web checkout or clones WEB_REF (default feature/shared-finder-host). Both want to move to main once this lands.

🤖 Generated with Claude Code

pgodwin and others added 12 commits August 17, 2026 09:11
…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>
@pgodwin pgodwin changed the title Shared Finder host, multi-server sessions, and an AFP mount regression fix Share the Finder UI between the PWA and the ClassicStack SPA Aug 21, 2026
@pgodwin
pgodwin merged commit e8a0dce into main Aug 21, 2026
1 check passed
@pgodwin
pgodwin deleted the feature/shared-finder-host branch August 21, 2026 05:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant