claude-dev-server 1.1.3 → 1.2.0

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.
@@ -196,87 +196,77 @@ async function handleInspect(msg, ws, projectRoot) {
196
196
  // src/client/injection.js
197
197
  var CLIENT_STYLES = `
198
198
  <style id="claude-dev-server-styles">
199
- .claude-dev-server-toggle {
200
- position: fixed;
201
- bottom: 20px;
202
- right: 20px;
203
- width: 44px;
204
- height: 44px;
205
- background: #1e1e1e;
206
- border: none;
207
- border-radius: 50%;
208
- box-shadow: 0 2px 12px rgba(0,0,0,0.3);
209
- cursor: pointer;
210
- z-index: 999999;
211
- display: flex;
212
- align-items: center;
213
- justify-content: center;
214
- font-size: 20px;
215
- transition: transform 0.2s, background 0.2s;
199
+ * {
200
+ margin: 0;
216
201
  padding: 0;
202
+ box-sizing: border-box;
217
203
  }
218
- .claude-dev-server-toggle:hover {
219
- transform: scale(1.1);
220
- background: #2d2d2d;
221
- }
222
- .claude-dev-server-toggle.hidden {
223
- display: none;
204
+ html, body {
205
+ width: 100%;
206
+ height: 100%;
207
+ overflow: hidden;
208
+ background: transparent;
224
209
  }
225
- .claude-dev-server-inspect-btn {
226
- position: fixed;
227
- bottom: 74px;
228
- right: 20px;
229
- width: 44px;
230
- height: 44px;
231
- background: #1e1e1e;
232
- border: none;
233
- border-radius: 50%;
234
- box-shadow: 0 2px 12px rgba(0,0,0,0.3);
235
- cursor: pointer;
236
- z-index: 999999;
210
+ .claude-dev-server-container {
237
211
  display: flex;
238
- align-items: center;
239
- justify-content: center;
240
- font-size: 16px;
241
- transition: transform 0.2s, background 0.2s;
242
- }
243
- .claude-dev-server-inspect-btn:hover {
244
- transform: scale(1.1);
245
- background: #2d2d2d;
212
+ width: 100vw;
213
+ height: 100vh;
214
+ overflow: hidden;
215
+ background: transparent;
246
216
  }
247
- .claude-dev-server-inspect-btn.active {
248
- background: #007acc;
217
+ .claude-dev-server-left {
218
+ width: 40%;
219
+ min-width: 300px;
220
+ max-width: 60%;
221
+ overflow: hidden;
222
+ position: relative;
223
+ background: #fff;
224
+ display: flex;
225
+ flex-direction: column;
249
226
  }
250
- .claude-dev-server-inspect-btn.hidden {
251
- display: none;
227
+ .claude-dev-server-left iframe {
228
+ width: 100%;
229
+ height: 100%;
230
+ border: none;
252
231
  }
253
- .claude-dev-server-toggle svg,
254
- .claude-dev-server-inspect-btn svg {
255
- width: 20px;
256
- height: 20px;
232
+ .claude-dev-server-divider {
233
+ width: 6px;
234
+ background: #3e3e3e;
235
+ position: relative;
236
+ cursor: col-resize;
237
+ transition: background 0.2s;
257
238
  flex-shrink: 0;
258
239
  }
259
- .claude-dev-server-panel {
260
- position: fixed;
261
- top: 0;
262
- right: 0;
263
- width: 650px;
264
- height: 100vh;
265
- background: #1e1e1e;
266
- color: #d4d4d4;
267
- font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Consolas', monospace;
268
- font-size: 13px;
269
- box-shadow: -4px 0 20px rgba(0,0,0,0.3);
270
- transform: translateX(100%);
271
- transition: transform 0.2s ease;
272
- z-index: 999999;
240
+ .claude-dev-server-divider:hover,
241
+ .claude-dev-server-divider.dragging {
242
+ background: #d97757;
243
+ }
244
+ .claude-dev-server-right {
245
+ flex: 1;
246
+ min-width: 300px;
247
+ overflow: hidden;
248
+ position: relative;
273
249
  display: flex;
274
250
  flex-direction: column;
275
251
  }
276
- .claude-dev-server-panel.open {
277
- transform: translateX(0);
252
+ .claude-dev-server-terminal {
253
+ flex: 1;
254
+ overflow: hidden;
255
+ position: relative;
256
+ background: #000;
278
257
  }
279
- .claude-dev-server-header {
258
+ .claude-dev-server-terminal iframe,
259
+ .claude-dev-server-terminal-iframe {
260
+ width: 100%;
261
+ height: 100%;
262
+ border: none;
263
+ }
264
+ .claude-dev-server-dev-iframe {
265
+ width: 100%;
266
+ height: 100%;
267
+ border: none;
268
+ }
269
+ .claude-dev-server-terminal-header {
280
270
  padding: 8px 12px;
281
271
  background: #2d2d2d;
282
272
  border-bottom: 1px solid #3e3e3e;
@@ -284,6 +274,7 @@ var CLIENT_STYLES = `
284
274
  justify-content: space-between;
285
275
  align-items: center;
286
276
  user-select: none;
277
+ min-height: 40px;
287
278
  }
288
279
  .claude-dev-server-title {
289
280
  font-weight: 600;
@@ -292,19 +283,21 @@ var CLIENT_STYLES = `
292
283
  align-items: center;
293
284
  gap: 8px;
294
285
  font-size: 13px;
286
+ font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Consolas', monospace;
295
287
  }
296
- .claude-dev-server-title::before {
297
- content: '\u{1F916}';
298
- font-size: 14px;
288
+ .claude-dev-server-title svg {
289
+ width: 14px;
290
+ height: 14px;
291
+ flex-shrink: 0;
299
292
  }
300
293
  .claude-dev-server-actions {
301
294
  display: flex;
302
295
  gap: 6px;
303
296
  }
304
297
  .claude-dev-server-btn {
305
- background: #3e3e3e;
298
+ background: #d97757;
306
299
  border: none;
307
- color: #d4d4d4;
300
+ color: #fff;
308
301
  cursor: pointer;
309
302
  font-family: inherit;
310
303
  font-size: 11px;
@@ -316,42 +309,12 @@ var CLIENT_STYLES = `
316
309
  gap: 4px;
317
310
  }
318
311
  .claude-dev-server-btn:hover {
319
- background: #4e4e4e;
312
+ background: #c96a4a;
320
313
  }
321
314
  .claude-dev-server-btn.active {
322
- background: #007acc;
315
+ background: #b85d3f;
323
316
  color: #fff;
324
317
  }
325
- .claude-dev-server-close {
326
- background: none;
327
- border: none;
328
- color: #858585;
329
- cursor: pointer;
330
- font-size: 18px;
331
- padding: 0;
332
- width: 20px;
333
- height: 20px;
334
- display: flex;
335
- align-items: center;
336
- justify-content: center;
337
- border-radius: 3px;
338
- transition: background 0.15s, color 0.15s;
339
- }
340
- .claude-dev-server-close:hover {
341
- background: #3e3e3e;
342
- color: #fff;
343
- }
344
- .claude-dev-server-terminal {
345
- flex: 1;
346
- overflow: hidden;
347
- position: relative;
348
- background: #000;
349
- }
350
- .claude-dev-server-terminal iframe {
351
- width: 100%;
352
- height: 100%;
353
- border: none;
354
- }
355
318
  .claude-dev-server-inspect-overlay {
356
319
  position: fixed;
357
320
  top: 0;
@@ -359,7 +322,7 @@ var CLIENT_STYLES = `
359
322
  right: 0;
360
323
  bottom: 0;
361
324
  pointer-events: none;
362
- z-index: 999998;
325
+ z-index: 2147483646;
363
326
  display: none;
364
327
  }
365
328
  .claude-dev-server-inspect-overlay.active {
@@ -385,39 +348,22 @@ var CLIENT_STYLES = `
385
348
  font-family: monospace;
386
349
  white-space: nowrap;
387
350
  }
