create-walle 0.9.21 → 0.9.22

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.
Files changed (52) hide show
  1. package/README.md +5 -5
  2. package/package.json +2 -2
  3. package/template/claude-task-manager/api-prompts.js +13 -0
  4. package/template/claude-task-manager/api-reviews.js +5 -2
  5. package/template/claude-task-manager/db.js +348 -15
  6. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  7. package/template/claude-task-manager/docs/image-paste-ux.md +3 -0
  8. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  9. package/template/claude-task-manager/git-utils.js +146 -17
  10. package/template/claude-task-manager/lib/auth-rate-limit.js +23 -3
  11. package/template/claude-task-manager/lib/auth-rules.js +3 -0
  12. package/template/claude-task-manager/lib/document-review.js +33 -2
  13. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +83 -0
  14. package/template/claude-task-manager/lib/mobile-auth-api.js +14 -0
  15. package/template/claude-task-manager/lib/restart-guard.js +68 -0
  16. package/template/claude-task-manager/lib/session-standup.js +36 -13
  17. package/template/claude-task-manager/lib/session-stream.js +11 -4
  18. package/template/claude-task-manager/lib/transport-security.js +50 -0
  19. package/template/claude-task-manager/lib/walle-transcript.js +16 -0
  20. package/template/claude-task-manager/lib/worktree-active-sync.js +6 -3
  21. package/template/claude-task-manager/public/css/reviews.css +10 -0
  22. package/template/claude-task-manager/public/css/setup.css +13 -0
  23. package/template/claude-task-manager/public/css/walle.css +145 -0
  24. package/template/claude-task-manager/public/index.html +539 -44
  25. package/template/claude-task-manager/public/ipad.html +363 -0
  26. package/template/claude-task-manager/public/js/document-review-links.js +196 -0
  27. package/template/claude-task-manager/public/js/message-renderer.js +14 -3
  28. package/template/claude-task-manager/public/js/reviews.js +30 -6
  29. package/template/claude-task-manager/public/js/setup.js +42 -2
  30. package/template/claude-task-manager/public/js/stream-view.js +20 -1
  31. package/template/claude-task-manager/public/js/walle.js +314 -18
  32. package/template/claude-task-manager/public/m/app.css +789 -11
  33. package/template/claude-task-manager/public/m/app.js +1070 -67
  34. package/template/claude-task-manager/public/m/claim.html +9 -2
  35. package/template/claude-task-manager/public/m/index.html +17 -10
  36. package/template/claude-task-manager/public/m/sw.js +1 -1
  37. package/template/claude-task-manager/server.js +365 -95
  38. package/template/claude-task-manager/session-integrity.js +4 -0
  39. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +86 -35
  40. package/template/package.json +1 -1
  41. package/template/wall-e/api-walle.js +19 -1
  42. package/template/wall-e/brain.js +152 -6
  43. package/template/wall-e/chat.js +85 -0
  44. package/template/wall-e/coding-orchestrator.js +106 -12
  45. package/template/wall-e/http/model-admin.js +131 -0
  46. package/template/wall-e/lib/service-health.js +194 -0
  47. package/template/wall-e/llm/anthropic.js +7 -0
  48. package/template/wall-e/llm/client.js +46 -12
  49. package/template/wall-e/llm/openai.js +17 -2
  50. package/template/wall-e/llm/portkey-sync.js +201 -0
  51. package/template/wall-e/server.js +13 -0
  52. package/template/website/index.html +10 -10
