@ait-co/devtools 0.1.129 → 0.1.131

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 (86) hide show
  1. package/dist/{attach-orchestrator-DvrgUIx7.js → attach-orchestrator-BiaYiNOr.js} +31 -13
  2. package/dist/{attach-orchestrator-DvrgUIx7.js.map → attach-orchestrator-BiaYiNOr.js.map} +1 -1
  3. package/dist/{attach-orchestrator-RhOdO3DS.js → attach-orchestrator-Cjdhir2U.js} +31 -13
  4. package/dist/{attach-orchestrator-RhOdO3DS.js.map → attach-orchestrator-Cjdhir2U.js.map} +1 -1
  5. package/dist/{attach-orchestrator-Bdt5Zdo9.js → attach-orchestrator-ca34r46y.js} +31 -13
  6. package/dist/{attach-orchestrator-Bdt5Zdo9.js.map → attach-orchestrator-ca34r46y.js.map} +1 -1
  7. package/dist/{cell-Ba9lCI3B.js → cell-Bi8MAPA8.js} +11 -7
  8. package/dist/cell-Bi8MAPA8.js.map +1 -0
  9. package/dist/{cell-DA_lcpDI.js → cell-Cw4fVOl2.js} +11 -7
  10. package/dist/cell-Cw4fVOl2.js.map +1 -0
  11. package/dist/{cell-DLRv4iG6.js → cell-rr6gL9AT.js} +11 -7
  12. package/dist/cell-rr6gL9AT.js.map +1 -0
  13. package/dist/{debug-server-Bu3pCG9c.js → debug-server-BSbfnFR5.js} +9 -9
  14. package/dist/debug-server-BSbfnFR5.js.map +1 -0
  15. package/dist/{debug-server-e1qPe0ZB.js → debug-server-CD4zYBq4.js} +4 -4
  16. package/dist/{debug-server-e1qPe0ZB.js.map → debug-server-CD4zYBq4.js.map} +1 -1
  17. package/dist/{debug-server--Y0QZVJS.js → debug-server-qRSRG7YZ.js} +15 -11
  18. package/dist/debug-server-qRSRG7YZ.js.map +1 -0
  19. package/dist/in-app/auto.js +15 -0
  20. package/dist/in-app/auto.js.map +1 -1
  21. package/dist/in-app/index.d.ts +27 -0
  22. package/dist/in-app/index.d.ts.map +1 -1
  23. package/dist/in-app/index.js +15 -0
  24. package/dist/in-app/index.js.map +1 -1
  25. package/dist/mcp/cli.js +143 -35
  26. package/dist/mcp/cli.js.map +1 -1
  27. package/dist/mcp/server.js +1 -1
  28. package/dist/panel/index.js +11 -1
  29. package/dist/panel/index.js.map +1 -1
  30. package/dist/{pool-C0pc7T2p.d.ts → pool-BaSCZALv.d.ts} +9 -1
  31. package/dist/{pool-C0pc7T2p.d.ts.map → pool-BaSCZALv.d.ts.map} +1 -1
  32. package/dist/{qr-http-server-HANhOFwI.js → qr-http-server-BdNGPRoL.js} +76 -6
  33. package/dist/qr-http-server-BdNGPRoL.js.map +1 -0
  34. package/dist/{qr-http-server-CrGohOwC.js → qr-http-server-Bq9zR7-z.js} +76 -6
  35. package/dist/qr-http-server-Bq9zR7-z.js.map +1 -0
  36. package/dist/{qr-http-server-DM2R25jP.cjs → qr-http-server-BxoRp2w7.cjs} +76 -6
  37. package/dist/qr-http-server-BxoRp2w7.cjs.map +1 -0
  38. package/dist/{qr-http-server-Bt_RAxFn.cjs → qr-http-server-CV6TA9ZN.cjs} +76 -6
  39. package/dist/qr-http-server-CV6TA9ZN.cjs.map +1 -0
  40. package/dist/{qr-http-server-s1iW_5VQ.js → qr-http-server-DCbjORFa.js} +76 -6
  41. package/dist/qr-http-server-DCbjORFa.js.map +1 -0
  42. package/dist/{qr-http-server-BH0UFeUn.js → qr-http-server-cKbciY5k.js} +76 -6
  43. package/dist/qr-http-server-cKbciY5k.js.map +1 -0
  44. package/dist/{relay-factory-Ck3vBonr.js → relay-factory-Cloog1jb.js} +33 -13
  45. package/dist/relay-factory-Cloog1jb.js.map +1 -0
  46. package/dist/{relay-worker-Bt0QYKQH.js → relay-worker-CCAHLC4a.js} +9 -5
  47. package/dist/relay-worker-CCAHLC4a.js.map +1 -0
  48. package/dist/test-runner/bin.js +45 -19
  49. package/dist/test-runner/bin.js.map +1 -1
  50. package/dist/test-runner/config.d.ts +15 -9
  51. package/dist/test-runner/config.d.ts.map +1 -1
  52. package/dist/test-runner/config.js +1 -1
  53. package/dist/test-runner/pool.d.ts +1 -1
  54. package/dist/test-runner/pool.js +1 -1
  55. package/dist/test-runner/pool.js.map +1 -1
  56. package/dist/test-runner/relay-factory.d.ts +22 -8
  57. package/dist/test-runner/relay-factory.d.ts.map +1 -1
  58. package/dist/test-runner/relay-factory.js +32 -12
  59. package/dist/test-runner/relay-factory.js.map +1 -1
  60. package/dist/test-runner/relay-worker.js +1 -1
  61. package/dist/{tunnel-DyEV6WwL.cjs → tunnel-B2UCKAc7.cjs} +4 -3
  62. package/dist/{tunnel-DyEV6WwL.cjs.map → tunnel-B2UCKAc7.cjs.map} +1 -1
  63. package/dist/{tunnel-BFwOSwT4.js → tunnel-C-rQ34dM.js} +4 -3
  64. package/dist/{tunnel-BFwOSwT4.js.map → tunnel-C-rQ34dM.js.map} +1 -1
  65. package/dist/unplugin/index.cjs +1 -1
  66. package/dist/unplugin/index.js +1 -1
  67. package/dist/unplugin/tunnel.cjs +3 -2
  68. package/dist/unplugin/tunnel.cjs.map +1 -1
  69. package/dist/unplugin/tunnel.d.cts.map +1 -1
  70. package/dist/unplugin/tunnel.d.ts.map +1 -1
  71. package/dist/unplugin/tunnel.js +3 -2
  72. package/dist/unplugin/tunnel.js.map +1 -1
  73. package/package.json +1 -1
  74. package/dist/cell-Ba9lCI3B.js.map +0 -1
  75. package/dist/cell-DA_lcpDI.js.map +0 -1
  76. package/dist/cell-DLRv4iG6.js.map +0 -1
  77. package/dist/debug-server--Y0QZVJS.js.map +0 -1
  78. package/dist/debug-server-Bu3pCG9c.js.map +0 -1
  79. package/dist/qr-http-server-BH0UFeUn.js.map +0 -1
  80. package/dist/qr-http-server-Bt_RAxFn.cjs.map +0 -1
  81. package/dist/qr-http-server-CrGohOwC.js.map +0 -1
  82. package/dist/qr-http-server-DM2R25jP.cjs.map +0 -1
  83. package/dist/qr-http-server-HANhOFwI.js.map +0 -1
  84. package/dist/qr-http-server-s1iW_5VQ.js.map +0 -1
  85. package/dist/relay-factory-Ck3vBonr.js.map +0 -1
  86. package/dist/relay-worker-Bt0QYKQH.js.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -2024,7 +2024,7 @@ async function readMcpSdkVersion() {
2024
2024
  * some test environments that skip the build step).
