@venturewild/workspace 0.3.5 → 0.3.6

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.3.5",
3
+ "version": "0.3.6",
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": {
@@ -211,6 +211,12 @@ export function createCanvas({ baseDir } = {}) {
211
211
  const dir = baseDir || defaultCanvasDir();
212
212
  const blocksFile = path.join(dir, 'blocks.json');
213
213
  const themeFile = path.join(dir, 'theme.json');
214
+ // Server-side persistence for canvas state (layout, templates, user-theme).
215
+ // Fixes the localStorage-per-origin divergence bug (req-1 gave the user two
216
+ // origins → two divergent canvas states for the same workspace).
217
+ const layoutFile = path.join(dir, 'layout.json');
218
+ const templatesFile = path.join(dir, 'templates.json');
219
+ const userThemeFile = path.join(dir, 'user-theme.json');
214
220
 
215
221
  function ensureDir() {
216
222
  try {
@@ -298,10 +304,101 @@ export function createCanvas({ baseDir } = {}) {
298
304
  return theme;
299
305
  }
300
306
 
307
+ // --- canvas layout (which blocks exist and where) — one file per workspace ----
308
+
309
+ function getLayout() {
310
+ const v = readJsonSafe(layoutFile, null);
311
+ return v && typeof v === 'object' ? v : null;
312
+ }
313
+
314
+ function saveLayout(layout) {
315
+ // Validate: blocks is an array, layouts has an lg array.
316
+ const blocks = Array.isArray(layout?.blocks) ? layout.blocks : [];
317
+ const lg = Array.isArray(layout?.layouts?.lg) ? layout.layouts.lg : [];
318
+ const data = { blocks, layouts: { lg }, ts: Date.now() };
319
+ ensureDir();
320
+ try { writeJsonAtomic(layoutFile, data); } catch { /* best-effort */ }
321
+ return data;
322
+ }
323
+
324
+ // --- user templates (saved layouts) — global per install ----
325
+
326
+ function getTemplates() {
327
+ const v = readJsonSafe(templatesFile, null);
328
+ return Array.isArray(v) ? v : [];
329
+ }
330
+
331
+ function saveTemplates(templates) {
332
+ const data = Array.isArray(templates) ? templates : [];
333
+ ensureDir();
334
+ try { writeJsonAtomic(templatesFile, data); } catch { /* best-effort */ }
335
+ return data;
336
+ }
337
+
338
+ // --- user theme (mode + accent from the picker) — separate from the agent theme ---
339
+ // The agent theme (theme.json) holds tokens/wallpaper set by set_theme; the user
340
+ // theme holds the user's own mode+accent choice from the ThemePicker. They merge
341
+ // at render time (mergeAgentTheme in theme.js).
342
+
343
+ function getUserTheme() {
344
+ const v = readJsonSafe(userThemeFile, null);
345
+ return v && typeof v === 'object' ? v : null;
346
+ }
347
+
348
+ function saveUserTheme(raw) {
349
+ // Only persist allowlisted fields — mode, accent, hot, accentManual, tokens.
350
+ const accent = hexOr(raw?.accent);
351
+ const theme = {
352
+ mode: raw?.mode === 'dark' ? 'dark' : 'light',
353
+ accent: accent || '#0891b2',
354
+ hot: hexOr(raw?.hot) || (accent ? darkenHex(accent) : '#0e7490'),
355
+ accentManual: !!raw?.accentManual,
356
+ tokens: {},
357
+ };
358
+ if (raw?.tokens && typeof raw.tokens === 'object') {
359
+ for (const key of TOKEN_KEYS) {
360
+ const hex = hexOr(raw.tokens[key]);
361
+ if (hex) theme.tokens[key] = hex;
362
+ }
363
+ }
364
+ ensureDir();
365
+ try { writeJsonAtomic(userThemeFile, theme); } catch { /* best-effort */ }
366
+ return theme;
367
+ }
368
+
369
+ // Darken helper (standalone — can't import from theme.js which is client-only)
370
+ function darkenHex(hex, f = 0.16) {
371
+ const h = hex.replace('#', '');
372
+ const k = 1 - f;
373
+ const c = (i) => Math.max(0, Math.min(255, Math.round(parseInt(h.slice(i, i + 2), 16) * k)));
374
+ return `#${[0, 2, 4].map((i) => c(i).toString(16).padStart(2, '0')).join('')}`;
375
+ }
376
+
377
+ // --- batch get/save (used by /api/canvas/state) ---
378
+
379
+ function getState() {
380
+ return {
381
+ layout: getLayout(),
382
+ templates: getTemplates(),
383
+ userTheme: getUserTheme(),
384
+ };
385
+ }
386
+
387
+ function saveState(updates) {
388
+ const result = {};
389
+ if (updates.layout) result.layout = saveLayout(updates.layout);
390
+ if (updates.templates) result.templates = saveTemplates(updates.templates);
391
+ if (updates.userTheme) result.userTheme = saveUserTheme(updates.userTheme);
392
+ return result;
393
+ }
394
+
301
395
  return {
302
- dir, blocksFile, themeFile,
396
+ dir, blocksFile, themeFile, layoutFile, templatesFile, userThemeFile,
303
397
  listBlocks, getBlock, addBlock, updateBlock, removeBlock,
304
398
  getTheme, setTheme,
399
+ getLayout, saveLayout, getTemplates, saveTemplates,
400
+ getUserTheme, saveUserTheme,
401
+ getState, saveState,
305
402
  };
306
403
  }
307
404
 
@@ -1801,6 +1801,22 @@ export async function createServer(overrides = {}) {
1801
1801
  return c.json({ theme: canvas.getTheme() });
1802
1802
  });
1803
1803
 
1804
+ // Canvas state — the source of truth for layout, templates, and user-theme.
1805
+ // Fixes the localStorage-per-origin divergence (req-1 gave two origins for the
1806
+ // same workspace → divergent canvas). Server stores the canonical state; client
1807
+ // uses localStorage as a read-through cache.
1808
+ app.get('/api/canvas/state', (c) => {
1809
+ const forbidden = require(c, 'chat');
1810
+ if (forbidden) return forbidden;
1811
+ return c.json(canvas.getState());
1812
+ });
1813
+ app.post('/api/canvas/state', async (c) => {
1814
+ const forbidden = require(c, 'chatWrite');
1815
+ if (forbidden) return forbidden;
1816
+ const body = await c.req.json().catch(() => ({}));
1817
+ return c.json({ ok: true, ...canvas.saveState(body) });
1818
+ });
1819
+
1804
1820
  // The built site, served SAME-ORIGIN through this (already authed) server — no
1805
1821
  // squatted dev port, no mixed-content under the public proxy. The build dir
1806
1822
  // comes from preview.json (written by the agent's launch_preview / record_use).