@@ -0,0 +1,363 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>CTM iPad Preview</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ --bg: #111827;
11
+ --panel: #182235;
12
+ --panel-strong: #202c43;
13
+ --line: #34435f;
14
+ --text: #e5edf9;
15
+ --muted: #94a3b8;
16
+ --blue: #7aa2ff;
17
+ --green: #81c784;
18
+ --shadow: rgba(0, 0, 0, 0.36);
19
+ }
20
+
21
+ * { box-sizing: border-box; }
22
+
23
+ html,
24
+ body {
25
+ min-height: 100%;
26
+ margin: 0;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
30
+ letter-spacing: 0;
31
+ }
32
+
33
+ body {
34
+ display: grid;
35
+ grid-template-rows: auto 1fr;
36
+ overflow: hidden;
37
+ }
38
+
39
+ button,
40
+ input,
41
+ select {
42
+ font: inherit;
43
+ }
44
+
45
+ button,
46
+ input {
47
+ min-height: 40px;
48
+ border: 1px solid var(--line);
49
+ border-radius: 8px;
50
+ background: #121a2a;
51
+ color: var(--text);
52
+ }
53
+
54
+ button {
55
+ padding: 0 12px;
56
+ cursor: pointer;
57
+ }
58
+
59
+ button:hover,
60
+ button[aria-pressed="true"] {
61
+ border-color: color-mix(in srgb, var(--blue) 76%, var(--line));
62
+ background: color-mix(in srgb, var(--blue) 20%, #121a2a);
63
+ color: #f8fbff;
64
+ }
65
+
66
+ button:focus-visible,
67
+ input:focus-visible {
68
+ outline: 2px solid var(--blue);
69
+ outline-offset: 2px;
70
+ }
71
+
72
+ .topbar {
73
+ display: grid;
74
+ grid-template-columns: minmax(220px, 1fr) auto;
75
+ gap: 16px;
76
+ align-items: center;
77
+ padding: 14px 18px;
78
+ border-bottom: 1px solid var(--line);
79
+ background: color-mix(in srgb, var(--panel) 94%, transparent);
80
+ box-shadow: 0 14px 40px var(--shadow);
81
+ z-index: 2;
82
+ }
83
+
84
+ .title-block h1 {
85
+ margin: 0;
86
+ font-size: 18px;
87
+ line-height: 1.2;
88
+ }
89
+
90
+ .title-block p {
91
+ margin: 4px 0 0;
92
+ color: var(--muted);
93
+ font-size: 13px;
94
+ }
95
+
96
+ .controls {
97
+ display: flex;
98
+ flex-wrap: wrap;
99
+ justify-content: flex-end;
100
+ align-items: center;
101
+ gap: 8px;
102
+ }
103
+
104
+ .device-group {
105
+ display: flex;
106
+ gap: 4px;
107
+ padding: 3px;
108
+ border: 1px solid var(--line);
109
+ border-radius: 10px;
110
+ background: #0f1726;
111
+ }
112
+
113
+ .device-group button {
114
+ min-height: 34px;
115
+ border-color: transparent;
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .source-form {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 8px;
123
+ min-width: min(520px, 100%);
124
+ }
125
+
126
+ .source-form input {
127
+ width: min(360px, 38vw);
128
+ padding: 0 11px;
129
+ }
130
+
131
+ .stage {
132
+ position: relative;
133
+ min-height: 0;
134
+ display: grid;
135
+ place-items: center;
136
+ padding: 18px;
137
+ overflow: auto;
138
+ }
139
+
140
+ .preview-meta {
141
+ position: absolute;
142
+ left: 18px;
143
+ bottom: 14px;
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 10px;
147
+ color: var(--muted);
148
+ font-size: 12px;
149
+ pointer-events: none;
150
+ }
151
+
152
+ .status-dot {
153
+ width: 8px;
154
+ height: 8px;
155
+ border-radius: 999px;
156
+ background: var(--green);
157
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--green) 16%, transparent);
158
+ }
159
+
160
+ .device-slot {
161
+ position: relative;
162
+ }
163
+
164
+ .device {
165
+ transform-origin: top left;
166
+ border-radius: 34px;
167
+ background:
168
+ linear-gradient(145deg, #3b465f, #111827 52%, #050812);
169
+ padding: 16px;
170
+ box-shadow:
171
+ 0 28px 80px rgba(0, 0, 0, 0.42),
172
+ inset 0 0 0 1px rgba(255, 255, 255, 0.08),
173
+ inset 0 0 0 7px rgba(255, 255, 255, 0.04);
174
+ }
175
+
176
+ .screen-shell {
177
+ position: relative;
178
+ overflow: hidden;
179
+ border-radius: 22px;
180
+ background: #0b1020;
181
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.10);
182
+ }
183
+
184
+ iframe {
185
+ display: block;
186
+ border: 0;
187
+ background: #101419;
188
+ }
189
+
190
+ .home-indicator {
191
+ position: absolute;
192
+ left: 50%;
193
+ bottom: 7px;
194
+ width: 110px;
195
+ height: 4px;
196
+ transform: translateX(-50%);
197
+ border-radius: 999px;
198
+ background: rgba(255, 255, 255, 0.34);
199
+ pointer-events: none;
200
+ }
201
+
202
+ .camera {
203
+ position: absolute;
204
+ top: 8px;
205
+ left: 50%;
206
+ width: 42px;
207
+ height: 5px;
208
+ transform: translateX(-50%);
209
+ border-radius: 999px;
210
+ background: rgba(255, 255, 255, 0.22);
211
+ pointer-events: none;
212
+ }
213
+
214
+ @media (max-width: 980px) {
215
+ .topbar {
216
+ grid-template-columns: 1fr;
217
+ }
218
+
219
+ .controls {
220
+ justify-content: flex-start;
221
+ }
222
+
223
+ .source-form,
224
+ .source-form input {
225
+ width: 100%;
226
+ }
227
+ }
228
+ </style>
229
+ </head>
230
+ <body>
231
+ <header class="topbar">
232
+ <div class="title-block">
233
+ <h1 id="preview-title">CTM iPad Preview</h1>
234
+ <p>Same-origin preview of the mobile CTM app. Use this for quick iPad layout checks before testing on a real iPad.</p>
235
+ </div>
236
+ <div class="controls" aria-label="Preview controls">
237
+ <div class="device-group" role="group" aria-label="Device size">
238
+ <button type="button" data-device="ipad-test-portrait">Test Portrait</button>
239
+ <button type="button" data-device="ipad-test-landscape">Test Landscape</button>
240
+ <button type="button" data-device="ipad-11-portrait">11 Portrait</button>
241
+ <button type="button" data-device="ipad-11-landscape">11 Landscape</button>
242
+ </div>
243
+ <form id="source-form" class="source-form">
244
+ <label for="preview-src">Source</label>
245
+ <input id="preview-src" type="text" inputmode="url" autocomplete="off" spellcheck="false" value="/m/">
246
+ <button id="preview-load" type="submit">Load</button>
247
+ <button id="preview-reload" type="button">Reload</button>
248
+ <button id="preview-open" type="button">Open</button>
249
+ </form>
250
+ </div>
251
+ </header>
252
+
253
+ <main id="stage" class="stage">
254
+ <div id="device-slot" class="device-slot">
255
+ <section id="device" class="device" aria-label="iPad preview device">
256
+ <div class="camera" aria-hidden="true"></div>
257
+ <div id="screen-shell" class="screen-shell">
258
+ <iframe id="preview-frame" title="CTM mobile app iPad preview" src="/m/"></iframe>
259
+ </div>
260
+ <div class="home-indicator" aria-hidden="true"></div>
261
+ </section>
262
+ </div>
263
+ <div class="preview-meta" aria-live="polite">
264
+ <span class="status-dot" aria-hidden="true"></span>
265
+ <span id="preview-size">768 x 1024</span>
266
+ <span id="preview-scale">100%</span>
267
+ </div>
268
+ </main>
269
+
270
+ <script>
271
+ (function initIpadPreview() {
272
+ var DEVICES = {
273
+ 'ipad-test-portrait': { label: 'Test Portrait', width: 768, height: 1024 },
274
+ 'ipad-test-landscape': { label: 'Test Landscape', width: 1024, height: 768 },
275
+ 'ipad-11-portrait': { label: '11 Portrait', width: 834, height: 1194 },
276
+ 'ipad-11-landscape': { label: '11 Landscape', width: 1194, height: 834 },
277
+ };
278
+ var params = new URLSearchParams(window.location.search || '');
279
+ var stage = document.getElementById('stage');
280
+ var slot = document.getElementById('device-slot');
281
+ var device = document.getElementById('device');
282
+ var screen = document.getElementById('screen-shell');
283
+ var frame = document.getElementById('preview-frame');
284
+ var srcInput = document.getElementById('preview-src');
285
+ var sizeLabel = document.getElementById('preview-size');
286
+ var scaleLabel = document.getElementById('preview-scale');
287
+ var currentDevice = DEVICES[params.get('device')] ? params.get('device') : 'ipad-test-portrait';
288
+
289
+ function normalizeSrc(value) {
290
+ var raw = String(value || '').trim() || '/m/';
291
+ if (/^https?:\/\//i.test(raw)) return raw;
292
+ if (raw.charAt(0) !== '/') raw = '/' + raw;
293
+ return raw;
294
+ }
295
+
296
+ function setPressed(key) {
297
+ document.querySelectorAll('[data-device]').forEach(function(btn) {
298
+ btn.setAttribute('aria-pressed', btn.getAttribute('data-device') === key ? 'true' : 'false');
299
+ });
300
+ }
301
+
302
+ function updateUrl() {
303
+ var next = new URL(window.location.href);
304
+ next.searchParams.set('device', currentDevice);
305
+ next.searchParams.set('src', srcInput.value);
306
+ window.history.replaceState(null, '', next);
307
+ }
308
+
309
+ function fitDevice() {
310
+ var spec = DEVICES[currentDevice] || DEVICES['ipad-test-portrait'];
311
+ var outerW = spec.width + 32;
312
+ var outerH = spec.height + 32;
313
+ screen.style.width = spec.width + 'px';
314
+ screen.style.height = spec.height + 'px';
315
+ frame.style.width = spec.width + 'px';
316
+ frame.style.height = spec.height + 'px';
317
+ device.style.width = outerW + 'px';
318
+ device.style.height = outerH + 'px';
319
+ var availableW = Math.max(320, stage.clientWidth - 42);
320
+ var availableH = Math.max(320, stage.clientHeight - 42);
321
+ var scale = Math.min(1, availableW / outerW, availableH / outerH);
322
+ device.style.transform = 'scale(' + scale.toFixed(4) + ')';
323
+ slot.style.width = Math.ceil(outerW * scale) + 'px';
324
+ slot.style.height = Math.ceil(outerH * scale) + 'px';
325
+ sizeLabel.textContent = spec.width + ' x ' + spec.height;
326
+ scaleLabel.textContent = Math.round(scale * 100) + '%';
327
+ setPressed(currentDevice);
328
+ }
329
+
330
+ function loadFrame(value) {
331
+ var src = normalizeSrc(value);
332
+ srcInput.value = src;
333
+ frame.setAttribute('src', src);
334
+ updateUrl();
335
+ }
336
+
337
+ document.querySelectorAll('[data-device]').forEach(function(btn) {
338
+ btn.addEventListener('click', function() {
339
+ currentDevice = btn.getAttribute('data-device') || currentDevice;
340
+ fitDevice();
341
+ updateUrl();
342
+ });
343
+ });
344
+
345
+ document.getElementById('source-form').addEventListener('submit', function(event) {
346
+ event.preventDefault();
347
+ loadFrame(srcInput.value);
348
+ });
349
+ document.getElementById('preview-reload').addEventListener('click', function() {
350
+ try { frame.contentWindow.location.reload(); } catch (_) { frame.setAttribute('src', frame.getAttribute('src') || '/m/'); }
351
+ });
352
+ document.getElementById('preview-open').addEventListener('click', function() {
353
+ window.open(frame.getAttribute('src') || '/m/', '_blank', 'noopener,noreferrer');
354
+ });
355
+ window.addEventListener('resize', fitDevice);
356
+
357
+ srcInput.value = normalizeSrc(params.get('src') || '/m/');
358
+ frame.setAttribute('src', srcInput.value);
359
+ fitDevice();
360
+ })();
361
+ </script>
362
+ </body>
363
+ </html>
@@ -0,0 +1,196 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ const DOC_EXT_RE = /\.(?:md|markdown|mdown|txt)$/i;
5
+ const DOC_TOKEN_RE = /(^|[\s([{<"'`])([^\s<>"'`]*\.(?:md|markdown|mdown|txt)(?::\d+)?)(?=$|[\s)\]}>.,;!?])/gi;
6
+ const SKIP_SELECTOR = 'a,code,pre,kbd,samp,textarea,input,select,button,script,style';
7
+
8
+ function _state() {
9
+ return (typeof window !== 'undefined' && (window._ctmState || window.state)) || {};
10
+ }
11
+
12
+ function _recentSessions() {
13
+ try {
14
+ return Array.isArray(window.allRecentSessions) ? window.allRecentSessions : [];
15
+ } catch {
16
+ return [];
17
+ }
18
+ }
19
+
20
+ function splitReference(raw, fallbackLine) {
21
+ let value = String(raw || '').trim();
22
+ while (/[.,;!?]$/.test(value)) value = value.slice(0, -1);
23
+ const match = value.match(/^(.*):(\d+)$/);
24
+ const line = match ? Math.max(1, Number(match[2]) || 1) : Math.max(1, Number(fallbackLine) || 1);
25
+ const path = match ? match[1] : value;
26
+ return { raw: value, path, line, label: value };
27
+ }
28
+
29
+ function _looksLikeDocumentReference(raw) {
30
+ const ref = splitReference(raw);
31
+ if (!ref.path || !DOC_EXT_RE.test(ref.path)) return false;
32
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(ref.path)) return false;
33
+ if (/^data:/i.test(ref.path)) return false;
34
+ return true;
35
+ }
36
+
37
+ function findReferences(text) {
38
+ const source = String(text || '');
39
+ const refs = [];
40
+ DOC_TOKEN_RE.lastIndex = 0;
41
+ let match;
42
+ while ((match = DOC_TOKEN_RE.exec(source))) {
43
+ const prefix = match[1] || '';
44
+ const token = match[2] || '';
45
+ if (!_looksLikeDocumentReference(token)) continue;
46
+ const start = match.index + prefix.length;
47
+ const end = start + token.length;
48
+ refs.push({ ...splitReference(token), start, end });
49
+ }
50
+ return refs;
51
+ }
52
+
53
+ function _rowMatchesSession(row, sessionId) {
54
+ if (!row || !sessionId) return false;
55
+ const ids = [
56
+ row.sessionId, row.session_id, row.id,
57
+ row.provisionalId, row.provisional_id,
58
+ row.agentSessionId, row.agent_session_id,
59
+ row.claudeSessionId, row.claude_session_id,
60
+ ];
61
+ if (Array.isArray(row.agentSessionIds)) ids.push(...row.agentSessionIds);
62
+ if (Array.isArray(row.agent_session_ids)) ids.push(...row.agent_session_ids);
63
+ return ids.map(String).includes(String(sessionId));
64
+ }
65
+
66
+ function contextForSession(sessionId) {
67
+ const state = _state();
68
+ const id = sessionId || state.reviewingSessionId || state.activeTab || '';
69
+ const live = id && state.sessions && typeof state.sessions.get === 'function'
70
+ ? state.sessions.get(id)
71
+ : null;
72
+ const meta = live && live.meta || {};
73
+ const recent = _recentSessions().find(row => _rowMatchesSession(row, id)) || null;
74
+ const cwd = meta.cwd || live?.cwd || meta.project || live?.project || recent?.cwd || recent?.project || '';
75
+ return { sessionId: id || '', cwd: cwd || '' };
76
+ }
77
+
78
+ function defaultContext() {
79
+ return contextForSession();
80
+ }
81
+
82
+ function hashForReference(raw, opts) {
83
+ const ref = splitReference(raw, opts && opts.line);
84
+ const params = new URLSearchParams();
85
+ params.set('type', 'doc');
86
+ params.set('path', ref.path);
87
+ params.set('line', String(ref.line));
88
+ if (opts && opts.cwd) params.set('cwd', opts.cwd);
89
+ if (opts && opts.sessionId) params.set('session', opts.sessionId);
90
+ return '#review&' + params.toString();
91
+ }
92
+
93
+ function openReference(raw, opts) {
94
+ const context = Object.assign({}, opts || {});
95
+ const ref = splitReference(raw, context.line);
96
+ if (typeof window.CR !== 'undefined' && typeof window.CR.openDocumentReference === 'function') {
97
+ window.CR.openDocumentReference(ref.label, context);
98
+ return;
99
+ }
100
+ window.location.hash = hashForReference(ref.label, context);
101
+ }
102
+
103
+ function _makeAnchor(ref, context) {
104
+ const a = document.createElement('a');
105
+ a.className = 'ctm-doc-review-link';
106
+ a.href = hashForReference(ref.label, context);
107
+ a.textContent = ref.label;
108
+ a.title = 'Open document in Review';
109
+ a.dataset.ctmDocPath = ref.path;
110
+ a.dataset.ctmDocLine = String(ref.line);
111
+ if (context && context.cwd) a.dataset.ctmDocCwd = context.cwd;
112
+ if (context && context.sessionId) a.dataset.ctmDocSessionId = context.sessionId;
113
+ a.addEventListener('click', (event) => {
114
+ event.preventDefault();
115
+ openReference(ref.label, context);
116
+ });
117
+ return a;
118
+ }
119
+
120
+ function _replaceTextNode(node, context) {
121
+ const text = node.nodeValue || '';
122
+ const refs = findReferences(text);
123
+ if (!refs.length) return;
124
+ const frag = document.createDocumentFragment();
125
+ let offset = 0;
126
+ for (const ref of refs) {
127
+ if (ref.start > offset) frag.appendChild(document.createTextNode(text.slice(offset, ref.start)));
128
+ frag.appendChild(_makeAnchor(ref, context));
129
+ offset = ref.end;
130
+ }
131
+ if (offset < text.length) frag.appendChild(document.createTextNode(text.slice(offset)));
132
+ node.parentNode.replaceChild(frag, node);
133
+ }
134
+
135
+ function linkifyElement(root, opts) {
136
+ if (!root || typeof document === 'undefined') return;
137
+ const context = Object.assign({}, opts || defaultContext());
138
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
139
+ acceptNode(node) {
140
+ const parent = node && node.parentElement;
141
+ if (!parent || !String(node.nodeValue || '').trim()) return NodeFilter.FILTER_REJECT;
142
+ if (parent.closest && parent.closest(SKIP_SELECTOR)) return NodeFilter.FILTER_REJECT;
143
+ return findReferences(node.nodeValue || '').length
144
+ ? NodeFilter.FILTER_ACCEPT
145
+ : NodeFilter.FILTER_REJECT;
146
+ },
147
+ });
148
+ const nodes = [];
149
+ while (walker.nextNode()) nodes.push(walker.currentNode);
150
+ for (const node of nodes) _replaceTextNode(node, context);
151
+ }
152
+
153
+ function _bufferLineText(term, y) {
154
+ try {
155
+ const line = term && term.buffer && term.buffer.active && term.buffer.active.getLine(y - 1);
156
+ return line ? line.translateToString(true) : '';
157
+ } catch {
158
+ return '';
159
+ }
160
+ }
161
+
162
+ function registerTerminalLinkProvider(term, sessionId) {
163
+ if (!term || typeof term.registerLinkProvider !== 'function') return null;
164
+ return term.registerLinkProvider({
165
+ provideLinks(y, callback) {
166
+ const text = _bufferLineText(term, y);
167
+ const context = contextForSession(sessionId);
168
+ const links = findReferences(text).map(ref => ({
169
+ range: {
170
+ start: { x: ref.start + 1, y },
171
+ end: { x: ref.end, y },
172
+ },
173
+ text: ref.label,
174
+ activate() { openReference(ref.label, context); },
175
+ hover() {},
176
+ leave() {},
177
+ }));
178
+ callback(links.length ? links : undefined);
179
+ },
180
+ });
181
+ }
182
+
183
+ const api = {
184
+ findReferences,
185
+ splitReference,
186
+ contextForSession,
187
+ defaultContext,
188
+ hashForReference,
189
+ openReference,
190
+ linkifyElement,
191
+ registerTerminalLinkProvider,
192
+ };
193
+
194
+ if (typeof window !== 'undefined') window.CTMDocLinks = api;
195
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
196
+ })();
@@ -1741,9 +1741,21 @@
1741
1741
  return _nodeWithin(root, sel.anchorNode) || _nodeWithin(root, sel.focusNode);
1742
1742
  };