2025
2025
  */
2026
2026
  function readDevtoolsVersion() {
2027
- return "0.1.129";
2027
+ return "0.1.131";
2028
2028
  }
2029
2029
  /**
2030
2030
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -2864,32 +2864,50 @@ async function renderAndMaybeWait(deps, prep, waitForAttach, callTimeoutMs, conn
2864
2864
  }, null, 2)}\n\n${qr}`);
2865
2865
  }
2866
2866
  /**
2867
- * Builds a self-contained IIFE DOM expression that renders a dismissible
2868
- * "Debugger Connected" badge on the bottom-left of the phone screen.
2867
+ * Builds a self-contained IIFE DOM expression that renders a LIVE
2868
+ * "Debugger Connected" / "Debugger Disconnected" badge on the bottom-left of
2869
+ * the phone screen (#730).
2869
2870
  *
2870
2871
  * **Pure function** — returns a JS expression string; does NOT inject it.
2871
2872
  * Injection is performed by {@link injectDebugIndicator} in `cell.ts`.
2872
2873
  *
2873
2874
  * The expression, when evaluated on the page, does the following:
2874
- * 1. Early-returns if `#__ait_debug_indicator` already exists (idempotent
2875
- * prevents duplicate badges on re-injection after page reload).
2876
- * 2. Appends a fixed-position `<div id="__ait_debug_indicator">` at the
2877
- * bottom-left, accounting for safe-area insets.
2878
- * 3. Attaches a `{ once: true }` `pointerdown` listener that removes the
2879
- * badge on first touch one-tap dismiss.
2875
+ * 1. Idempotent controller a single `window.__ait_indicator` controller
2876
+ * object is created once (keyed on its presence, not on the DOM node).
2877
+ * Re-injection (e.g. explicit disconnect notice from `close()`) UPDATES
2878
+ * the existing badge's state/label in place — it never duplicates the
2879
+ * `<div>` or stacks a second observer.
2880
+ * 2. Renders a fixed-position `<div id="__ait_debug_indicator">` at the
2881
+ * bottom-left, accounting for safe-area insets. Background/text flip
2882
+ * between the attached (red) and disconnected (grey) visuals.
2883
+ * 3. A `pointerdown` listener dismisses (hides) the badge on tap — dismiss
2884
+ * is NON-terminal: a later state transition un-dismisses it, so a
2885
+ * genuine disconnect after a dismissed tap is still visible.
2886
+ * 4. Observes relay-socket lifecycle WITHOUT opening any new connection:
2887
+ * - Preferred path: if the in-app module (`src/in-app/attach.ts`) has
2888
+ * already installed its relay-WS observer (`window.__ait_relay_ws_observed`),
2889
+ * subscribe to the `ait:relay-ws-state` CustomEvent it broadcasts.
2890
+ * - Fallback path: wrap `window.WebSocket` in a `Proxy` and match dials
2891
+ * by PATHNAME SHAPE only (`/target/`) — never by host/wss value — so a
2892
+ * bare CDP-injected badge (no in-app bundle) still reacts to the
2893
+ * chii target socket's own open/close lifecycle.
2880
2894
  *
2881
2895
  * The expression intentionally contains NO relay URLs, wss addresses, TOTP
2882
- * codes, or any other secrets. It is pure DOM UI text only.
2896
+ * codes, or any other secrets. It is pure DOM UI text + enum state only.
2883
2897
  *
2884
2898
  * SECRET-HANDLING: this expression contains no secrets, relay URLs, wss
2885
- * addresses, or TOTP codes whatsoever — DOM label text only.
2899
+ * addresses, or TOTP codes whatsoever — DOM label text + a structural
2900
+ * pathname match (`/target/`) only.
2886
2901
  *
2887
- * @param opts.label - Badge text (default: `'Debugger Connected'`).
2902
+ * @param opts.label - Attached-state badge text (default: `'Debugger Connected'`).
2903
+ * @param opts.disconnectedLabel - Disconnected-state badge text (default: `'Debugger Disconnected'`).
2904
+ * @param opts.state - Initial/forced state for THIS injection call (default: `'attached'`).
2888
2905
  * @returns A JS expression string suitable for `Runtime.evaluate`.
2889
2906
  */