388
- /* Responsive layout for portrait mode */
351
+ /* Portrait mode */
389
352
  @media (orientation: portrait) {
390
- .claude-dev-server-toggle {
391
- top: auto;
392
- bottom: 20px;
393
- right: 10px;
394
- width: 36px;
395
- height: 36px;
353
+ .claude-dev-server-container {
354
+ flex-direction: column;
396
355
  }
397
- .claude-dev-server-inspect-btn {
398
- top: auto;
399
- bottom: 66px;
400
- right: 10px;
401
- width: 36px;
402
- height: 36px;
403
- }
404
- .claude-dev-server-panel {
405
- top: auto;
406
- bottom: 0;
407
- right: 0;
408
- left: 0;
356
+ .claude-dev-server-divider {
409
357
  width: 100%;
410
- height: 80vh;
411
- transform: translateY(100%);
412
- box-shadow: 0 -4px 20px rgba(0,0,0,0.3);
413
- }
414
- .claude-dev-server-panel.open {
415
- transform: translateY(0);
358
+ height: 6px;
359
+ cursor: row-resize;
416
360
  }
417
- .claude-dev-server-toggle svg,
418
- .claude-dev-server-inspect-btn svg {
419
- width: 16px;
420
- height: 16px;
361
+ .claude-dev-server-right {
362
+ width: 100%;
363
+ min-width: unset;
364
+ max-width: unset;
365
+ height: 50%;
366
+ min-height: 200px;
421
367
  }
422
368
  }
423
369
  </style>
@@ -425,15 +371,16 @@ var CLIENT_STYLES = `
425
371
  var CLIENT_SCRIPT = `
426
372
  (() => {
427
373
  let ws = null
428
- let panel = null
429
- let toggleBtn = null
430
- let inspectBtn = null // \u60AC\u6D6E inspect \u6309\u94AE
431
- let terminalContainer = null
432
- let terminalIframe = null
374
+ let ttydIframe = null
375
+ let devIframe = null
433
376
  let overlay = null
434
- let isOpen = false
435
377
  let isInspectMode = false
436
378
  let ttydWsUrl = null
379
+ let leftPanel = null
380
+ let divider = null
381
+ let rightPanel = null
382
+ let isDragging = false
383
+ let inspectListenersRegistered = false
437
384
 
438
385
  // Fetch the WebSocket port from the server
439
386
  async function getWsPort() {
@@ -448,75 +395,250 @@ var CLIENT_SCRIPT = `
448
395
  connect(port)
449
396
  } catch (err) {
450
397
  console.error('[Claude Dev Server] Failed to get port:', err)
451
- // Retry after 1 second
452
398
  setTimeout(initWhenReady, 1000)
453
399
  return
454
400
  }
455
- createToggleBtn()
456
- createInspectBtn()
401
+ // Create overlay first before split layout
457
402
  createOverlay()
458
- createPanel()
403
+ createSplitLayout()
459
404
  }
460
405
 
461
- function createToggleBtn() {
462
- if (toggleBtn) return
463
- toggleBtn = document.createElement('button')
464
- toggleBtn.className = 'claude-dev-server-toggle'
465
- toggleBtn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M11.376 24L10.776 23.544L10.44 22.8L10.776 21.312L11.16 19.392L11.472 17.856L11.76 15.96L11.928 15.336L11.904 15.288L11.784 15.312L10.344 17.28L8.16 20.232L6.432 22.056L6.024 22.224L5.304 21.864L5.376 21.192L5.784 20.616L8.16 17.568L9.6 15.672L10.536 14.592L10.512 14.448H10.464L4.128 18.576L3 18.72L2.496 18.264L2.568 17.52L2.808 17.28L4.704 15.96L9.432 13.32L9.504 13.08L9.432 12.96H9.192L8.4 12.912L5.712 12.84L3.384 12.744L1.104 12.624L0.528 12.504L0 11.784L0.048 11.424L0.528 11.112L1.224 11.16L2.736 11.28L5.016 11.424L6.672 11.52L9.12 11.784H9.504L9.552 11.616L9.432 11.52L9.336 11.424L6.96 9.84L4.416 8.16L3.072 7.176L2.352 6.672L1.992 6.216L1.848 5.208L2.496 4.488L3.384 4.56L3.6 4.608L4.488 5.304L6.384 6.768L8.88 8.616L9.24 8.904L9.408 8.808V8.736L9.24 8.472L7.896 6.024L6.456 3.528L5.808 2.496L5.64 1.872C5.576 1.656 5.544 1.416 5.544 1.152L6.288 0.144001L6.696 0L7.704 0.144001L8.112 0.504001L8.736 1.92L9.72 4.152L11.28 7.176L11.736 8.088L11.976 8.904L12.072 9.168H12.24V9.024L12.36 7.296L12.6 5.208L12.84 2.52L12.912 1.752L13.296 0.840001L14.04 0.360001L14.616 0.624001L15.096 1.32L15.024 1.752L14.76 3.6L14.184 6.504L13.824 8.472H14.04L14.28 8.208L15.264 6.912L16.92 4.848L17.64 4.032L18.504 3.12L19.056 2.688H20.088L20.832 3.816L20.496 4.992L19.44 6.336L18.552 7.464L17.28 9.168L16.512 10.536L16.584 10.632H16.752L19.608 10.008L21.168 9.744L22.992 9.432L23.832 9.816L23.928 10.2L23.592 11.016L21.624 11.496L19.32 11.952L15.888 12.768L15.84 12.792L15.888 12.864L17.424 13.008L18.096 13.056H19.728L22.752 13.272L23.544 13.8L24 14.424L23.928 14.928L22.704 15.528L21.072 15.144L17.232 14.232L15.936 13.92H15.744V14.016L16.848 15.096L18.84 16.896L21.36 19.224L21.48 19.8L21.168 20.28L20.832 20.232L18.624 18.552L17.76 17.808L15.84 16.2H15.72V16.368L16.152 17.016L18.504 20.544L18.624 21.624L18.456 21.96L17.832 22.176L17.184 22.056L15.792 20.136L14.376 17.952L13.224 16.008L13.104 16.104L12.408 23.352L12.096 23.712L11.376 24Z" fill="#d97757"/></svg>'
466
- toggleBtn.title = 'Open Claude Code (Cmd/Ctrl + \`)'
467
- toggleBtn.setAttribute('data-dev-tool', 'true')
468
- toggleBtn.addEventListener('click', () => togglePanel(true))
469
- document.body.appendChild(toggleBtn)
470
- }
406
+ function createSplitLayout() {
407
+ // IMPORTANT: Only create layout in the top-level window, not in iframes
408
+ // This prevents infinite nesting when the dev iframe loads
409
+ if (window.top !== window.self) {
410
+ console.log('[Claude Dev Server] Not in top window, skipping layout creation')
411
+ return
412
+ }
471
413
 
472
- function createInspectBtn() {
473
- if (inspectBtn) return
474
- inspectBtn = document.createElement('button')
475
- inspectBtn.className = 'claude-dev-server-inspect-btn'
476
- inspectBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 11V4a2 2 0 00-2-2H4a2 2 0 00-2 2v13a2 2 0 002 2h7"/><path d="M12 12l4.166 10 1.48-4.355L22 16.166 12 12z"/><path d="M18 18l3 3"/></svg>'
477
- inspectBtn.title = 'Inspect Element (Cmd/Ctrl + Shift + I)'
478
- inspectBtn.setAttribute('data-dev-tool', 'true')
414
+ // Check if already created
415
+ if (document.querySelector('.claude-dev-server-container')) return
416
+ // Also check if we're already in an injected page
417
+ if (window.__CLAUDE_SPLIT_LAYOUT_CREATED__) return
418
+ window.__CLAUDE_SPLIT_LAYOUT_CREATED__ = true
419
+
420
+ // Get original HTML from window variable (set by server)
421
+ // The HTML is Base64 encoded to avoid any escaping issues
422
+ let originalHtml
423
+ if (window.__CLAUDE_ORIGINAL_HTML_BASE64__) {
424
+ // Decode Base64
425
+ try {
426
+ const decoded = atob(window.__CLAUDE_ORIGINAL_HTML_BASE64__)
427
+ // Handle UTF-8 encoding
428
+ const bytes = new Uint8Array(decoded.length)
429
+ for (let i = 0; i < decoded.length; i++) {
430
+ bytes[i] = decoded.charCodeAt(i)
431
+ }
432
+ originalHtml = new TextDecoder().decode(bytes)
433
+ } catch (e) {
434
+ console.error('[Claude Dev Server] Failed to decode Base64 HTML:', e)
435
+ originalHtml = document.documentElement.outerHTML
436
+ }
437
+ } else {
438
+ originalHtml = document.documentElement.outerHTML
439
+ }
440
+
441
+ // Create container
442
+ const container = document.createElement('div')
443
+ container.className = 'claude-dev-server-container'
444
+
445
+ // Left panel - terminal (Claude)
446
+ leftPanel = document.createElement('div')
447
+ leftPanel.className = 'claude-dev-server-left'
448
+
449
+ // Terminal header
450
+ const header = document.createElement('div')
451
+ header.className = 'claude-dev-server-terminal-header'
452
+ header.innerHTML = \`
453
+ <span class="claude-dev-server-title">
454
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
455
+ <path d="M11.376 24L10.776 23.544L10.44 22.8L10.776 21.312L11.16 19.392L11.472 17.856L11.76 15.96L11.928 15.336L11.904 15.288L11.784 15.312L10.344 17.28L8.16 20.232L6.432 22.056L6.024 22.224L5.304 21.864L5.376 21.192L5.784 20.616L8.16 17.568L9.6 15.672L10.536 14.592L10.512 14.448H10.464L4.128 18.576L3 18.72L2.496 18.264L2.568 17.52L2.808 17.28L4.704 15.96L9.432 13.32L9.504 13.08L9.432 12.96H9.192L8.4 12.912L5.712 12.84L3.384 12.744L1.104 12.624L0.528 12.504L0 11.784L0.048 11.424L0.528 11.112L1.224 11.16L2.736 11.28L5.016 11.424L6.672 11.52L9.12 11.784H9.504L9.552 11.616L9.432 11.52L9.336 11.424L6.96 9.84L4.416 8.16L3.072 7.176L2.352 6.672L1.992 6.216L1.848 5.208L2.496 4.488L3.384 4.56L3.6 4.608L4.488 5.304L6.384 6.768L8.88 8.616L9.24 8.904L9.408 8.808V8.736L9.24 8.472L7.896 6.024L6.456 3.528L5.808 2.496L5.64 1.872C5.576 1.656 5.544 1.416 5.544 1.152L6.288 0.144001L6.696 0L7.704 0.144001L8.112 0.504001L8.736 1.92L9.72 4.152L11.28 7.176L11.736 8.088L11.976 8.904L12.072 9.168H12.24V9.024L12.36 7.296L12.6 5.208L12.84 2.52L12.912 1.752L13.296 0.840001L14.04 0.360001L14.616 0.624001L15.096 1.32L15.024 1.752L14.76 3.6L14.184 6.504L13.824 8.472H14.04L14.28 8.208L15.264 6.912L16.92 4.848L17.64 4.032L18.504 3.12L19.056 2.688H20.088L20.832 3.816L20.496 4.992L19.44 6.336L18.552 7.464L17.28 9.168L16.512 10.536L16.584 10.632H16.752L19.608 10.008L21.168 9.744L22.992 9.432L23.832 9.816L23.928 10.2L23.592 11.016L21.624 11.496L19.32 11.952L15.888 12.768L15.84 12.792L15.888 12.864L17.424 13.008L18.096 13.056H19.728L22.752 13.272L23.544 13.8L24 14.424L23.928 14.928L22.704 15.528L21.072 15.144L17.232 14.232L15.936 13.92H15.744V14.016L16.848 15.096L18.84 16.896L21.36 19.224L21.48 19.8L21.168 20.28L20.832 20.232L18.624 18.552L17.76 17.808L15.84 16.2H15.72V16.368L16.152 17.016L18.504 20.544L18.624 21.624L18.456 21.96L17.832 22.176L17.184 22.056L15.792 20.136L14.376 17.952L13.224 16.008L13.104 16.104L12.408 23.352L12.096 23.712L11.376 24Z" fill="#d97757"/>
456
+ </svg>
457
+ Claude Code
458
+ </span>
459
+ <div class="claude-dev-server-actions">
460
+ <button class="claude-dev-server-btn claude-dev-server-btn-inspect" title="Inspect Element">
461
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
462
+ <path d="M19 11V4a2 2 0 00-2-2H4a2 2 0 00-2 2v13a2 2 0 002 2h7"/>
463
+ <path d="M12 12l4.166 10 1.48-4.355L22 16.166 12 12z"/>
464
+ <path d="M18 18l3 3"/>
465
+ </svg>
466
+ Inspect
467
+ </button>
468
+ </div>
469
+ \`
470
+
471
+ const inspectBtn = header.querySelector('.claude-dev-server-btn-inspect')
479
472
  inspectBtn.addEventListener('click', () => {
480
473
  if (isInspectMode) {
481
474
  disableInspectMode()
482
475
  } else {
483
- // \u70B9\u51FB Inspect \u65F6\u5148\u6536\u8D77\u9762\u677F
484
- if (isOpen) {
485
- togglePanel(false)
486
- }
487
476
  enableInspectMode()
488
477
  }
489
478
  })
490
- document.body.appendChild(inspectBtn)
491
- }
492
479
 
493
- function createOverlay() {
494
- if (overlay) return
495
- overlay = document.createElement('div')
496
- overlay.className = 'claude-dev-server-inspect-overlay'
480
+ // Terminal container
481
+ const terminal = document.createElement('div')
482
+ terminal.className = 'claude-dev-server-terminal'
483
+
484
+ // Ttyd iframe
485
+ ttydIframe = document.createElement('iframe')
486
+ ttydIframe.className = 'claude-dev-server-terminal-iframe'
487
+ ttydIframe.allow = 'clipboard-read; clipboard-write'
488
+
489
+ terminal.appendChild(ttydIframe)
490
+ leftPanel.appendChild(header)
491
+ leftPanel.appendChild(terminal)
492
+
493
+ // Divider - draggable
494
+ divider = document.createElement('div')
495
+ divider.className = 'claude-dev-server-divider'
496
+ setupDraggable(divider)
497
+
498
+ // Right panel - dev server
499
+ rightPanel = document.createElement('div')
500
+ rightPanel.className = 'claude-dev-server-right'
501
+
502
+ // Create dev server iframe with srcdoc
503
+ devIframe = document.createElement('iframe')
504
+ devIframe.className = 'claude-dev-server-dev-iframe'
505
+ // The HTML already has inspect script injected by server
506
+ devIframe.srcdoc = originalHtml
507
+
508
+ rightPanel.appendChild(devIframe)
509
+
510
+ // Assemble layout: Claude (left) | divider | dev server (right)
511
+ container.appendChild(leftPanel)
512
+ container.appendChild(divider)
513
+ container.appendChild(rightPanel)
514
+
515
+ // Replace body content
516
+ document.body.innerHTML = ''
517
+ document.body.appendChild(container)
497
518
  document.body.appendChild(overlay)
519
+
520
+ // Wait for iframe to load before setting up communication
521
+ devIframe.onload = () => {
522
+ console.log('[Claude Dev Server] Dev iframe loaded')
523
+ // Setup inspect mode communication
524
+ setupInspectCommunication()
525
+ }
498
526
  }
499
527
 
500
- function highlightElement(el) {
501
- if (!overlay) return
502
- overlay.innerHTML = ''
503
- const rect = el.getBoundingClientRect()
504
- const highlight = document.createElement('div')
505
- highlight.className = 'claude-dev-server-highlight'
506
- highlight.style.top = rect.top + 'px'
507
- highlight.style.left = rect.left + 'px'
508
- highlight.style.width = rect.width + 'px'
509
- highlight.style.height = rect.height + 'px'
510
- const className = el.className ? String(el.className) : ''
511
- highlight.dataset.element = el.tagName.toLowerCase() +
512
- (el.id ? '#' + el.id : '') +
513
- (className ? '.' + className.split(' ').join('.') : '')
514
- overlay.appendChild(highlight)
528
+ function setupInspectCommunication() {
529
+ // Inspect mode is now handled entirely in the outer layer
530
+ // We'll access the iframe's document directly
515
531
  }
516
532
 
517
- async function getSourceLocation(el) {
533
+ function setupIframeInspectListeners() {
534
+ if (!devIframe || !devIframe.contentDocument) return
535
+
536
+ // Prevent duplicate listener registration
537
+ if (inspectListenersRegistered) {
538
+ return
539
+ }
540
+
541
+ const iframeDoc = devIframe.contentDocument
542
+ const iframeWindow = devIframe.contentWindow
543
+
544
+ const inspectHandler = (e) => {
545
+ if (!isInspectMode) return
546
+
547
+ e.preventDefault()
548
+ e.stopPropagation()
549
+
550
+ if (e.type === 'click') {
551
+ const el = e.target
552
+ const rect = el.getBoundingClientRect()
553
+ const className = el.className ? String(el.className) : ''
554
+ // Limit classnames to first 2-3 to avoid overly long selectors
555
+ const classNames = className.split(' ').filter(c => c).slice(0, 3)
556
+ const selector = el.tagName.toLowerCase() +
557
+ (el.id ? '#' + el.id : '') +
558
+ (classNames.length ? '.' + classNames.join('.') : '')
559
+
560
+ // Highlight element
561
+ if (overlay) {
562
+ overlay.innerHTML = ''
563
+
564
+ // Get iframe position on page
565
+ const iframeRect = devIframe.getBoundingClientRect()
566
+
567
+ // Calculate highlight position relative to main document
568
+ // Element rect is relative to iframe viewport, need to add iframe position
569
+ const highlightTop = iframeRect.top + rect.top
570
+ const highlightLeft = iframeRect.left + rect.left
571
+
572
+ const highlight = document.createElement('div')
573
+ highlight.className = 'claude-dev-server-highlight'
574
+ highlight.style.top = highlightTop + 'px'
575
+ highlight.style.left = highlightLeft + 'px'
576
+ highlight.style.width = rect.width + 'px'
577
+ highlight.style.height = rect.height + 'px'
578
+ highlight.dataset.element = selector
579
+ overlay.appendChild(highlight)
580
+ }
581
+
582
+ // Get source location and send to terminal
583
+ getSourceLocationFromElement(el).then(location => {
584
+ if (location) {
585
+ sendToTerminal(location)
586
+ }
587
+ disableInspectMode()
588
+ })
589
+ } else if (e.type === 'mousemove') {
590
+ const el = e.target
591
+ const rect = el.getBoundingClientRect()
592
+ const className = el.className ? String(el.className) : ''
593
+ // Limit classnames to first 2-3 to avoid overly long selectors
594
+ const classNames = className.split(' ').filter(c => c).slice(0, 3)
595
+ const selector = el.tagName.toLowerCase() +
596
+ (el.id ? '#' + el.id : '') +
597
+ (classNames.length ? '.' + classNames.join('.') : '')
598
+
599
+ if (overlay) {
600
+ overlay.innerHTML = ''
601
+
602
+ // Get iframe position on page
603
+ const iframeRect = devIframe.getBoundingClientRect()
604
+
605
+ // Calculate highlight position relative to main document
606
+ const highlightTop = iframeRect.top + rect.top
607
+ const highlightLeft = iframeRect.left + rect.left
608
+
609
+ const highlight = document.createElement('div')
610
+ highlight.className = 'claude-dev-server-highlight'
611
+ highlight.style.top = highlightTop + 'px'
612
+ highlight.style.left = highlightLeft + 'px'
613
+ highlight.style.width = rect.width + 'px'
614
+ highlight.style.height = rect.height + 'px'
615
+ highlight.dataset.element = selector
616
+ overlay.appendChild(highlight)
617
+ }
618
+ }
619
+ }
620
+
621
+ // Store handler reference for later removal
622
+ iframeDoc._claudeInspectHandler = inspectHandler
623
+
624
+ // Add event listeners in capture phase
625
+ iframeDoc.addEventListener('click', inspectHandler, true)
626
+ iframeDoc.addEventListener('mousemove', inspectHandler, true)
627
+
628
+ // Mark listeners as registered
629
+ inspectListenersRegistered = true
630
+ }
631
+
632
+ async function handleInspectElement(elementInfo) {
633
+ // This is now handled directly in setupIframeInspectListeners
634
+ }
635
+
636
+ async function getSourceLocationFromElement(element) {
637
+ // Access the iframe's document to detect Next.js chunks
638
+ const iframeDoc = devIframe?.contentDocument || document
639
+
518
640
  // Early check: detect Next.js by checking for _next/static chunks
519
- const hasNextJsChunks = Array.from(document.querySelectorAll('script[src]'))
641
+ const hasNextJsChunks = Array.from(iframeDoc.querySelectorAll('script[src]'))
520
642
  .some(script => script.getAttribute('src')?.includes('/_next/static/chunks/'))
521
643
 
522
644
  if (hasNextJsChunks) {
@@ -540,16 +662,37 @@ var CLIENT_SCRIPT = `
540
662
  const result = await response.json()
541
663
  console.log('[Claude Dev Server] Lookup result:', result)
542
664
  if (result.file) {
543
- const elClassName = el.className ? String(el.className) : ''
544
- const selector = el.tagName.toLowerCase() +
545
- (el.id ? '#' + el.id : '') +
546
- (elClassName ? '.' + elClassName.split(' ').filter(c => c).join('.') : '')
665
+ const elClassName = element.className ? String(element.className) : ''
666
+ // Limit classnames to first 2-3 to avoid overly long selectors
667
+ const classNames = elClassName.split(' ').filter(c => c).slice(0, 3)
668
+ const selector = element.tagName.toLowerCase() +
669
+ (element.id ? '#' + element.id : '') +
670
+ (classNames.length ? '.' + classNames.join('.') : '')
671
+
672
+ // Extract text content from element
673
+ let textContent = ''
674
+ if (element.nodeType === Node.TEXT_NODE) {
675
+ textContent = element.textContent ? element.textContent.trim().substring(0, 50) : ''
676
+ } else if (element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) {
677
+ textContent = element.childNodes[0].textContent ? element.childNodes[0].textContent.trim().substring(0, 50) : ''
678
+ } else {
679
+ for (const child of element.childNodes) {
680
+ if (child.nodeType === Node.TEXT_NODE && child.textContent && child.textContent.trim()) {
681
+ textContent = child.textContent.trim().substring(0, 50)
682
+ break
683
+ }
684
+ }
685
+ }
686
+ if (textContent && textContent.length >= 50) {
687
+ textContent += '...'
688
+ }
547
689
 
548
690
  return {
549
691
  url: result.file,
550
692
  line: result.line || undefined,
551
693
  column: result.column || undefined,
552
694
  selector: selector,
695
+ text: textContent,
553
696
  hint: 'File: ' + result.file
554
697
  }
555
698
  }
@@ -682,12 +825,12 @@ var CLIENT_SCRIPT = `
682
825
  }
683
826
 
684
827
  // Try React DevTools - handle React 18's randomized suffix
685
- const elKeys = Object.keys(el)
828
+ const elKeys = Object.keys(element)
686
829
  let fiberKey = elKeys.find(k => k === '__reactFiber__' || k === '__reactInternalInstance' || k.indexOf('__reactFiber') === 0)
687
830
  let propsKey = elKeys.find(k => k === '__reactProps__' || k.indexOf('__reactProps') === 0)
688
831
 
689
832
  if (fiberKey) {
690
- const fiber = el[fiberKey]
833
+ const fiber = element[fiberKey]
691
834
  console.log('[Claude Dev Server] Found fiber at key:', fiberKey)
692
835
 
693
836
  // Log fiber structure for debugging
@@ -835,7 +978,7 @@ var CLIENT_SCRIPT = `
835
978
 
836
979
  // Try Vue component
837
980
  if (!sourceFile) {
838
- const vueComponent = el.__vueParentComponent || el.__vnode
981
+ const vueComponent = element.__vueParentComponent || element.__vnode
839
982
  if (vueComponent) {
840
983
  const type = vueComponent.type || vueComponent.component
841
984
  if (type && type.__file) {
@@ -848,7 +991,7 @@ var CLIENT_SCRIPT = `
848
991
  // Try Vite's HMR source map
849
992
  if (!sourceFile) {
850
993
  // Look for data-vite-dev-id or similar attributes
851
- const viteId = el.getAttribute('data-vite-dev-id')
994
+ const viteId = element.getAttribute('data-vite-dev-id')
852
995
  if (viteId) {
853
996
  // Extract file path from viteId (remove query params if any)
854
997
  const queryIndex = viteId.indexOf('?')
@@ -859,22 +1002,22 @@ var CLIENT_SCRIPT = `
859
1002
 
860
1003
  // Check for inline script
861
1004
  if (!sourceFile) {
862
- const inlineScripts = document.querySelectorAll('script:not([src])')
1005
+ const inlineScripts = iframeDoc.querySelectorAll('script:not([src])')
863
1006
  if (inlineScripts.length > 0) {
864
1007
  const pathParts = window.location.pathname.split('/')
865
1008
  const fileName = pathParts[pathParts.length - 1] || 'index.html'
866
1009
  sourceFile = '/' + fileName
867
1010
 
868
- const html = document.documentElement.outerHTML
869
- const elId = el.id ? el.id : ''
870
- const elClass = el.className ? el.className : ''
1011
+ const html = iframeDoc.documentElement.outerHTML
1012
+ const elId = element.id ? element.id : ''
1013
+ const elClass = element.className ? element.className : ''
871
1014
  let elPattern
872
1015
  if (elId) {
873
1016
  elPattern = 'id="' + elId + '"'
874
1017
  } else if (elClass) {
875
1018
  elPattern = 'class="' + elClass.split(' ')[0] + '"'
876
1019
  } else {
877
- elPattern = el.tagName.toLowerCase()
1020
+ elPattern = element.tagName.toLowerCase()
878
1021
  }
879
1022
 
880
1023
  const matchIndex = html.indexOf(elPattern)
@@ -887,7 +1030,7 @@ var CLIENT_SCRIPT = `
887
1030
 
888
1031
  // Find main script
889
1032
  if (!sourceFile) {
890
- const scripts = document.querySelectorAll('script[src]')
1033
+ const scripts = iframeDoc.querySelectorAll('script[src]')
891
1034
  for (const script of scripts) {
892
1035
  const src = script.getAttribute('src')
893
1036
  if (src && !src.includes('cdn') && !src.includes('node_modules') &&
@@ -905,155 +1048,227 @@ var CLIENT_SCRIPT = `
905
1048
  sourceFile = '/' + fileName
906
1049
  }
907
1050
 
908
- const elClassName = el.className ? String(el.className) : ''
909
- const selector = el.tagName.toLowerCase() +
910
- (el.id ? '#' + el.id : '') +
911
- (elClassName ? '.' + elClassName.split(' ').filter(c => c).join('.') : '')
1051
+ const elClassName = element.className ? String(element.className) : ''
1052
+ // Limit classnames to first 2-3 to avoid overly long selectors
1053
+ const classNames = elClassName.split(' ').filter(c => c).slice(0, 3)
1054
+ const selector = element.tagName.toLowerCase() +
1055
+ (element.id ? '#' + element.id : '') +
1056
+ (classNames.length ? '.' + classNames.join('.') : '')
1057
+
1058
+ // Get text content for better context
1059
+ let textContent = ''
1060
+ if (element.nodeType === Node.TEXT_NODE) {
1061
+ textContent = element.textContent ? element.textContent.trim().substring(0, 50) : ''
1062
+ } else if (element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) {
1063
+ textContent = element.childNodes[0].textContent ? element.childNodes[0].textContent.trim().substring(0, 50) : ''
1064
+ } else {
1065
+ // Try to get first text node from children
1066
+ for (const child of element.childNodes) {
1067
+ if (child.nodeType === Node.TEXT_NODE && child.textContent && child.textContent.trim()) {
1068
+ textContent = child.textContent.trim().substring(0, 50)
1069
+ break
1070
+ }
1071
+ }
1072
+ }
1073
+ if (textContent && textContent.length >= 50) {
1074
+ textContent += '...'
1075
+ }
912
1076
 
913
1077
  return {
914
1078
  url: sourceFile,
915
1079
  line: sourceLine,
916
1080
  column: sourceColumn,
917
1081
  selector: selector,
1082
+ text: textContent,
918
1083
  hint: sourceFile ? 'File: ' + sourceFile : 'Element: ' + selector
919
1084
  }
920
1085
  }
921
1086
 
922
- function enableInspectMode() {
923
- isInspectMode = true
924
- overlay && overlay.classList.add('active')
925
- document.body.style.cursor = 'crosshair'
926
-
927
- // \u66F4\u65B0\u60AC\u6D6E inspect \u6309\u94AE\u72B6\u6001
928
- if (inspectBtn) inspectBtn.classList.add('active')
929
-
930
- // \u68C0\u67E5\u5143\u7D20\u662F\u5426\u5C5E\u4E8E dev tools
931
- function isDevToolElement(el) {
932
- if (!el) return false
933
- // \u68C0\u67E5\u5143\u7D20\u672C\u8EAB
934
- if (el.dataset && el.dataset.devTool === 'true') return true
935
- // \u68C0\u67E5\u7236\u5143\u7D20
936
- let parent = el.parentElement
937
- while (parent) {
938
- if (parent.dataset && parent.dataset.devTool === 'true') return true
939
- parent = parent.parentElement
1087
+ function setupDraggable(divider) {
1088
+ let startX = 0
1089
+ let startWidth = 0
1090
+ let containerWidth = 0
1091
+
1092
+ // Remove existing listeners to avoid duplicates
1093
+ divider.removeEventListener('mousedown', startDrag)
1094
+ divider.removeEventListener('touchstart', startDrag)
1095
+
1096
+ divider.addEventListener('mousedown', startDrag)
1097
+ divider.addEventListener('touchstart', startDrag)
1098
+
1099
+ function startDrag(e) {
1100
+ isDragging = true
1101
+ divider.classList.add('dragging')
1102
+
1103
+ // Disable pointer events on iframes to prevent interference
1104
+ const iframes = document.querySelectorAll('iframe')
1105
+ iframes.forEach(iframe => iframe.style.pointerEvents = 'none')
1106
+
1107
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX
1108
+ startX = clientX
1109
+
1110
+ const container = document.querySelector('.claude-dev-server-container')
1111
+ const leftPanelEl = document.querySelector('.claude-dev-server-left')
1112
+
1113
+ if (container && leftPanelEl) {
1114
+ containerWidth = container.offsetWidth
1115
+ startWidth = leftPanelEl.offsetWidth
940
1116
  }
941
- return false
1117
+
1118
+ e.preventDefault()
942
1119
  }
943
1120
 
944
- const onMouseMove = async (e) => {
945
- if (!isInspectMode) return
946
- const el = document.elementFromPoint(e.clientX, e.clientY)
947
- // \u5FFD\u7565 dev tools \u5143\u7D20\u548C overlay
948
- if (el && el !== overlay && !isDevToolElement(el) && (!overlay || !overlay.contains(el))) {
949
- highlightElement(el)
1121
+ function drag(e) {
1122
+ if (!isDragging) return
1123
+
1124
+ // Safety check: if mouse button is not pressed, end drag
1125
+ if (e.buttons === 0 && !e.touches) {
1126
+ endDrag()
1127
+ return
1128
+ }
1129
+
1130
+ const clientX = e.touches ? e.touches[0].clientX : e.clientX
1131
+ const deltaX = clientX - startX
1132
+
1133
+ // Calculate new width in pixels, then convert to percentage
1134
+ let newWidth = startWidth + deltaX
1135
+ let percentage = (newWidth / containerWidth) * 100
1136
+
1137
+ // Clamp between 20% and 80%
1138
+ const clamped = Math.max(20, Math.min(80, percentage))
1139
+
1140
+ const leftPanelEl = document.querySelector('.claude-dev-server-left')
1141
+ if (leftPanelEl) {
1142
+ leftPanelEl.style.flex = 'none'
1143
+ leftPanelEl.style.width = clamped + '%'
950
1144
  }
951
1145
  }
952
1146
 
953
- const onClick = async (e) => {
954
- if (!isInspectMode) return
955
- e.preventDefault()
956
- e.stopPropagation()
1147
+ function endDrag() {
1148
+ if (isDragging) {
1149
+ isDragging = false
1150
+ divider.classList.remove('dragging')
957
1151
 
958
- const el = document.elementFromPoint(e.clientX, e.clientY)
959
- // \u5FFD\u7565 dev tools \u5143\u7D20\u548C overlay
960
- if (el && el !== overlay && !isDevToolElement(el) && (!overlay || !overlay.contains(el))) {
961
- const location = await getSourceLocation(el)
962
- if (location) {
963
- await inspectElement(location, el)
964
- }
965
- disableInspectMode()
1152
+ // Re-enable pointer events on iframes
1153
+ const iframes = document.querySelectorAll('iframe')
1154
+ iframes.forEach(iframe => iframe.style.pointerEvents = '')
966
1155
  }
967
1156
  }
968
1157
 
969
- document.addEventListener('mousemove', onMouseMove, true)
970
- document.addEventListener('click', onClick, true)
1158
+ // Use capture phase to ensure events are caught
1159
+ // Remove old listeners first to prevent duplicates
1160
+ document.removeEventListener('mousemove', drag, true)
1161
+ document.removeEventListener('touchmove', drag, true)
1162
+ document.removeEventListener('mouseup', endDrag, true)
1163
+ document.removeEventListener('touchend', endDrag, true)
1164
+
1165
+ document.addEventListener('mousemove', drag, true)
1166
+ document.addEventListener('touchmove', drag, { passive: false, capture: true })
1167
+ document.addEventListener('mouseup', endDrag, true)
1168
+ document.addEventListener('touchend', endDrag, true)
1169
+ }
971
1170
 
972
- if (overlay) overlay._inspectHandlers = { onMouseMove, onClick }
1171
+ function createOverlay() {
1172
+ if (overlay) return
1173
+ overlay = document.createElement('div')
1174
+ overlay.className = 'claude-dev-server-inspect-overlay'
1175
+ document.body.appendChild(overlay)
1176
+ }
1177
+
1178
+ function enableInspectMode() {
1179
+ isInspectMode = true
1180
+ if (overlay) overlay.classList.add('active')
1181
+ // Setup inspect listeners on the iframe
1182
+ setupIframeInspectListeners()
1183
+ // Change cursor in iframe
1184
+ if (devIframe && devIframe.contentDocument && devIframe.contentDocument.body) {
1185
+ devIframe.contentDocument.body.style.cursor = 'crosshair'
1186
+ }
1187
+ const btn = document.querySelector('.claude-dev-server-btn-inspect')
1188
+ if (btn) btn.classList.add('active')
973
1189
  }
974
1190
 
975
1191
  function disableInspectMode() {
976
1192
  isInspectMode = false
977
- if (overlay) overlay.classList.remove('active')
978
- document.body.style.cursor = ''
979
- if (overlay) overlay.innerHTML = ''
1193
+ if (overlay) {
1194
+ overlay.classList.remove('active')
1195
+ overlay.innerHTML = ''
1196
+ }
1197
+ // Restore cursor in iframe
1198
+ if (devIframe && devIframe.contentDocument && devIframe.contentDocument.body) {
1199
+ devIframe.contentDocument.body.style.cursor = ''
1200
+ }
1201
+ const btn = document.querySelector('.claude-dev-server-btn-inspect')
1202
+ if (btn) btn.classList.remove('active')
980
1203
 
981
- // \u66F4\u65B0\u60AC\u6D6E inspect \u6309\u94AE\u72B6\u6001
982
- if (inspectBtn) inspectBtn.classList.remove('active')
1204
+ // Remove inspect listeners to free up resources
1205
+ removeIframeInspectListeners()
1206
+ }
983
1207
 
984
- const btn = panel && panel.querySelector('.claude-dev-server-btn-inspect')
985
- if (btn) btn.classList.remove('active')
1208
+ function removeIframeInspectListeners() {
1209
+ if (!devIframe || !devIframe.contentDocument) return
986
1210
 
987
- const handlers = overlay && overlay._inspectHandlers
988
- if (handlers) {
989
- document.removeEventListener('mousemove', handlers.onMouseMove, true)
990
- document.removeEventListener('click', handlers.onClick, true)
1211
+ const iframeDoc = devIframe.contentDocument
1212
+ const existingHandler = iframeDoc._claudeInspectHandler
1213
+
1214
+ if (existingHandler) {
1215
+ iframeDoc.removeEventListener('click', existingHandler, true)
1216
+ iframeDoc.removeEventListener('mousemove', existingHandler, true)
1217
+ delete iframeDoc._claudeInspectHandler
1218
+ inspectListenersRegistered = false
991
1219
  }
992
1220
  }
993
1221
 
994
- async function inspectElement(location, el) {
995
- // \u6784\u5EFA\u683C\u5F0F\u5316\u7684 prompt
1222
+ async function sendToTerminal(location) {
1223
+ // Use format: @filename <selector> "text content" (without line number)
996
1224
  const filePath = location.url || location.file || 'unknown'
997
- const tagName = el.tagName ? el.tagName.toLowerCase() : ''
998
- const id = el.id ? '#' + el.id : ''
999
-
1000
- // \u5BF9\u4E8E Tailwind CSS \u7B49\u5927\u91CF class \u7684\u60C5\u51B5\uFF0C\u53EA\u53D6\u524D 2-3 \u4E2A class
1001
- let className = ''
1002
- if (el.className) {
1003
- const classes = String(el.className).split(' ').filter(c => c)
1004
- // \u5982\u679C class \u592A\u591A\uFF08\u53EF\u80FD\u662F Tailwind\uFF09\uFF0C\u53EA\u53D6\u524D 2 \u4E2A
1005
- if (classes.length > 5) {
1006
- className = '.' + classes.slice(0, 2).join('.')
1007
- } else {
1008
- className = '.' + classes.join('.')
1009
- }
1010
- }
1011
1225
 
1012
- const selector = tagName + id + className
1226
+ // Build selector - handle Tailwind CSS classes by limiting
1227
+ const tagName = location.selector ? location.selector.split(/[.#]/)[0] : 'div'
1228
+ let selector = location.selector || tagName
1013
1229
 
1014
- // \u83B7\u53D6\u5143\u7D20\u7684\u6587\u672C\u5185\u5BB9\uFF08\u5982\u679C\u662F\u6587\u672C\u8282\u70B9\u6216\u77ED\u6587\u672C\u5143\u7D20\uFF09
1230
+ // Get text content for better context
1015
1231
  let textContent = ''
1016
- if (el.nodeType === Node.TEXT_NODE) {
1017
- textContent = el.textContent ? el.textContent.trim().substring(0, 50) : ''
1018
- } else if (el.childNodes.length === 1 && el.childNodes[0].nodeType === Node.TEXT_NODE) {
1019
- textContent = el.childNodes[0].textContent ? el.childNodes[0].textContent.trim().substring(0, 50) : ''
1232
+ if (location.text) {
1233
+ textContent = location.text
1234
+ } else if (location.hint && location.hint.includes('File:')) {
1235
+ // No text content available
1020
1236
  }
1021
1237
 
1022
- // Check if file is in node_modules - if so, skip the @filename part
1023
- const isNodeModules = filePath.includes('node_modules')
1024
-
1025
- // \u683C\u5F0F: @filename:line <selector> "text content" \u6216 @filename <selector> "text content"
1026
- // \u5982\u679C\u662F node_modules \u6587\u4EF6\uFF0C\u53EA\u4FDD\u7559 <selector> "text content"
1027
- let prompt = ''
1028
- if (isNodeModules) {
1029
- // Skip @filename for node_modules, just show element and text
1030
- prompt = \`<\${selector}>\`
1031
- if (textContent) {
1032
- prompt += \` "\${textContent}"\`
1033
- }
1034
- console.log('[Claude Dev Server] Skipping node_modules path:', filePath)
1035
- } else {
1036
- const linePart = location.line ? \`:\${location.line}\` : ''
1037
- prompt = \`@\${filePath}\${linePart} <\${selector}>\`
1038
- if (textContent) {
1039
- prompt += \` "\${textContent}"\`
1040
- }
1238
+ // Format: @filename <selector> "text content" (no line number)
1239
+ let prompt = \`@\${filePath} <\${selector}>\`
1240
+ if (textContent) {
1241
+ prompt += \` "\${textContent}"\`
1242
+ }
1243
+
1244
+ console.log('[Claude Dev Server] Sending to terminal:', prompt)
1245
+
1246
+ // Store the interval ID so we can clear it if needed
1247
+ if (!window._claudeSendRetryInterval) {
1248
+ window._claudeSendRetryInterval = null
1249
+ }
1250
+
1251
+ // Clear any existing retry interval
1252
+ if (window._claudeSendRetryInterval) {
1253
+ clearInterval(window._claudeSendRetryInterval)
1254
+ window._claudeSendRetryInterval = null
1041
1255
  }
1042
1256
 
1043
- // \u53D1\u9001\u6210\u529F\u540E\u5C55\u5F00\u9762\u677F
1044
- const sendToTerminal = () => {
1045
- if (!terminalIframe || !terminalIframe.contentWindow) {
1257
+ // Send to terminal with retry logic
1258
+ const sendToTerminalInternal = () => {
1259
+ if (!ttydIframe || !ttydIframe.contentWindow) {
1046
1260
  return false
1047
1261
  }
1048
1262
 
1049
- const win = terminalIframe.contentWindow
1050
-
1051
- // Check if sendToTerminal function is available
1263
+ const win = ttydIframe.contentWindow
1052
1264
  if (win.sendToTerminal) {
1053
1265
  win.sendToTerminal(prompt)
1054
1266
  console.log('[Claude Dev Server] Sent via sendToTerminal:', prompt)
1055
- // \u53D1\u9001\u6210\u529F\u540E\u5C55\u5F00\u9762\u677F
1056
- setTimeout(() => togglePanel(true), 100)
1267
+ // Clear interval on success
1268
+ if (window._claudeSendRetryInterval) {
1269
+ clearInterval(window._claudeSendRetryInterval)
1270
+ window._claudeSendRetryInterval = null
1271
+ }
1057
1272
  return true
1058
1273
  }
1059
1274
 
@@ -1061,17 +1276,18 @@ var CLIENT_SCRIPT = `
1061
1276
  }
1062
1277
 
1063
1278
  // Try immediately
1064
- if (sendToTerminal()) {
1279
+ if (sendToTerminalInternal()) {
1065
1280
  return
1066
1281
  }
1067
1282
 
1068
1283
  // If not ready, wait and retry
1069
1284
  let attempts = 0
1070
1285
  const maxAttempts = 50
1071
- const retryInterval = setInterval(() => {
1286
+ window._claudeSendRetryInterval = setInterval(() => {
1072
1287
  attempts++
1073
- if (sendToTerminal() || attempts >= maxAttempts) {
1074
- clearInterval(retryInterval)
1288
+ if (sendToTerminalInternal() || attempts >= maxAttempts) {
1289
+ clearInterval(window._claudeSendRetryInterval)
1290
+ window._claudeSendRetryInterval = null
1075
1291
  if (attempts >= maxAttempts) {
1076
1292
  console.warn('[Claude Dev Server] Could not send to terminal after retries')
1077
1293
  }
@@ -1080,86 +1296,12 @@ var CLIENT_SCRIPT = `
1080
1296
  }
1081
1297
 
1082
1298
  function loadTerminalIframe(ttydUrl) {
1083
- if (!terminalContainer || terminalIframe) return
1299
+ if (!ttydIframe) return
1084
1300
  ttydWsUrl = ttydUrl
1085
-
1086
- // Create iframe pointing to ttyd/index.html with ttyd WebSocket URL
1087
- terminalIframe = document.createElement('iframe')
1088
- // Pass the ttyd WebSocket URL as query param
1089
- terminalIframe.src = '/ttyd/index.html?ws=' + encodeURIComponent(ttydUrl)
1090
- terminalIframe.allow = 'clipboard-read; clipboard-write'
1091
-
1092
- // Load event - iframe is ready
1093
- terminalIframe.onload = () => {
1301
+ ttydIframe.src = '/ttyd/index.html?ws=' + encodeURIComponent(ttydUrl)
1302
+ ttydIframe.onload = () => {
1094
1303
  console.log('[Claude Dev Server] Terminal iframe loaded')
1095
1304
  }
1096
-
1097
- terminalContainer.appendChild(terminalIframe)
1098
- }
1099
-
1100
- function createPanel() {
1101
- if (panel) return
1102
-
1103
- panel = document.createElement('div')
1104
- panel.className = 'claude-dev-server-panel'
1105
- panel.innerHTML = \`
1106
- <div class="claude-dev-server-header">
1107
- <span class="claude-dev-server-title">Claude Code</span>
1108
- <div class="claude-dev-server-actions">
1109
- <button class="claude-dev-server-btn claude-dev-server-btn-inspect" title="Inspect Element">
1110
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1111
- <path d="M2 12h20M12 2v20M4.93 4.93l14.14 14.14M19.07 4.93L4.93 19.07"/>
1112
- </svg>
1113
- Inspect
1114
- </button>
1115
- </div>
1116
- <button class="claude-dev-server-close" aria-label="Close" title="Minimize to icon">&times;</button>
1117
- </div>
1118
- <div class="claude-dev-server-terminal"></div>
1119
- \`
1120
-
1121
- document.body.appendChild(panel)
1122
-
1123
- terminalContainer = panel.querySelector('.claude-dev-server-terminal')
1124
- const closeBtn = panel.querySelector('.claude-dev-server-close')
1125
- const inspectBtn = panel.querySelector('.claude-dev-server-btn-inspect')
1126
-
1127
- closeBtn && closeBtn.addEventListener('click', () => togglePanel(false))
1128
-
1129
- inspectBtn && inspectBtn.addEventListener('click', () => {
1130
- if (isInspectMode) {
1131
- disableInspectMode()
1132
- } else {
1133
- // \u70B9\u51FB Inspect \u65F6\u5148\u6536\u8D77\u9762\u677F
1134
- if (isOpen) {
1135
- togglePanel(false)
1136
- }
1137
- enableInspectMode()
1138
- }
1139
- })
1140
- }
1141
-
1142
- function togglePanel(force) {
1143
- createPanel()
1144
- if (typeof force === 'boolean') {
1145
- isOpen = force
1146
- } else {
1147
- isOpen = !isOpen
1148
- }
1149
-
1150
- if (panel) panel.classList.toggle('open', isOpen)
1151
- if (toggleBtn) toggleBtn.classList.toggle('hidden', isOpen)
1152
-
1153
- if (isOpen) {
1154
- // Focus iframe when panel opens
1155
- if (terminalIframe && terminalIframe.contentWindow) {
1156
- setTimeout(() => {
1157
- if (terminalIframe.contentWindow.terminalInstance) {
1158
- terminalIframe.contentWindow.terminalInstance.focus()
1159
- }
1160
- }, 100)
1161
- }
1162
- }
1163
1305
  }
1164
1306
 
1165
1307
  function connect(port) {
@@ -1176,10 +1318,6 @@ var CLIENT_SCRIPT = `
1176
1318
  console.log('[Claude Dev Server] Received message:', msg.type, msg)
1177
1319
  if (msg.type === 'ready' && msg.ttydUrl) {
1178
1320
  loadTerminalIframe(msg.ttydUrl)
1179
- } else if (msg.type === 'inspectResult') {
1180
- if (msg.location) {
1181
- showContextPanel(msg.location, null, false)
1182
- }
1183
1321
  }
1184
1322
  } catch (err) {
1185
1323
  console.error('[Claude Dev Server] Message parse error:', err)
@@ -1199,12 +1337,13 @@ var CLIENT_SCRIPT = `
1199
1337
  initWhenReady()
1200
1338
 
1201
1339
  document.addEventListener('keydown', (e) => {
1202
- if ((e.metaKey || e.ctrlKey) && e.code === 'Backquote') {
1340
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'I') {
1203
1341
  e.preventDefault()
1204
- togglePanel()
1205
- }
1206
- if (e.key === 'Escape' && isOpen) {
1207
- togglePanel(false)
1342
+ if (isInspectMode) {
1343
+ disableInspectMode()
1344
+ } else {
1345
+ enableInspectMode()
1346
+ }
1208
1347
  }
1209
1348
  if (e.key === 'Escape' && isInspectMode) {
1210
1349
  disableInspectMode()
@@ -1493,21 +1632,59 @@ async function handleTurbopackLookup(projectRoot, pagePath, targetPort) {
1493
1632
  return { error: String(err) };
1494
1633
  }
1495
1634
  }
1635
+ function isHtmlPageRequest(req) {
1636
+ const accept = req.headers.accept || "";
1637
+ const url = req.url || "";
1638
+ if (!accept.includes("text/html")) {
1639
+ return false;
1640
+ }
1641
+ if (url.startsWith("/@") || url.startsWith("/_next/") || url.startsWith("/ttyd") || url.startsWith("/dev.html")) {
1642
+ return false;
1643
+ }
1644
+ return true;
1645
+ }
1496
1646
  function createProxyServer(targetPort, wsPort, projectRoot) {
1497
1647
  const moduleDir = dirname(fileURLToPath(import.meta.url));
1498
1648
  const assetsPath = join(moduleDir, "assets");
1499
1649
  let ttydHtml;
1500
1650
  let ttydBridgeJs;
1651
+ let devHtml;
1501
1652
  try {
1502
1653
  ttydHtml = readFileSync(join(assetsPath, "ttyd-terminal.html"), "utf-8");
1503
1654
  ttydBridgeJs = readFileSync(join(assetsPath, "ttyd-bridge.js"), "utf-8");
1655
+ devHtml = readFileSync(join(assetsPath, "dev.html"), "utf-8");
1504
1656
  } catch (e) {
1505
- console.error("[Claude Dev Server] Failed to read ttyd assets from", assetsPath);
1657
+ console.error("[Claude Dev Server] Failed to read assets from", assetsPath);
1506
1658
  console.error("[Claude Dev Server] moduleDir:", moduleDir);
1507
1659
  console.error("[Claude Dev Server] Error:", e.message);
1508
- throw new Error("ttyd assets not found. Please run `npm run build` first.");
1660
+ throw new Error("Assets not found. Please run `npm run build` first.");
1509
1661
  }
1510
1662
  const server = http.createServer((req, res) => {
1663
+ const referer = req.headers.referer || "";
1664
+ const isFromDevPage = referer.includes("dev.html");
1665
+ if (req.url?.startsWith("/dev.html")) {
1666
+ const urlParams = new URL(req.url || "", `http://${req.headers.host}`);
1667
+ const originalPath = urlParams.searchParams.get("path") || "/";
1668
+ const host = req.headers.host || "localhost:3000";
1669
+ const origin = `http://${host}`;
1670
+ const modifiedDevHtml = devHtml.replace(
1671
+ /__CLAUDE_IFRAME_SRC__/g,
1672
+ `${origin}${originalPath}`
1673
+ ).replace(
1674
+ /__CLAUDE_ORIGINAL_PATH__/g,
1675
+ originalPath
1676
+ );
1677
+ res.setHeader("Content-Type", "text/html");
1678
+ res.end(modifiedDevHtml);
1679
+ return;
1680
+ }
1681
+ if (!isFromDevPage && isHtmlPageRequest(req)) {
1682
+ const currentPath = req.url || "/";
1683
+ const devPageUrl = `/dev.html?path=${encodeURIComponent(currentPath)}`;
1684
+ res.writeHead(302, { "Location": devPageUrl });
1685
+ res.end();
1686
+ return;
1687
+ }
1511
1688
  if (req.url === "/@claude-port") {
1512
1689
  res.setHeader("Content-Type", "application/json");
1513
1690
  res.end(JSON.stringify({ port: wsPort }));
@@ -1569,6 +1746,7 @@ function createProxyServer(targetPort, wsPort, projectRoot) {
1569
1746
  }
1570
1747
  const proxyHeaders = { ...req.headers };
1571
1748
  delete proxyHeaders["accept-encoding"];
1749
+ const shouldInject = !isFromDevPage;
1572
1750
  const options = {
1573
1751
  hostname: "localhost",
1574
1752
  port: targetPort,
@@ -1577,7 +1755,7 @@ function createProxyServer(targetPort, wsPort, projectRoot) {
1577
1755
  headers: proxyHeaders
1578
1756
  };
1579
1757
  const proxyReq = http.request(options, (proxyRes) => {
1580
- if (proxyRes.headers["content-type"]?.includes("text/html")) {
1758
+ if (proxyRes.headers["content-type"]?.includes("text/html") && shouldInject) {
1581
1759
  const body = [];
1582
1760
  proxyRes.on("data", (chunk) => body.push(chunk));
1583
1761
  proxyRes.on("end", () => {
@@ -1659,20 +1837,33 @@ function createProxyServer(targetPort, wsPort, projectRoot) {
1659
1837
  return server;
1660
1838
  }
1661
1839
  function injectScripts(html, wsPort, projectRoot) {
1662
- const projectRootScript = `<script>window.__CLAUDE_PROJECT_ROOT__ = ${JSON.stringify(projectRoot)}</script>`;
1663
- return html.replace(
1664
- "</head>",
1665
- `<!-- Claude Dev Server -->
1666
- ${CLIENT_STYLES}
1667
- ${projectRootScript}
1668
- <script type="module">${CLIENT_SCRIPT}</script>
1669
- </head>`
1670
- ).replace(
1671
- /wsPort:\s*\d+/,
1672
- `wsPort: ${wsPort}`
1673
- );
1840
+ if (html.includes("claude-dev-server-container") || html.includes("__CLAUDE_ORIGINAL_HTML__")) {
1841
+ console.log("[Claude Dev Server] HTML already injected, returning as-is");
1842
+ return html;
1843
+ }
1844
+ const hasOurScripts = html.includes("CLIENT_SCRIPT") || html.includes("claude-dev-server-container");
1845
+ if (hasOurScripts) {
1846
+ console.log("[Claude Dev Server] Warning: Original HTML already has our scripts");
1847
+ }
1848
+ const modifiedHtml = html;
1849
+ console.log("[Claude Dev Server] Creating split layout, original HTML length:", html.length);
1850
+ const base64Html = Buffer.from(modifiedHtml, "utf-8").toString("base64");
1851
+ const projectRootScript = `<script>window.__CLAUDE_PROJECT_ROOT__ = ${JSON.stringify(projectRoot)};window.__CLAUDE_ORIGINAL_HTML_BASE64__ = "${base64Html}";</script>`;
1852
+ return `<!DOCTYPE html>
1853
+ <html lang="en">
1854
+ <head>
1855
+ <meta charset="UTF-8">
1856
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1857
+ <title>Claude Dev Server</title>
1858
+ ${CLIENT_STYLES}
1859
+ ${projectRootScript}
1860
+ </head>
1861
+ <body>
1862
+ <script type="module">${CLIENT_SCRIPT.replace(/wsPort:\s*\d+/, `wsPort: ${wsPort}`)}</script>
1863
+ </body>
1864
+ </html>`;
1674
1865
  }
1675
1866
 
1676
1867
  export { startUniversalServer };
1677
- //# sourceMappingURL=chunk-F6IKDNXJ.js.map
1678
- //# sourceMappingURL=chunk-F6IKDNXJ.js.map
1868
+ //# sourceMappingURL=chunk-PAE5WTS2.js.map
1869
+ //# sourceMappingURL=chunk-PAE5WTS2.js.map