1743
1743
 
1744
+ MR.isPromptTurnPromptTextTarget = function (ev, headerEl) {
1745
+ if (!ev || !headerEl || !ev.target || !ev.target.closest) return false;
1746
+ const textEl = ev.target.closest('.prompt-turn-prompt .msg-text');
1747
+ return !!(textEl && headerEl.contains(textEl));
1748
+ };
1749
+
1744
1750
  MR.shouldKeepPromptTextSelection = function (ev, headerEl) {
1751
+ if (!MR.isPromptTurnPromptTextTarget(ev, headerEl)) return false;
1752
+ return MR.hasTextSelectionInside(headerEl);
1753
+ };
1754
+
1755
+ MR.shouldIgnorePromptTurnHeaderToggle = function (ev, headerEl) {
1745
1756
  if (!ev || !headerEl || !ev.target || !ev.target.closest) return false;
1746
- if (!ev.target.closest('.prompt-turn-prompt .msg-text')) return false;
1757
+ if (ev.target.closest('a,button,input,textarea,select')) return true;
1758
+ if (MR.isPromptTurnPromptTextTarget(ev, headerEl)) return true;
1747
1759
  return MR.hasTextSelectionInside(headerEl);
1748
1760
  };
1749
1761
 
@@ -1815,8 +1827,7 @@
1815
1827
  function _wirePromptTurnHeader(turnEl, header) {
1816
1828
  if (!turnEl || !header) return;
1817
1829
  const toggle = (ev) => {
1818
- if (ev && ev.target && ev.target.closest && ev.target.closest('a,button,input,textarea,select')) return;
1819
- if (MR.shouldKeepPromptTextSelection(ev, header)) return;
1830
+ if (MR.shouldIgnorePromptTurnHeaderToggle(ev, header)) return;
1820
1831
  MR.setPromptTurnExpanded(turnEl, !turnEl.classList.contains('expanded'));
1821
1832
  };
1822
1833
  header.addEventListener('click', toggle);
@@ -336,11 +336,12 @@ CR.openReview = async function(sessionId, projectPath) {
336
336
  };
337
337
 
338
338
  CR.openDocumentReview = async function(filePath, line, opts) {
339
+ opts = opts || {};
339
340
  const seq = ++crState._openSeq;
340
341
  const targetLine = Math.max(1, Number(line) || 1);
341
342
  crState._view = 'review';
342
343
  crState.reviewType = 'doc';
343
- crState.sessionId = opts?.sessionId || null;
344
+ crState.sessionId = opts.sessionId || null;
344
345
  crState.projectPath = null;
345
346
  crState.baseRef = '';
346
347
  crState.branch = '';
@@ -360,9 +361,13 @@ CR.openDocumentReview = async function(filePath, line, opts) {
360
361
  if (typeof window.renderTabs === 'function') window.renderTabs();
361
362
  }
362
363
 
363
- history.replaceState(null, '', location.pathname + location.search
364
- + '#review&type=doc&path=' + encodeURIComponent(filePath || '')
365
- + '&line=' + encodeURIComponent(String(targetLine)));
364
+ const routeParams = new URLSearchParams();
365
+ routeParams.set('type', 'doc');
366
+ routeParams.set('path', filePath || '');
367
+ routeParams.set('line', String(targetLine));
368
+ if (opts.cwd) routeParams.set('cwd', opts.cwd);
369
+ if (crState.sessionId) routeParams.set('session', crState.sessionId);
370
+ history.replaceState(null, '', location.pathname + location.search + '#review&' + routeParams.toString());
366
371
 
367
372
  renderDocHeader();
368
373
  renderLoading();
@@ -371,6 +376,7 @@ CR.openDocumentReview = async function(filePath, line, opts) {
371
376
  const data = await api('/reviews/document-review', 'POST', {
372
377
  path: filePath,
373
378
  line: targetLine,
379
+ cwd: opts.cwd || undefined,
374
380
  session_id: crState.sessionId || undefined,
375
381
  });
376
382
  if (seq !== crState._openSeq || crState.reviewType !== 'doc') return;
@@ -381,6 +387,14 @@ CR.openDocumentReview = async function(filePath, line, opts) {
381
387
  crState.comments = data.review?.comments || [];
382
388
  const targetBlock = findDocBlockForLine(crState.document?.line || targetLine);
383
389
  crState.activeBlock = targetBlock?.id || null;
390
+ if (crState.document?.path) {
391
+ const canonicalParams = new URLSearchParams();
392
+ canonicalParams.set('type', 'doc');
393
+ canonicalParams.set('path', crState.document.path);
394
+ canonicalParams.set('line', String(crState.document.line || targetLine));
395
+ if (crState.sessionId) canonicalParams.set('session', crState.sessionId);
396
+ history.replaceState(null, '', location.pathname + location.search + '#review&' + canonicalParams.toString());
397
+ }
384
398
  renderDocHeader();
385
399
  renderDocTree();
386
400
  renderDocReview();
@@ -392,6 +406,13 @@ CR.openDocumentReview = async function(filePath, line, opts) {
392
406
  }
393
407
  };
394
408
 
409
+ CR.openDocumentReference = async function(rawReference, opts) {
410
+ const parsed = window.CTMDocLinks && typeof window.CTMDocLinks.splitReference === 'function'
411
+ ? window.CTMDocLinks.splitReference(rawReference, opts && opts.line)
412
+ : { path: String(rawReference || ''), line: opts && opts.line || 1 };
413
+ return CR.openDocumentReview(parsed.path, parsed.line, opts || {});
414
+ };
415
+
395
416
  async function loadDiff() {
396
417
  const seq = ++crState._diffSeq;
397
418
  renderLoading();
@@ -1670,7 +1691,7 @@ CR.handleFilesChanged = function(msg) {
1670
1691
  updateBadge(total);
1671
1692
 
1672
1693
  // Refresh project list if it's currently visible (not in a diff review)
1673
- if (!crState.reviewId) {
1694
+ if (!crState.reviewId && crState._view === 'projects') {
1674
1695
  const panel = document.getElementById('codereview-panel');
1675
1696
  if (panel && panel.classList.contains('active')) {
1676
1697
  CR.showProjectList();
@@ -1934,7 +1955,10 @@ window.crState = crState;
1934
1955
  if (window._ctmState?.pendingDocumentReview) {
1935
1956
  const pending = window._ctmState.pendingDocumentReview;
1936
1957
  delete window._ctmState.pendingDocumentReview;
1937
- setTimeout(() => CR.openDocumentReview(pending.path, pending.line || 1), 0);
1958
+ setTimeout(() => CR.openDocumentReview(pending.path, pending.line || 1, {
1959
+ cwd: pending.cwd || '',
1960
+ sessionId: pending.sessionId || '',
1961
+ }), 0);
1938
1962
  }
1939
1963
 
1940
1964
  })();