2890
2907
  function buildIndicatorExpression(opts) {
2891
2908
  const label = opts?.label ?? "Debugger Connected";
2892
- return `(() => {if (document.getElementById('__ait_debug_indicator')) return;const el = document.createElement('div');el.id = '__ait_debug_indicator';el.style.cssText = ['position:fixed','left:max(12px,calc(env(safe-area-inset-left,0px) + 8px))','bottom:max(12px,calc(env(safe-area-inset-bottom,0px) + 8px))','z-index:2147483647','background:#e5484d','color:#fff','font:bold 11px/1 system-ui,sans-serif','padding:5px 9px','border-radius:6px','pointer-events:auto','user-select:none',].join(';');el.textContent = ${JSON.stringify(label)};el.addEventListener('pointerdown', () => el.remove(), { once: true });document.body.appendChild(el);})()`;
2909
+ const disconnectedLabel = opts?.disconnectedLabel ?? "Debugger Disconnected";
2910
+ return `(() => {var W = window;function render(c) {if (c.dismissed) { c.el.style.display = 'none'; return; }c.el.style.display = 'block';var up = c.state !== 'disconnected';c.el.style.background = up ? '#e5484d' : '#8a8f98';c.el.textContent = up ? ${JSON.stringify(label)} : ${JSON.stringify(disconnectedLabel)};}function setState(c, next) { c.state = next; c.dismissed = false; render(c); }var c = W.__ait_indicator;if (!c) {var el = document.createElement('div');el.id = '__ait_debug_indicator';el.style.cssText = ['position:fixed','left:max(12px,calc(env(safe-area-inset-left,0px) + 8px))','bottom:max(12px,calc(env(safe-area-inset-bottom,0px) + 8px))','z-index:2147483647','color:#fff','font:bold 11px/1 system-ui,sans-serif','padding:5px 9px','border-radius:6px','pointer-events:auto','user-select:none',].join(';');document.body.appendChild(el);c = { el: el, state: 'attached', dismissed: false };el.addEventListener('pointerdown', function () { c.dismissed = true; render(c); }, { passive: true });W.__ait_indicator = c;if (W.__ait_relay_ws_observed) {W.addEventListener('ait:relay-ws-state', function (e) {setState(c, e.detail && e.detail.state === 'open' ? 'attached' : 'disconnected');});} else {try {var Native = W.WebSocket;W.WebSocket = new Proxy(Native, {construct: function (t, a) {var ws = Reflect.construct(t, a);try {var u = new URL(String(a[0]), location.href);if (/\\/target\\//.test(u.pathname)) {ws.addEventListener('open', function () { setState(c, 'attached'); });ws.addEventListener('close', function () { setState(c, 'disconnected'); });ws.addEventListener('error', function () { setState(c, 'disconnected'); });}} catch (_) {}return ws;},});} catch (_) {}}}setState(c, ${JSON.stringify(opts?.state ?? "attached")});})()`;
2893
2911
  }
