@wipcomputer/wip-ldm-os 0.4.85-alpha.2 → 0.4.85-alpha.4

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.
@@ -13,13 +13,21 @@
13
13
  --accent: #0033FF;
14
14
  --input-bg: #F5F3ED;
15
15
  --input-border: #E0DDD6;
16
+ --card-bg: #FFFFFF;
16
17
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
17
18
  }
18
19
  html, body { height: 100%; font-family: var(--font); background: var(--bg); color: var(--text); }
19
20
  .page { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
20
21
  .card { max-width: 420px; width: 100%; text-align: center; }
21
22
  h1 { font-size: 26px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 8px; }
22
- .sub { font-size: 16px; color: var(--text-muted); margin-bottom: 32px; }
23
+ .sub { font-size: 16px; color: var(--text-muted); margin-bottom: 24px; }
24
+ .code-display {
25
+ font-family: ui-monospace, "SF Mono", monospace;
26
+ font-size: 28px; font-weight: 600; letter-spacing: 0.3em;
27
+ background: var(--card-bg); border: 1px solid var(--input-border);
28
+ border-radius: 12px; padding: 18px;
29
+ margin: 0 auto 20px; display: inline-block; min-width: 240px;
30
+ }
23
31
  input[type="text"] {
24
32
  width: 100%; padding: 18px; font-size: 22px; font-weight: 600;
25
33
  text-align: center; letter-spacing: 0.4em;
@@ -28,9 +36,10 @@
28
36
  text-transform: uppercase; font-family: ui-monospace, "SF Mono", monospace;
29
37
  }
30
38
  input[type="text"]:focus { border-color: var(--accent); outline: none; }
31
- .btn { display: block; width: 100%; padding: 18px; border: none; border-radius: 12px; font-size: 18px; font-weight: 600; font-family: var(--font); cursor: pointer; margin-top: 16px; }
39
+ .btn { display: block; width: 100%; padding: 18px; border: none; border-radius: 12px; font-size: 18px; font-weight: 600; font-family: var(--font); cursor: pointer; margin-top: 12px; }
32
40
  .btn:active { transform: scale(0.98); }
33
41
  .btn-primary { background: var(--accent); color: white; }
42
+ .btn-secondary { background: var(--card-bg); color: var(--text-muted); border: 1px solid var(--input-border); }
34
43
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }
35
44
  .status { margin-top: 18px; font-size: 14px; min-height: 1.4em; color: var(--text-muted); }
