@venturewild/workspace 0.6.29 → 0.6.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venturewild/workspace",
3
- "version": "0.6.29",
3
+ "version": "0.6.30",
4
4
  "description": "Claude Code Web — Replit/Lovable-style chat-first browser UI that wraps the AI agent already installed on your machine.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -65,6 +65,7 @@ import {
65
65
  proxyWsUpgrade,
66
66
  reconcilePreviews,
67
67
  previewUrlFor,
68
+ pickPreviewSlug,
68
69
  PREVIEW_ENDED_PAGE,
69
70
  } from './preview-proxy.mjs';
70
71
  import { createPreviewRails } from './preview-rails.mjs';
@@ -1107,6 +1108,10 @@ export async function createServer(overrides = {}) {
1107
1108
  // relay strips any client-forged X-Forwarded-* so the host can't be spoofed
1108
1109
  // from off-box. Populated by the dev-port watcher (Part B).
1109
1110
  const localPreviews = overrides.localPreviews || new LocalPreviewRegistry();
1111
+ // Most-recently-viewed workspace id (set by the auth middleware from
1112
+ // X-Workspace-Id) — the shared-preview watcher tags previews with the workspace
1113
+ // the user is actively collaborating in, so teammates can discover them.
1114
+ let lastActiveWorkspaceId = null;
1110
1115
  app.use('*', async (c, next) => {
1111
1116
  const token = previewTokenFromHeaders((n) => c.req.header(n));
1112
1117
  if (!token) return next();
@@ -1180,7 +1185,13 @@ export async function createServer(overrides = {}) {
1180
1185
  c.set('session', session);
1181
1186
  // Resolve the active workspace for this request (lobby M1). Per-tab via the
1182
1187
  // X-Workspace-Id header; absent/unknown → the boot default.
1183
- c.set('workspace', resolveWorkspace(c.req.header('x-workspace-id')));
1188
+ const xwid = c.req.header('x-workspace-id');
1189
+ const activeWs = resolveWorkspace(xwid);
1190
+ c.set('workspace', activeWs);
1191
+ // Track the most-recently-viewed workspace so the shared-preview watcher can
1192
+ // tag previews with the workspace the user is actually collaborating in
1193
+ // (not the boot-default launch folder). (tk-5d09bf04-5.)
1194
+ if (xwid && activeWs?.id) lastActiveWorkspaceId = activeWs.id;
1184
1195
  // Block the API for denied (non-localhost, unauthenticated) requests, but
1185
1196
  // let static assets + the public endpoints through so the SPA can still
1186
1197
  // load and prompt for sign-in. (Concern C1.)
@@ -3704,10 +3715,18 @@ export async function createServer(overrides = {}) {
3704
3715
  // shared slug for member discovery (the picker covers the multi-server edge).
3705
3716
  // Skipped under the test runner; tests drive reconcilePreviews directly.
3706
3717
  let previewTimer = null;
3707
- function defaultSharedSlug() {
3718
+ // Tag previews with the workspace the user is actively collaborating in (active
3719
+ // → sole shared workspace → boot-default), so a teammate's Live view discovers
3720
+ // them. Tagging only with the boot-default (a local launch folder) meant
3721
+ // cross-member discovery never fired. (tk-5d09bf04-5.)
3722
+ function previewSlug() {
3708
3723
  try {
3709
- const entry = getWorkspace(config.workspaceId, registryEnv);
3710
- return entry?.kind === 'shared' && entry.shared?.slug ? entry.shared.slug : null;
3724
+ return pickPreviewSlug({
3725
+ activeId: lastActiveWorkspaceId,
3726
+ bootDefaultId: config.workspaceId,
3727
+ resolve: (id) => getWorkspace(id, registryEnv),
3728
+ all: () => listWorkspaces(registryEnv),
3729
+ });
3711
3730
  } catch {
3712
3731
  return null;
3713
3732
  }
@@ -3721,7 +3740,7 @@ export async function createServer(overrides = {}) {
3721
3740
  detect: () => detectPreviewPorts(),
3722
3741
  registry: localPreviews,
3723
3742
  rails: previewRails,
3724
- slug: defaultSharedSlug(),
3743
+ slug: previewSlug(),
3725
3744
  initiator: config.account?.email || config.account?.slug || null,
3726
3745
  selfPort: config.port,
3727
3746
  });
@@ -204,6 +204,32 @@ export function previewUrlFor(shareBaseUrl, token) {
204
204
  return `https://${parts.join('.')}`;
205
205
  }
206
206
 
207
+ // Choose which shared-workspace slug to TAG a published preview with, so
208
+ // collaborators can discover it (the membership-gated `/api/preview/list` filters
209
+ // by this tag). A raw dev port can't be perfectly attributed to a workspace, so:
210
+ // 1. the workspace the user is actively in (last X-Workspace-Id) — strongest;
211
+ // 2. else the SOLE shared workspace, if the install has exactly one — the
212
+ // unambiguous common case (a small team in one shared workspace);
213
+ // 3. else the boot-default workspace (original behaviour; usually local → null).
214
+ // `resolve(id)` → a registry entry (with `kind`/`shared`) or null; `all()` → all
215
+ // entries. Pure (deps injected) so it's unit-testable without a live registry.
216
+ //
217
+ // WHY this exists: tagging with the boot-default alone (the install's launch
218
+ // folder, almost always a LOCAL workspace) meant cross-member discovery never
219
+ // fired — a teammate's preview never appeared in your picker. (tk-5d09bf04-5.)
220
+ export function pickPreviewSlug({ activeId, bootDefaultId, resolve, all }) {
221
+ const sharedSlugOf = (id) => {
222
+ if (!id) return null;
223
+ const e = resolve(id);
224
+ return e && e.kind === 'shared' && e.shared && e.shared.slug ? e.shared.slug : null;
225
+ };
226
+ const active = sharedSlugOf(activeId);
227
+ if (active) return active;
228
+ const shared = (all() || []).filter((w) => w && w.kind === 'shared' && w.shared && w.shared.slug);
229
+ if (shared.length === 1) return shared[0].shared.slug;
230
+ return sharedSlugOf(bootDefaultId);
231
+ }
232
+
207
233
  // ---------------------------------------------------------------------------
208
234
  // Dev-port reconcile (Part B) — one detection pass, kept pure for unit tests.
209
235
  // ---------------------------------------------------------------------------