2894
2912
  //#endregion
2895
2913
  //#region src/test-runner/cell.ts
@@ -2935,12 +2953,16 @@ async function injectGlobals(conn, globals) {
2935
2953
  }
2936
2954
  }
2937
2955
  /**
2938
- * Injects the "Debugger Connected" on-phone indicator via `Runtime.evaluate`.
2956
+ * Injects (or updates) the "Debugger Connected"/"Debugger Disconnected"
2957
+ * on-phone indicator via `Runtime.evaluate` (#730).
2939
2958
  *
2940
2959
  * Uses the same CDP mechanism as {@link injectGlobals} — a single
2941
2960
  * `Runtime.evaluate` round-trip with the expression built by
2942
- * {@link buildIndicatorExpression}. The indicator is a dismissible red badge
2943
- * rendered at the bottom-left of the page (position:fixed, safe-area-aware).
2961
+ * {@link buildIndicatorExpression}. The indicator is a LIVE badge rendered at
2962
+ * the bottom-left of the page (position:fixed, safe-area-aware): the badge's
2963
+ * controller is idempotent, so calling this again (e.g. right before
2964
+ * `close()` with `{ state: 'disconnected' }`) UPDATES the existing badge in
2965
+ * place instead of injecting a duplicate.
2944
2966
  *
2945
2967
  * **Isolation**: unlike {@link injectGlobals}, this function NEVER throws to
2946
2968
  * its caller. Injection failure (e.g. page detached during inject, CSS not
@@ -2950,12 +2972,12 @@ async function injectGlobals(conn, globals) {
2950
2972
  * `src/in-app/attach.ts`.
2951
2973
  *
2952
2974
  * Call this ONLY on the manual debug paths (start_attach MCP, devtools-test
2953
- * CLI). Do NOT call on the `run_tests` auto-attach path — the red badge
2975
+ * CLI). Do NOT call on the `run_tests` auto-attach path — the badge
2954
2976
  * would contaminate screenshots, measure_safe_area probes, and DOM snapshots
2955
2977
  * taken during automated measurement runs.
2956
2978
  *
2957
2979
  * SECRET-HANDLING: the injected expression contains no secrets, relay URLs,
2958
- * wss addresses, or TOTP codes — it is pure DOM UI text built by
2980
+ * wss addresses, or TOTP codes — it is pure DOM UI text + enum state built by
2959
2981
  * {@link buildIndicatorExpression}.
2960
2982
  *
2961
2983
  * @param conn - The live CDP connection (relay-attached page).
@@ -3235,7 +3257,11 @@ var ChiiCdpConnection = class {
3235
3257
  * EventEmitter is missed (defensive belt-and-suspenders).
3236
3258
  *
3237
3259
  * @param filterFn - Predicate that the returned targets must satisfy.
3238
- * @param timeoutMs - Reject after this many ms (default 90 000).
3260
+ * @param timeoutMs - Reject after this many ms (default 90 000). Pass a
3261
+ * non-finite value (`Infinity`) to disable the rejection timer entirely —
3262
+ * used by the test-runner's unbounded QR-attach wait (devtools#735). Node
3263
+ * clamps `setTimeout(fn, Infinity)` to ~1ms, so a non-finite value MUST
3264
+ * skip arming the timer rather than pass it through.
3239
3265
  * @param pollIntervalMs - Fallback poll interval (default 500ms).
3240
3266
  */
