codex-webstrapper 0.9.0 → 0.9.5

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": "codex-webstrapper",
3
- "version": "0.9.0",
3
+ "version": "0.9.5",
4
4
  "type": "module",
5
5
  "description": "Web wrapper for Codex desktop assets with bridge + token auth",
6
6
  "license": "MIT",
package/src/assets.mjs CHANGED
@@ -151,13 +151,24 @@ export async function ensureExtractedAssets({
151
151
  }
152
152
 
153
153
  export async function buildPatchedIndexHtml(indexPath) {
154
- const html = await fsp.readFile(indexPath, "utf8");
154
+ let html = await fsp.readFile(indexPath, "utf8");
155
155
  const shimTag = '<script src="/__webstrapper/shim.js"></script>';
156
156
 
157
157
  if (html.includes(shimTag)) {
158
158
  return html;
159
159
  }
160
160
 
161
+ // Replace or inject mobile viewport meta — the Electron app's default
162
+ // viewport lacks maximum-scale and user-scalable=no, causing iOS zoom issues
163
+ const viewportMeta =
164
+ '<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">';
165
+ const existingViewport = html.match(/<meta\s+name=["']viewport["'][^>]*>/i);
166
+ if (existingViewport) {
167
+ html = html.replace(existingViewport[0], viewportMeta);
168
+ } else if (html.includes("</head>")) {
169
+ html = html.replace("</head>", ` ${viewportMeta}\n</head>`);
170
+ }
171
+
161
172
  if (html.includes("</head>")) {
162
173
  return html.replace("</head>", ` ${shimTag}\n</head>`);
163
174
  }
package/src/auth.mjs CHANGED
@@ -126,10 +126,29 @@ export function createAuthController({ token, sessionStore, cookieName = SESSION
126
126
  );
127
127
  }
128
128
 
129
- function requireAuth(req, res) {
129
+ function requireAuth(req, res, parsedUrl) {
130
130
  if (hasValidSession(req)) {
131
131
  return true;
132
132
  }
133
+
134
+ // Auto-authenticate if a valid token is in the URL query string.
135
+ // This lets iOS "Add to Home Screen" bookmarks work — the saved URL
136
+ // includes ?token=X, so each launch re-authenticates automatically.
137
+ if (parsedUrl) {
138
+ const provided = parsedUrl.searchParams.get("token") || "";
139
+ if (provided && provided === token) {
140
+ const session = sessionStore.createSession();
141
+ const cookie = serializeCookie(cookieName, session.id, {
142
+ maxAgeSeconds: Math.floor(sessionStore.ttlMs / 1000),
143
+ httpOnly: true,
144
+ sameSite: "Lax",
145
+ path: "/"
146
+ });
147
+ res.setHeader("set-cookie", cookie);
148
+ return true;
149
+ }
150
+ }
151
+
133
152
  rejectUnauthorized(res);
134
153
  return false;
135
154
  }
@@ -153,7 +172,7 @@ export function createAuthController({ token, sessionStore, cookieName = SESSION
153
172
 
154
173
  res.statusCode = 302;
155
174
  res.setHeader("set-cookie", cookie);
156
- res.setHeader("location", "/");
175
+ res.setHeader("location", `/?token=${encodeURIComponent(provided)}`);
157
176
  res.end();
158
177
  }
159
178
 
@@ -7,6 +7,7 @@
7
7
  const mainMessageHistory = [];
8
8
  const CONTEXT_MENU_ROOT_ID = "__codex-webstrap-context-menu-root";
9
9
  const CONTEXT_MENU_STYLE_ID = "__codex-webstrap-context-menu-style";
10
+ const MOBILE_STYLE_ID = "__codex-webstrap-mobile-style";
10
11
 
11
12
  let ws = null;
12
13
  let connected = false;
@@ -317,6 +318,200 @@
317
318
  document.head.appendChild(style);
318
319
  }
319
320
 
321
+ // ---------------------------------------------------------------------------
322
+ // Mobile-responsive CSS overrides
323
+ // ---------------------------------------------------------------------------
324
+
325
+ function ensureMobileStyles() {
326
+ if (document.getElementById(MOBILE_STYLE_ID)) {
327
+ return;
328
+ }
329
+
330
+ const style = document.createElement("style");
331
+ style.id = MOBILE_STYLE_ID;
332
+ style.textContent = `
333
+ /* ==== Tablet & small screen fixes (max-width: 768px) ==== */
334
+ @media (max-width: 768px) {
335
+
336
+ /* Viewport stabilization */
337
+ html, body {
338
+ overflow-x: hidden !important;
339
+ -webkit-text-size-adjust: 100% !important;
340
+ }
341
+
342
+ /* Header overflow fix */
343
+ .h-toolbar {
344
+ max-width: 100vw !important;
345
+ overflow: hidden !important;
346
+ }
347
+
348
+ .h-toolbar button {
349
+ flex-shrink: 1 !important;
350
+ min-width: 0 !important;
351
+ overflow: hidden !important;
352
+ text-overflow: ellipsis !important;
353
+ white-space: nowrap !important;
354
+ }
355
+
356
+ /* Prevent iOS auto-zoom on focus (triggers when font-size < 16px) */
357
+ input, textarea, select, [contenteditable="true"] {
358
+ font-size: 16px !important;
359
+ max-width: 100% !important;
360
+ box-sizing: border-box !important;
361
+ }
362
+
363
+ /* Disable double-tap zoom */
364
+ * {
365
+ touch-action: manipulation;
366
+ }
367
+
368
+ [contenteditable="true"]:focus {
369
+ scroll-margin-bottom: 20px;
370
+ }
371
+
372
+ /* Terminal — constrain height */
373
+ [class*="terminal"],
374
+ [class*="Terminal"] {
375
+ max-height: 40vh !important;
376
+ }
377
+
378
+ /* Dialogs/modals — fit screen */
379
+ [role="dialog"] {
380
+ max-width: calc(100vw - 16px) !important;
381
+ max-height: calc(100dvh - 32px) !important;
382
+ overflow-y: auto !important;
383
+ margin: 8px !important;
384
+ }
385
+
386
+ /* Context menu — responsive sizing */
387
+ #${CONTEXT_MENU_ROOT_ID} .cw-menu {
388
+ min-width: min(220px, calc(100vw - 24px)) !important;
389
+ max-width: calc(100vw - 24px) !important;
390
+ }
391
+
392
+ #${CONTEXT_MENU_ROOT_ID} .cw-item--submenu > .cw-menu {
393
+ position: fixed !important;
394
+ left: 12px !important;
395
+ right: 12px !important;
396
+ top: auto !important;
397
+ width: auto !important;
398
+ }
399
+
400
+ /* Overflow prevention */
401
+ pre, code {
402
+ overflow-x: auto !important;
403
+ max-width: 100% !important;
404
+ word-break: break-word !important;
405
+ }
406
+ }
407
+
408
+ /* ==== Phone layout (max-width: 600px) ==== */
409
+ @media (max-width: 600px) {
410
+
411
+ /* Safe area support for notched devices */
412
+ body {
413
+ padding-top: env(safe-area-inset-top) !important;
414
+ padding-bottom: env(safe-area-inset-bottom) !important;
415
+ padding-left: env(safe-area-inset-left) !important;
416
+ padding-right: env(safe-area-inset-right) !important;
417
+ }
418
+
419
+ /* CRITICAL: Collapse sidebar token so main content gets full width.
420
+ The Codex app uses --spacing-token-sidebar with a 240px clamp minimum
421
+ which is far too wide on phones. Setting it to 0 makes the sidebar an
422
+ overlay instead of pushing the main content off-screen. */
423
+ :root {
424
+ --spacing-token-sidebar: 0px !important;
425
+ }
426
+
427
+ /* Sidebar becomes full-screen overlay when open */
428
+ .window-fx-sidebar-surface,
429
+ .w-token-sidebar {
430
+ width: 85vw !important;
431
+ max-width: 320px !important;
432
+ z-index: 50 !important;
433
+ box-shadow: 4px 0 24px rgba(0, 0, 0, 0.5) !important;
434
+ background-color: rgb(24, 24, 24) !important;
435
+ }
436
+
437
+ /* Main content takes full width */
438
+ .main-surface,
439
+ .left-token-sidebar {
440
+ left: 0 !important;
441
+ width: 100vw !important;
442
+ }
443
+
444
+ /* Header left section — collapse to fit phone */
445
+ .app-header-left {
446
+ width: auto !important;
447
+ max-width: 50vw !important;
448
+ flex-shrink: 1 !important;
449
+ padding-left: 8px !important;
450
+ }
451
+
452
+ /* Use stable viewport height */
453
+ #root {
454
+ height: calc(var(--cw-vh, 1vh) * 100) !important;
455
+ height: 100dvh !important;
456
+ }
457
+ }
458
+ `;
459
+ document.head.appendChild(style);
460
+ }
461
+
462
+ // ---------------------------------------------------------------------------
463
+ // Viewport height stabilizer (mobile keyboard / chrome bar)
464
+ // ---------------------------------------------------------------------------
465
+
466
+ function installViewportStabilizer() {
467
+ if (!window.visualViewport) {
468
+ return;
469
+ }
470
+
471
+ const update = () => {
472
+ document.documentElement.style.setProperty(
473
+ "--cw-vh",
474
+ window.visualViewport.height * 0.01 + "px"
475
+ );
476
+ };
477
+
478
+ window.visualViewport.addEventListener("resize", update);
479
+ update();
480
+ }
481
+
482
+ // ---------------------------------------------------------------------------
483
+ // Auto-collapse sidebar on mobile first load
484
+ // ---------------------------------------------------------------------------
485
+
486
+ function autoCollapseSidebarOnMobile() {
487
+ if (window.innerWidth > 600) {
488
+ return;
489
+ }
490
+
491
+ // The React app restores sidebar state from storage after mount.
492
+ // We poll briefly after DOM ready to catch the sidebar in its open state.
493
+ function tryCollapse(attempts) {
494
+ if (attempts <= 0) {
495
+ return;
496
+ }
497
+ const btn = document.querySelector('button[aria-label="Hide sidebar"]');
498
+ if (btn) {
499
+ btn.click();
500
+ return;
501
+ }
502
+ setTimeout(() => tryCollapse(attempts - 1), 200);
503
+ }
504
+
505
+ if (document.readyState === "loading") {
506
+ document.addEventListener("DOMContentLoaded", () => {
507
+ // Give React time to mount and restore sidebar state
508
+ setTimeout(() => tryCollapse(15), 500);
509
+ });
510
+ } else {
511
+ setTimeout(() => tryCollapse(15), 500);
512
+ }
513
+ }
514
+
320
515
  function normalizeContextMenuItems(items) {
321
516
  if (!Array.isArray(items)) {
322
517
  return [];
@@ -344,6 +539,7 @@
344
539
 
345
540
  window.removeEventListener("keydown", current.onKeyDown, true);
346
541
  window.removeEventListener("resize", current.onWindowChange, true);
542
+ clearTimeout(current.resizeDebounce);
347
543
 
348
544
  current.root.remove();
349
545
  current.resolve(result ?? null);
@@ -449,8 +645,10 @@
449
645
  }
450
646
  };
451
647
 
648
+ let resizeDebounce = null;
452
649
  const onWindowChange = () => {
453
- closeContextMenu(null);
650
+ clearTimeout(resizeDebounce);
651
+ resizeDebounce = setTimeout(() => closeContextMenu(null), 300);
454
652
  };
455
653
 
456
654
  root.addEventListener("mousedown", onRootMouseDown);
@@ -462,7 +660,8 @@
462
660
  root,
463
661
  resolve,
464
662
  onKeyDown,
465
- onWindowChange
663
+ onWindowChange,
664
+ get resizeDebounce() { return resizeDebounce; }
466
665
  };
467
666
 
468
667
  window.addEventListener("keydown", onKeyDown, true);
@@ -675,6 +874,9 @@
675
874
  window.codexWindowType = "electron";
676
875
  window.electronBridge = electronBridge;
677
876
  installBrowserCompatibilityShims();
877
+ ensureMobileStyles();
878
+ installViewportStabilizer();
879
+ autoCollapseSidebarOnMobile();
678
880
  window.addEventListener("contextmenu", rememberContextMenuPosition, true);
679
881
 
680
882
  connect();
package/src/server.mjs CHANGED
@@ -216,7 +216,7 @@ async function main() {
216
216
  return;
217
217
  }
218
218
 
219
- if (!auth.requireAuth(req, res)) {
219
+ if (!auth.requireAuth(req, res, url)) {
220
220
  return;
221
221
  }
222
222