codex-webstrapper 0.9.0 → 0.9.7

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.7",
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,241 @@
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 — constrain to viewport */
343
+ .h-toolbar {
344
+ max-width: 100vw !important;
345
+ }
346
+
347
+ .h-toolbar button {
348
+ flex-shrink: 1 !important;
349
+ min-width: 0 !important;
350
+ overflow: hidden !important;
351
+ text-overflow: ellipsis !important;
352
+ white-space: nowrap !important;
353
+ }
354
+
355
+ /* Prevent iOS auto-zoom on focus (triggers when font-size < 16px) */
356
+ input, textarea, select {
357
+ font-size: 16px !important;
358
+ max-width: 100% !important;
359
+ box-sizing: border-box !important;
360
+ }
361
+
362
+ [contenteditable="true"] {
363
+ max-width: 100% !important;
364
+ box-sizing: border-box !important;
365
+ }
366
+
367
+ [contenteditable="true"]:focus {
368
+ font-size: 16px !important;
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
+ When collapsed the app sets opacity-0 but the element still covers
429
+ the screen (translate is 0 because we zeroed the token). We must
430
+ disable pointer events so it doesn't block taps on main content. */
431
+ .window-fx-sidebar-surface,
432
+ .w-token-sidebar {
433
+ width: 85vw !important;
434
+ max-width: 320px !important;
435
+ z-index: 50 !important;
436
+ box-shadow: 4px 0 24px rgba(0, 0, 0, 0.5) !important;
437
+ background-color: rgb(24, 24, 24) !important;
438
+ pointer-events: none !important;
439
+ transition: opacity 0.3s ease, pointer-events 0s linear 0.3s !important;
440
+ }
441
+
442
+ /* Re-enable pointer events only when sidebar is visible (open) */
443
+ .window-fx-sidebar-surface.opacity-100,
444
+ .w-token-sidebar.opacity-100 {
445
+ pointer-events: auto !important;
446
+ transition: opacity 0.3s ease, pointer-events 0s linear 0s !important;
447
+ }
448
+
449
+ /* Main content takes full width */
450
+ .main-surface,
451
+ .left-token-sidebar {
452
+ left: 0 !important;
453
+ width: 100vw !important;
454
+ }
455
+
456
+ /* Header toolbar — swipeable so all buttons remain accessible.
457
+ When the sidebar is collapsed the portal area expands and can
458
+ push right-side buttons off-screen; scrolling keeps them
459
+ reachable via horizontal swipe. */
460
+ .h-toolbar {
461
+ overflow-x: auto !important;
462
+ overflow-y: hidden !important;
463
+ -webkit-overflow-scrolling: touch !important;
464
+ scrollbar-width: none !important;
465
+ }
466
+ .h-toolbar::-webkit-scrollbar {
467
+ display: none !important;
468
+ }
469
+
470
+ /* Prevent header-left from consuming more than half the bar.
471
+ The app sets an inline min-width via CSS vars that over-allocates
472
+ space for the portal area — override it so buttons fit. */
473
+ .app-header-left {
474
+ width: auto !important;
475
+ min-width: 0 !important;
476
+ max-width: 50vw !important;
477
+ flex-shrink: 0 !important;
478
+ padding-left: 4px !important;
479
+ padding-right: 0 !important;
480
+ }
481
+
482
+ /* Collapse the empty portal gap when sidebar is hidden */
483
+ .app-header-left-portal {
484
+ gap: 0 !important;
485
+ padding-right: 2px !important;
486
+ }
487
+
488
+ /* Keep right-side buttons from shrinking below usable size */
489
+ .h-toolbar button {
490
+ flex-shrink: 0 !important;
491
+ }
492
+
493
+ /* Use stable viewport height */
494
+ #root {
495
+ height: calc(var(--cw-vh, 1vh) * 100) !important;
496
+ height: 100dvh !important;
497
+ }
498
+ }
499
+ `;
500
+ document.head.appendChild(style);
501
+ }
502
+
503
+ // ---------------------------------------------------------------------------
504
+ // Viewport height stabilizer (mobile keyboard / chrome bar)
505
+ // ---------------------------------------------------------------------------
506
+
507
+ function installViewportStabilizer() {
508
+ if (!window.visualViewport) {
509
+ return;
510
+ }
511
+
512
+ const update = () => {
513
+ document.documentElement.style.setProperty(
514
+ "--cw-vh",
515
+ window.visualViewport.height * 0.01 + "px"
516
+ );
517
+ };
518
+
519
+ window.visualViewport.addEventListener("resize", update);
520
+ update();
521
+ }
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // Auto-collapse sidebar on mobile first load
525
+ // ---------------------------------------------------------------------------
526
+
527
+ function autoCollapseSidebarOnMobile() {
528
+ if (window.innerWidth > 600) {
529
+ return;
530
+ }
531
+
532
+ // The React app restores sidebar state from storage after mount.
533
+ // We poll briefly after DOM ready to catch the sidebar in its open state.
534
+ function tryCollapse(attempts) {
535
+ if (attempts <= 0) {
536
+ return;
537
+ }
538
+ const btn = document.querySelector('button[aria-label="Hide sidebar"]');
539
+ if (btn) {
540
+ btn.click();
541
+ return;
542
+ }
543
+ setTimeout(() => tryCollapse(attempts - 1), 200);
544
+ }
545
+
546
+ if (document.readyState === "loading") {
547
+ document.addEventListener("DOMContentLoaded", () => {
548
+ // Give React time to mount and restore sidebar state
549
+ setTimeout(() => tryCollapse(15), 500);
550
+ });
551
+ } else {
552
+ setTimeout(() => tryCollapse(15), 500);
553
+ }
554
+ }
555
+
320
556
  function normalizeContextMenuItems(items) {
321
557
  if (!Array.isArray(items)) {
322
558
  return [];
@@ -344,6 +580,7 @@
344
580
 
345
581
  window.removeEventListener("keydown", current.onKeyDown, true);
346
582
  window.removeEventListener("resize", current.onWindowChange, true);
583
+ clearTimeout(current.resizeDebounce);
347
584
 
348
585
  current.root.remove();
349
586
  current.resolve(result ?? null);
@@ -449,8 +686,10 @@
449
686
  }
450
687
  };
451
688
 
689
+ let resizeDebounce = null;
452
690
  const onWindowChange = () => {
453
- closeContextMenu(null);
691
+ clearTimeout(resizeDebounce);
692
+ resizeDebounce = setTimeout(() => closeContextMenu(null), 300);
454
693
  };
455
694
 
456
695
  root.addEventListener("mousedown", onRootMouseDown);
@@ -462,7 +701,8 @@
462
701
  root,
463
702
  resolve,
464
703
  onKeyDown,
465
- onWindowChange
704
+ onWindowChange,
705
+ get resizeDebounce() { return resizeDebounce; }
466
706
  };
467
707
 
468
708
  window.addEventListener("keydown", onKeyDown, true);
@@ -675,6 +915,9 @@
675
915
  window.codexWindowType = "electron";
676
916
  window.electronBridge = electronBridge;
677
917
  installBrowserCompatibilityShims();
918
+ ensureMobileStyles();
919
+ installViewportStabilizer();
920
+ autoCollapseSidebarOnMobile();
678
921
  window.addEventListener("contextmenu", rememberContextMenuPosition, true);
679
922
 
680
923
  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