3241
3267
  waitForFirstTarget(filterFn, timeoutMs = 9e4, pollIntervalMs = 500) {
@@ -3247,7 +3273,7 @@ var ChiiCdpConnection = class {
3247
3273
  const settle = (targets) => {
3248
3274
  if (settled) return;
3249
3275
  settled = true;
3250
- clearTimeout(timeoutHandle);
3276
+ if (timeoutHandle !== null) clearTimeout(timeoutHandle);
3251
3277
  if (pollHandle !== null) {
3252
3278
  clearInterval(pollHandle);
3253
3279
  pollHandle = null;
@@ -3258,7 +3284,7 @@ var ChiiCdpConnection = class {
3258
3284
  const onAttach = (targets) => {
3259
3285
  if (filterFn(targets)) settle(targets);
3260
3286
  };
3261
- const timeoutHandle = setTimeout(() => {
3287
+ const timeoutHandle = Number.isFinite(timeoutMs) ? setTimeout(() => {
3262
3288
  if (settled) return;
3263
3289
  settled = true;
3264
3290
  if (pollHandle !== null) {
@@ -3267,7 +3293,7 @@ var ChiiCdpConnection = class {
3267
3293
  }
3268
3294
  this.emitter.off("target:attached", onAttach);
3269
3295
  reject(/* @__PURE__ */ new Error(`waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`));
3270
- }, timeoutMs);
3296
+ }, timeoutMs) : null;
3271
3297
  this.emitter.on("target:attached", onAttach);
3272
3298
  pollHandle = setInterval(() => {
3273
3299
  this.refreshTargets().then((targets) => {
@@ -4252,9 +4278,9 @@ OPTIONS
4252
4278
  the CLI waits for a human to scan the QR code — use
4253
4279
  --attach-timeout for that.
4254
4280
  --attach-timeout <ms> How long to wait for a human to scan the QR code with
4255
- their phone (default: 600000 10 minutes). Omit to
4256
- use the relay factory's generous default. Decrease for
4257
- CI environments where a scan should arrive quickly.
4281
+ their phone. Omit (default) to wait indefinitely the
4282
+ runner stays up until you stop it (Ctrl-C/SIGTERM).
4283
+ Pass a value to bound the wait for CI/headless runs.
4258
4284
  --cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)
4259
4285
  --cell-platform <plat> Platform to inject as __AIT_CELL__.platform
4260
4286
  (mock|ios|android, default: AIT_CELL_PLATFORM env)
@@ -5397,6 +5423,11 @@ const en = {
5397
5423
  "dashboard.watchdog.title": "Server has shut down",
5398
5424
  "dashboard.watchdog.body": "The MCP server has stopped. This session is no longer active. You may close this tab.",
5399
5425
  "dashboard.watchdog.close": "Close tab",
5426
+ "dashboard.conn.lost": "Connection lost — reconnecting…",
5427
+ "dashboard.session.completeTitle": "Test run complete",
5428
+ "dashboard.session.completeBody": "The runner exited cleanly. See your terminal for results.",
5429
+ "dashboard.session.shutdownTitle": "Debug server stopped",
5430
+ "dashboard.session.shutdownBody": "The MCP server has shut down; this session is no longer live. You can close this tab.",
5400
5431
  "attach.title": "AIT Debug Session — QR Scan",
5401
5432
  "attach.deployment": "deployment: {label}",
5402
5433
  "attach.steps.section": "How to scan",
@@ -5635,6 +5666,11 @@ const tables = {
5635
5666
  "dashboard.watchdog.title": "서버가 종료되었습니다",
5636
5667
  "dashboard.watchdog.body": "MCP server가 종료되어 이 세션이 더 이상 유효하지 않습니다. 이 탭을 닫아도 됩니다.",
5637
5668
  "dashboard.watchdog.close": "탭 닫기",
5669
+ "dashboard.conn.lost": "연결 끊김 — 재연결 시도 중…",
5670
+ "dashboard.session.completeTitle": "테스트 실행 완료",
5671
+ "dashboard.session.completeBody": "러너가 정상 종료되었습니다. 결과는 터미널에서 확인하세요.",
5672
+ "dashboard.session.shutdownTitle": "디버그 서버가 종료되었습니다",
5673
+ "dashboard.session.shutdownBody": "MCP server가 종료되어 이 세션이 더 이상 유효하지 않습니다. 이 탭을 닫아도 됩니다.",
5638
5674
  "attach.title": "AIT 디버그 세션 — QR 스캔",
5639
5675
  "attach.deployment": "deployment: {label}",
5640
5676
  "attach.steps.section": "스캔 절차",
@@ -6216,7 +6252,12 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
6216
6252
  watchdogTimeoutMs,
6217
6253
  watchdogTitle: JSON.stringify(s("dashboard.watchdog.title")),
6218
6254
  watchdogBody: JSON.stringify(s("dashboard.watchdog.body")),
6219
- watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close"))
6255
+ watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close")),
6256
+ connectionLost: JSON.stringify(s("dashboard.conn.lost")),
6257
+ completeTitle: JSON.stringify(s("dashboard.session.completeTitle")),
6258
+ completeBody: JSON.stringify(s("dashboard.session.completeBody")),
6259
+ shutdownTitle: JSON.stringify(s("dashboard.session.shutdownTitle")),
6260
+ shutdownBody: JSON.stringify(s("dashboard.session.shutdownBody"))
6220
6261
  };
6221
6262
  const langSwitcher = buildLangSwitcher(path, params, locale, s);
6222
6263
  const filled = dashboardChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__NOW__", escapeHtml(now)).replaceAll("__TUNNEL_CLASS__", tunnelClass).replaceAll("__TUNNEL_STATUS__", escapeHtml(tunnelStatus)).replaceAll("__ATTACH_SECTION__", attachSection).replaceAll("__INSPECTOR_SECTION__", inspectorSection).replaceAll("__PAGES_SECTION__", pagesSection);
@@ -6273,6 +6314,14 @@ function buildSseScript(strings) {
6273
6314
  var WATCHDOG_BODY = ${strings.watchdogBody};
6274
6315
  var WATCHDOG_CLOSE_LABEL = ${strings.watchdogCloseLabel};
6275
6316
 
6317
+ // ── 실시간 상태 표면 상수 (#730) ───────────────────────────────────────
6318
+ var CONN_LOST = ${strings.connectionLost};
6319
+ var COMPLETE_TITLE = ${strings.completeTitle};
6320
+ var COMPLETE_BODY = ${strings.completeBody};
6321
+ var SHUTDOWN_TITLE = ${strings.shutdownTitle};
6322
+ var SHUTDOWN_BODY = ${strings.shutdownBody};
6323
+ var disconnectedShown = false;
6324
+
6276
6325
  // ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────
6277
6326
  function copyText(text) {
6278
6327
  if (navigator.clipboard && navigator.clipboard.writeText) {
@@ -6350,6 +6399,36 @@ function buildSseScript(strings) {
6350
6399
  }
6351
6400
  }
6352
6401
 
6402
+ // ── Terminal 상태 배너 (#730) ─────────────────────────────────────────
6403
+ // "완료"/"종료"는 watchdog(최대 210s 지연)을 기다리지 않고 close() 직전에
6404
+ // 서버가 직접 push하는 마지막 SSE 프레임으로 즉시 렌더한다. watchdogFired를
6405
+ // 함께 세워 뒤이은 onerror의 showConnLost가 이 배너를 덮어쓰지 않게 한다.
6406
+ function showTerminalBanner(title, body) {
6407
+ if (watchdogTimer) { clearInterval(watchdogTimer); watchdogTimer = null; }
6408
+ watchdogFired = true; // 이후 watchdog/onerror 발화를 억제
6409
+ if (document.body) {
6410
+ document.body.innerHTML =
6411
+ '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;' +
6412
+ 'height:100vh;font-family:sans-serif;text-align:center;padding:2rem;box-sizing:border-box;">' +
6413
+ '<p style="font-size:1.4rem;font-weight:bold;margin-bottom:1rem;">' + title + '</p>' +
6414
+ '<p style="color:#555;">' + body + '</p></div>';
6415
+ }
6416
+ }
6417
+
6418
+ // ── SSE onerror 즉시 신호 (#730) ──────────────────────────────────────
6419
+ // EventSource는 "재시도 중"과 "서버 종료"를 구분하지 못하므로, onerror에서
6420
+ // 즉시 구분되는 '연결 끊김' 상태를 tunnel-status 배지에 표시한다. 이미
6421
+ // terminal 배너가 뜬 뒤(watchdogFired)라면 덮어쓰지 않는다.
6422
+ function showConnLost() {
6423
+ if (disconnectedShown || watchdogFired) return;
6424
+ disconnectedShown = true;
6425
+ var el = document.getElementById('tunnel-status');
6426
+ if (el) { el.textContent = CONN_LOST; el.className = 'status status-down'; }
6427
+ }
6428
+ function clearConnLost() {
6429
+ disconnectedShown = false;
6430
+ }
6431
+
6353
6432
  function fireWatchdog() {
6354
6433
  if (watchdogFired) return;
6355
6434
  watchdogFired = true;
@@ -6380,7 +6459,7 @@ function buildSseScript(strings) {
6380
6459
  src.onopen = function () {
6381
6460
  // 연결 복구 시 lastSeen 갱신 — 일시적 끊김이 watchdog을 발화시키지 않게.
6382
6461
  // watchdog이 이미 발화했으면 갱신하지 않는다(폴백 UI를 그대로 유지).
6383
- if (!watchdogFired) { lastSeen = Date.now(); }
6462
+ if (!watchdogFired) { lastSeen = Date.now(); clearConnLost(); }
6384
6463
  };
6385
6464
  src.onmessage = function (e) {
6386
6465
  // SSE 메시지 수신 — lastSeen 갱신으로 watchdog 리셋.
@@ -6388,6 +6467,13 @@ function buildSseScript(strings) {
6388
6467
  if (!watchdogFired) { lastSeen = Date.now(); }
6389
6468
  try {
6390
6469
  var s = JSON.parse(e.data);
6470
+ // 어떤 프레임이든 성공적으로 수신됐다는 건 서버가 살아있다는 뜻 —
6471
+ // onerror가 세운 '연결 끊김' 표시를 해제한다 (#730).
6472
+ clearConnLost();
6473
+ // phase gate (#730) — 'complete'/'shutdown'은 terminal 상태이므로
6474
+ // 나머지 필드 렌더보다 먼저 처리하고 즉시 반환한다.
6475
+ if (s.phase === 'complete') { showTerminalBanner(COMPLETE_TITLE, COMPLETE_BODY); return; }
6476
+ if (s.phase === 'shutdown') { showTerminalBanner(SHUTDOWN_TITLE, SHUTDOWN_BODY); return; }
6391
6477
  // 터널 상태 갱신
6392
6478
  var el = document.getElementById('tunnel-status');
6393
6479
  if (el) {
@@ -6466,7 +6552,11 @@ function buildSseScript(strings) {
6466
6552
  } catch (_) { /* 파싱 오류 무시 */ }
6467
6553
  };
6468
6554
  src.onerror = function () {
6469
- // 재연결은 EventSource 자동 처리 (spec 기본 동작).
6555
+ // EventSource "재시도 중"과 "서버 종료"를 구분하지 못하므로 즉시
6556
+ // 구분되는 "연결 끊김"을 표시한다(#730). 네이티브 auto-reconnect는 그대로
6557
+ // 진행되며 onopen/onmessage가 성공 즉시 이 상태를 해제한다. watchdog은
6558
+ // 여전히 2차 fallback으로 남는다(최종 폴백 UI 전환).
6559
+ showConnLost();
6470
6560
  };
6471
6561
  })();
6472
6562
  <\/script>`;
@@ -6515,7 +6605,12 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
6515
6605
  watchdogTimeoutMs,
6516
6606
  watchdogTitle: JSON.stringify(s("dashboard.watchdog.title")),
6517
6607
  watchdogBody: JSON.stringify(s("dashboard.watchdog.body")),
6518
- watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close"))
6608
+ watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close")),
6609
+ connectionLost: JSON.stringify(s("dashboard.conn.lost")),
6610
+ completeTitle: JSON.stringify(s("dashboard.session.completeTitle")),
6611
+ completeBody: JSON.stringify(s("dashboard.session.completeBody")),
6612
+ shutdownTitle: JSON.stringify(s("dashboard.session.shutdownTitle")),
6613
+ shutdownBody: JSON.stringify(s("dashboard.session.shutdownBody"))
6519
6614
  });
6520
6615
  return filled.replace("</body>", `${sseScript}\n</body>`);
6521
6616
  }
@@ -6542,7 +6637,8 @@ async function startQrHttpServer(getDashboardState, options) {
6542
6637
  },
6543
6638
  pages: state.pages,
6544
6639
  attachUrl: state.attachUrl,
6545
- inspectorUrl: state.inspectorUrl ?? null
6640
+ inspectorUrl: state.inspectorUrl ?? null,
6641
+ phase: state.phase ?? "active"
6546
6642
  });
6547
6643
  res.write(`data: ${payload}\n\n`);
6548
6644
  }
@@ -6851,7 +6947,7 @@ function createDebugServer(deps) {
6851
6947
  };
6852
6948
  const server = new Server({
6853
6949
  name: "ait-debug",
6854
- version: "0.1.129"
6950
+ version: "0.1.131"
6855
6951
  }, { capabilities: { tools: { listChanged: true } } });
6856
6952
  server.setRequestHandler(ListToolsRequestSchema, () => {
6857
6953
  const conn = router.active;
@@ -7813,6 +7909,7 @@ async function runDebugServer(options = {}) {
7813
7909
  return router.active;
7814
7910
  });
7815
7911
  let lastAttachParts = null;
7912
+ let currentPhase = "active";
7816
7913
  const getDashboardState = () => {
7817
7914
  const targets = router.active.listTargets();
7818
7915
  const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
@@ -7827,7 +7924,8 @@ async function runDebugServer(options = {}) {
7827
7924
  })),
7828
7925
  attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
7829
7926
  inspectorUrl,
7830
- mode: deriveEnvironment(router.active.kind, router.activeRelayOrigin)
7927
+ mode: deriveEnvironment(router.active.kind, router.activeRelayOrigin),
7928
+ phase: currentPhase
7831
7929
  };
7832
7930
  };
7833
7931
  const getDirectInspectorUrl = () => {
@@ -7891,6 +7989,8 @@ async function runDebugServer(options = {}) {
7891
7989
  const shutdown = () => {
7892
7990
  if (closed) return;
7893
7991
  closed = true;
7992
+ currentPhase = "shutdown";
7993
+ qrServer?.notifyStateChange();
7894
7994
  parentWatcher?.stop();
7895
7995
  maxAgeWatchdog?.stop();
7896
7996
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
@@ -8028,6 +8128,7 @@ async function runLocalDebugServer(options = {}) {
8028
8128
  return router.active;
8029
8129
  });
8030
8130
  let lastAttachParts = null;
8131
+ let currentPhase = "active";
8031
8132
  const getDashboardState = () => {
8032
8133
  const targets = router.active.listTargets();
8033
8134
  const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
@@ -8041,7 +8142,8 @@ async function runLocalDebugServer(options = {}) {
8041
8142
  url: t.url
8042
8143
  })),
8043
8144
  attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
8044
- inspectorUrl
8145
+ inspectorUrl,
8146
+ phase: currentPhase
8045
8147
  };
8046
8148
  };
8047
8149
  const getDirectInspectorUrl = () => {
@@ -8105,6 +8207,8 @@ async function runLocalDebugServer(options = {}) {
8105
8207
  const shutdown = () => {
8106
8208
  if (closed) return;
8107
8209
  closed = true;
8210
+ currentPhase = "shutdown";
8211
+ qrServer?.notifyStateChange();
8108
8212
  parentWatcher?.stop();
8109
8213
  maxAgeWatchdog?.stop();
8110
8214
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
@@ -8225,6 +8329,7 @@ async function runMobileDebugServer(options = {}) {
8225
8329
  return router.active;
8226
8330
  });
8227
8331
  let lastAttachParts = null;
8332
+ let currentPhase = "active";
8228
8333
  const getDashboardState = () => {
8229
8334
  const targets = router.active.listTargets();
8230
8335
  const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
@@ -8238,7 +8343,8 @@ async function runMobileDebugServer(options = {}) {
8238
8343
  url: t.url
8239
8344
  })),
8240
8345
  attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
8241
- inspectorUrl
8346
+ inspectorUrl,
8347
+ phase: currentPhase
8242
8348
  };
8243
8349
  };
8244
8350
  const getDirectInspectorUrl = () => {
@@ -8302,6 +8408,8 @@ async function runMobileDebugServer(options = {}) {
8302
8408
  const shutdown = () => {
8303
8409
  if (closed) return;
8304
8410
  closed = true;
8411
+ currentPhase = "shutdown";
8412
+ qrServer?.notifyStateChange();
8305
8413
  parentWatcher?.stop();
8306
8414
  maxAgeWatchdog?.stop();
8307
8415
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
@@ -8823,7 +8931,7 @@ function createDevServer(deps = {}) {
8823
8931
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
8824
8932
  const server = new Server({
8825
8933
  name: "ait-devtools",
8826
- version: "0.1.129"
8934
+ version: "0.1.131"
8827
8935
  }, { capabilities: { tools: {} } });
8828
8936
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
8829
8937
  server.setRequestHandler(CallToolRequestSchema, async (request) => {