36
45
  .status.error { color: #b00020; }
@@ -41,78 +50,159 @@
41
50
  <body>
42
51
  <div class="page">
43
52
  <div class="card">
44
- <h1>Pair your Mac</h1>
45
- <div class="sub">Type the code printed by <code>codex-daemon link</code></div>
46
- <input id="codeInput" type="text" maxlength="6" autocomplete="off" autocapitalize="characters" inputmode="text" placeholder="ABC123">
47
- <button id="pairBtn" class="btn btn-primary" type="button">Pair</button>
48
- <div id="status" class="status"></div>
49
- <div class="footer">Signed in as <span id="handle">...</span> ... <a href="#" id="signout">sign out</a></div>
53
+ <!-- View 1: confirm pair (when URL has a valid code + user is signed in) -->
54
+ <div id="confirm-view" style="display: none;">
55
+ <h1>Pair this laptop with Codex Remote Control?</h1>
56
+ <div class="sub">A device wants to drive its local Codex session from your phone.</div>
57
+ <div class="code-display" id="confirm-code">______</div>
58
+ <button id="confirmBtn" class="btn btn-primary" type="button">Confirm</button>
59
+ <button id="cancelBtn" class="btn btn-secondary" type="button">Cancel</button>
60
+ <div id="status" class="status"></div>
61
+ <div class="footer">Signed in as <span id="handle">...</span></div>
62
+ </div>
63
+
64
+ <!-- View 2: manual code entry (fallback for bare /pair) -->
65
+ <div id="manual-view" style="display: none;">
66
+ <h1>Pair your laptop</h1>
67
+ <div class="sub">Type the 6-character code printed by <code>codex-daemon link</code></div>
68
+ <input id="codeInput" type="text" maxlength="6" autocomplete="off" autocapitalize="characters" inputmode="text" placeholder="ABCDEF">
69
+ <button id="manualBtn" class="btn btn-primary" type="button">Pair</button>
70
+ <div id="manualStatus" class="status"></div>
71
+ <div class="footer">Signed in as <span id="manualHandle">...</span></div>
72
+ </div>
73
+
74
+ <!-- View 3: success -->
75
+ <div id="success-view" style="display: none;">
76
+ <h1>Paired.</h1>
77
+ <div class="sub">Your laptop will pick this up in a few seconds.</div>
78
+ <div class="footer">You can close this tab.</div>
79
+ </div>
50
80
  </div>
51
81
  </div>
52
82
  <script>
53
- function getApiKey() {
54
- return sessionStorage.getItem("wip_api_key");
55
- }
56
- function getHandle() {
57
- return sessionStorage.getItem("wip_handle") || "(unknown)";
58
- }
59
- function setStatus(msg, kind) {
60
- const el = document.getElementById("status");
83
+ // Real daemon alphabet: A-Z minus I and O, digits 2-9. L IS included.
84
+ // Length 6. Per CODEX_PAIR_ALPHABET in server.mjs and plan C1+C3 round 5.
85
+ var PAIR_CODE_REGEX = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
86
+
87
+ function getApiKey() { return sessionStorage.getItem('wip_api_key'); }
88
+ function getHandle() { return sessionStorage.getItem('wip_handle') || '(unknown)'; }
89
+
90
+ function setStatus(elId, msg, kind) {
91
+ var el = document.getElementById(elId);
61
92
  el.textContent = msg;
62
- el.className = "status" + (kind ? " " + kind : "");
63
- }
64
- function ensureSignedIn() {
65
- if (!getApiKey()) {
66
- location.href = "/app/login.html?next=" + encodeURIComponent("/pair");
67
- return false;
68
- }
69
- document.getElementById("handle").textContent = getHandle();
70
- return true;
93
+ el.className = 'status' + (kind ? ' ' + kind : '');
71
94
  }
72
95
 
73
- document.getElementById("signout").addEventListener("click", (ev) => {
74
- ev.preventDefault();
75
- sessionStorage.removeItem("wip_api_key");
76
- sessionStorage.removeItem("wip_handle");
77
- location.href = "/app/login.html?next=" + encodeURIComponent("/pair");
78
- });
96
+ // Read the code from /pair/<CODE>. Returns null if path is bare /pair or
97
+ // the code doesn't match the daemon alphabet. Defense-in-depth: server
98
+ // validates authoritatively via its route regex.
99
+ function readCodeFromPath() {
100
+ var m = location.pathname.match(/^\/pair\/([A-Za-z0-9]+)\/?$/);
101
+ if (!m) return null;
102
+ var raw = m[1].trim().toUpperCase();
103
+ return PAIR_CODE_REGEX.test(raw) ? raw : null;
104
+ }
79
105
 
80
- async function pair() {
81
- const input = document.getElementById("codeInput");
82
- const code = input.value.trim().toUpperCase();
83
- if (code.length !== 6) {
84
- setStatus("Enter the 6-character code shown on your Mac.", "error");
85
- return;
106
+ // POST the code to /api/codex-relay/pair-complete using the api_key
107
+ // from sessionStorage as Bearer token. On success, show the success view.
108
+ async function submitPair(code, statusElId) {
109
+ if (!PAIR_CODE_REGEX.test(code)) {
110
+ setStatus(statusElId, 'That does not look like a valid code.', 'error');
111
+ return false;
86
112
  }
87
- const btn = document.getElementById("pairBtn");
88
- btn.disabled = true;
89
- setStatus("Pairing...");
113
+ setStatus(statusElId, 'Pairing...', '');
90
114
  try {
91
- const res = await fetch("/api/codex-relay/pair-complete", {
92
- method: "POST",
115
+ var res = await fetch('/api/codex-relay/pair-complete', {
116
+ method: 'POST',
93
117
  headers: {
94
- "Content-Type": "application/json",
95
- "Authorization": "Bearer " + getApiKey(),
118
+ 'Content-Type': 'application/json',
119
+ 'Authorization': 'Bearer ' + getApiKey(),
96
120
  },
97
- body: JSON.stringify({ code }),
121
+ body: JSON.stringify({ code: code }),
98
122
  });
99
- const data = await res.json();
100
- if (!res.ok) throw new Error(data.error || "Pairing failed");
101
- setStatus("Paired. Your Mac will pick this up in a few seconds.", "success");
102
- btn.textContent = "Done";
123
+ var data = await res.json();
124
+ if (!res.ok) {
125
+ var msg = (data && data.error) || 'Pairing failed.';
126
+ // If the code expired or was already used, suggest rerunning the daemon.
127
+ if (res.status === 410 || res.status === 404) {
128
+ msg += ' Run codex-daemon link again on your laptop to get a fresh code.';
129
+ }
130
+ setStatus(statusElId, msg, 'error');
131
+ return false;
132
+ }
133
+ document.getElementById('confirm-view').style.display = 'none';
134
+ document.getElementById('manual-view').style.display = 'none';
135
+ document.getElementById('success-view').style.display = 'block';
136
+ return true;
103
137
  } catch (err) {
104
- setStatus(err.message || String(err), "error");
105
- btn.disabled = false;
138
+ setStatus(statusElId, (err && err.message) || String(err), 'error');
139
+ return false;
106
140
  }
107
141
  }
108
142
 
109
- if (ensureSignedIn()) {
110
- document.getElementById("pairBtn").addEventListener("click", pair);
111
- document.getElementById("codeInput").addEventListener("keydown", (ev) => {
112
- if (ev.key === "Enter") pair();
143
+ // ── Routing ──
144
+ //
145
+ // 1. If not signed in: redirect to /login?next=<this-path-with-code>.
146
+ // The existing Kaleidoscope login flow will QR + passkey + redirect
147
+ // back here. The server's `next` whitelist also enforces this shape.
148
+ // 2. If signed in AND URL has a valid code: render the confirm view.
149
+ // User must tap Confirm before we POST pair-complete (per plan C7).
150
+ // 3. If signed in but no code in URL (bare /pair): render the manual
151
+ // entry view. Fallback only.
152
+
153
+ (function init() {
154
+ var code = readCodeFromPath();
155
+ var apiKey = getApiKey();
156
+
157
+ if (!apiKey) {
158
+ // Not signed in. Send through /login keeping the current path as next.
159
+ var nextParam = encodeURIComponent(location.pathname);
160
+ location.replace('/login?next=' + nextParam);
161
+ return;
162
+ }
163
+
164
+ if (code) {
165
+ // Signed in + URL has a valid code. Show confirm step.
166
+ var confirmView = document.getElementById('confirm-view');
167
+ document.getElementById('confirm-code').textContent = code;
168
+ document.getElementById('handle').textContent = getHandle();
169
+ confirmView.style.display = 'block';
170
+ document.getElementById('confirmBtn').addEventListener('click', async function() {
171
+ var confirmBtn = document.getElementById('confirmBtn');
172
+ var cancelBtn = document.getElementById('cancelBtn');
173
+ confirmBtn.disabled = true;
174
+ cancelBtn.disabled = true;
175
+ var ok = await submitPair(code, 'status');
176
+ // On failure, re-enable Confirm/Cancel so the user can retry or cancel.
177
+ // Server may have rejected the code as expired or already-used; the
178
+ // user should see the error and have the buttons back.
179
+ if (!ok) {
180
+ confirmBtn.disabled = false;
181
+ cancelBtn.disabled = false;
182
+ }
183
+ });
184
+ document.getElementById('cancelBtn').addEventListener('click', function() {
185
+ // Best-effort: just close the tab if we can; otherwise redirect home.
186
+ try { window.close(); } catch (e) {}
187
+ setTimeout(function() { location.replace('/'); }, 100);
188
+ });
189
+ return;
190
+ }
191
+
192
+ // Bare /pair, signed in. Manual code entry fallback.
193
+ var manualView = document.getElementById('manual-view');
194
+ document.getElementById('manualHandle').textContent = getHandle();
195
+ manualView.style.display = 'block';
196
+ function manualSubmit() {
197
+ var raw = document.getElementById('codeInput').value.trim().toUpperCase();
198
+ submitPair(raw, 'manualStatus');
199
+ }
200
+ document.getElementById('manualBtn').addEventListener('click', manualSubmit);
201
+ document.getElementById('codeInput').addEventListener('keydown', function(ev) {
202
+ if (ev.key === 'Enter') manualSubmit();
113
203
  });
114
- document.getElementById("codeInput").focus();
115
- }
204
+ document.getElementById('codeInput').focus();
205
+ })();
116
206
  </script>
117
207
  </body>
118
208
  </html>
Binary file
@@ -1233,13 +1233,9 @@ if (sessionStorage.getItem('lesa-token')) {
1233
1233
  showChat();
1234
1234
  }
1235
1235
 
1236
- // If user has an account (created at /login), show sign-in mode
1237
- if (localStorage.getItem('kscope-has-account') && !sessionStorage.getItem('lesa-token')) {
1238
- document.getElementById('createBtn').textContent = 'Enter the Kaleidoscope';
1239
- document.getElementById('createBtn').onclick = function() { doSignIn(); };
1240
- document.getElementById('handleInputWrap').style.display = 'none';
1241
- document.getElementById('signInBtn').parentElement.style.display = 'none';
1242
- }
1236
+ // /demo/ always shows the original create-account UI. Do not key off
1237
+ // localStorage["kscope-has-account"] to swap into sign-in mode here;
1238
+ // that coupled /login state to /demo/ rendering.
1243
1239
 
1244
1240
  // Handle send (not used in demo flow, but wired up)
1245
1241
  function handleSend() {