@hienlh/ppm 0.9.0-beta.4 → 0.9.0-beta.6

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 (156) hide show
  1. package/CHANGELOG.md +27 -8
  2. package/dist/web/assets/{_basePickBy-COwDPZl_.js → _basePickBy-CZovQgWd.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-DCb0mkTp.js → _baseUniq-ClnvscgW.js} +1 -1
  4. package/dist/web/assets/{api-settings-CuUkz5gb.js → api-settings--eVrUeZM.js} +1 -1
  5. package/dist/web/assets/{arc-D0bJaFyD.js → arc-C2Qaz-ch.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-BVEUkQYB.js → architectureDiagram-2XIMDMQ5-Jq91S_rs.js} +1 -1
  8. package/dist/web/assets/{blockDiagram-WCTKOSBZ-CU2t4NHJ.js → blockDiagram-WCTKOSBZ-CKGufRTy.js} +1 -1
  9. package/dist/web/assets/browser-tab-DAvH4mv0.js +1 -0
  10. package/dist/web/assets/{c4Diagram-IC4MRINW-DzjR91sM.js → c4Diagram-IC4MRINW-BNP2L9r_.js} +1 -1
  11. package/dist/web/assets/channel-w7yboq56.js +1 -0
  12. package/dist/web/assets/chat-tab-WEBXxGgN.js +7 -0
  13. package/dist/web/assets/{chunk-4BX2VUAB-0YMkpW2S.js → chunk-4BX2VUAB-BptTlTyl.js} +1 -1
  14. package/dist/web/assets/{chunk-55IACEB6-Dp0pTM5r.js → chunk-55IACEB6-C4mUdyio.js} +1 -1
  15. package/dist/web/assets/{chunk-7E7YKBS2-CuYKSUgJ.js → chunk-7E7YKBS2-6xAQfBwa.js} +1 -1
  16. package/dist/web/assets/{chunk-7R4GIKGN-DvbvLUIN.js → chunk-7R4GIKGN-DXaGAn_K.js} +2 -2
  17. package/dist/web/assets/{chunk-C72U2L5F-CcEW1AMZ.js → chunk-C72U2L5F-DOtEiN5f.js} +1 -1
  18. package/dist/web/assets/{chunk-EGIJ26TM-Cgt-qg75.js → chunk-EGIJ26TM-D0KJTa_T.js} +1 -1
  19. package/dist/web/assets/{chunk-FMBD7UC4-JCLgVcaC.js → chunk-FMBD7UC4-C_1aG0eb.js} +1 -1
  20. package/dist/web/assets/{chunk-GEFDOKGD-B82RP9ow.js → chunk-GEFDOKGD-DwVPiYfW.js} +1 -1
  21. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +2 -0
  22. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +1 -0
  23. package/dist/web/assets/{chunk-JSJVCQXG-Pb-JMOgO.js → chunk-JSJVCQXG-BSrqCL_3.js} +1 -1
  24. package/dist/web/assets/{chunk-KX2RTZJC-BRj-ZEvL.js → chunk-KX2RTZJC-BCxGmbzy.js} +1 -1
  25. package/dist/web/assets/{chunk-KYZI473N-CBRPKraG.js → chunk-KYZI473N-BKO5gMeU.js} +1 -1
  26. package/dist/web/assets/{chunk-L3YUKLVL-DNFj84V6.js → chunk-L3YUKLVL-3wBgkSvL.js} +1 -1
  27. package/dist/web/assets/{chunk-MX3YWQON-BnPzQK-O.js → chunk-MX3YWQON-BgjSEzus.js} +1 -1
  28. package/dist/web/assets/{chunk-NQ4KR5QH-BRj25yO7.js → chunk-NQ4KR5QH-DLrZwBEm.js} +1 -1
  29. package/dist/web/assets/{chunk-O4XLMI2P-BdXwVXjJ.js → chunk-O4XLMI2P-BurQy8tt.js} +1 -1
  30. package/dist/web/assets/{chunk-OZEHJAEY-LfXT4p8B.js → chunk-OZEHJAEY-YTn24bGg.js} +1 -1
  31. package/dist/web/assets/{chunk-PQ6SQG4A-EdgQyTqa.js → chunk-PQ6SQG4A-BxtUGYhW.js} +1 -1
  32. package/dist/web/assets/{chunk-PU5JKC2W-D3thuSok.js → chunk-PU5JKC2W-B66ELkQm.js} +1 -1
  33. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +1 -0
  34. package/dist/web/assets/{chunk-R5LLSJPH-LdG7RqsM.js → chunk-R5LLSJPH-euR2RxLN.js} +1 -1
  35. package/dist/web/assets/{chunk-WL4C6EOR-BHFnnXOt.js → chunk-WL4C6EOR-_2CBOJdI.js} +1 -1
  36. package/dist/web/assets/{chunk-XIRO2GV7-DUmQrLsF.js → chunk-XIRO2GV7-kqQ0g6wW.js} +1 -1
  37. package/dist/web/assets/{chunk-XPW4576I-CsGTseUr.js → chunk-XPW4576I-CtcaMb09.js} +1 -1
  38. package/dist/web/assets/{chunk-XZSTWKYB-5W2emiq4.js → chunk-XZSTWKYB-BYxFzZwS.js} +1 -1
  39. package/dist/web/assets/{chunk-YBOYWFTD-COdZIaX4.js → chunk-YBOYWFTD-Dx_fX35n.js} +1 -1
  40. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +1 -0
  41. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +1 -0
  42. package/dist/web/assets/clone-BSi6cgDh.js +1 -0
  43. package/dist/web/assets/{code-editor-BRMOypkX.js → code-editor-B5sg_uJQ.js} +1 -1
  44. package/dist/web/assets/{cose-bilkent-S5V4N54A-C1QJ6GPW.js → cose-bilkent-S5V4N54A-CHHjH2dV.js} +1 -1
  45. package/dist/web/assets/{dagre-CWo8w9wK.js → dagre-CNtSxiE_.js} +1 -1
  46. package/dist/web/assets/{dagre-KLK3FWXG-Br4t5TRV.js → dagre-KLK3FWXG-ChenfPp1.js} +1 -1
  47. package/dist/web/assets/database-viewer-CwtyWCkE.js +1 -0
  48. package/dist/web/assets/{diagram-E7M64L7V-CkDC2uAj.js → diagram-E7M64L7V-CzKYZM0Y.js} +1 -1
  49. package/dist/web/assets/{diagram-IFDJBPK2-NvhckwcA.js → diagram-IFDJBPK2-ChB_paPo.js} +1 -1
  50. package/dist/web/assets/{diagram-P4PSJMXO--nUaNiyB.js → diagram-P4PSJMXO-D1eW1dkL.js} +1 -1
  51. package/dist/web/assets/{diff-viewer-jDU2bcGj.js → diff-viewer-CzE5M-Wd.js} +1 -1
  52. package/dist/web/assets/{erDiagram-INFDFZHY-DK4QEZYh.js → erDiagram-INFDFZHY-mCvUFSn6.js} +1 -1
  53. package/dist/web/assets/{flowDiagram-PKNHOUZH-B9h_Ba-v.js → flowDiagram-PKNHOUZH-14ohZ1M1.js} +1 -1
  54. package/dist/web/assets/{ganttDiagram-A5KZAMGK-BVlftqyZ.js → ganttDiagram-A5KZAMGK-DIX0pLbk.js} +1 -1
  55. package/dist/web/assets/git-graph-6yxCeeN9.js +1 -0
  56. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +1 -0
  57. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js → gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js} +1 -1
  58. package/dist/web/assets/{graphlib-BbbiUImY.js → graphlib-DhOZxqsh.js} +1 -1
  59. package/dist/web/assets/index-DE8b9u8F.css +2 -0
  60. package/dist/web/assets/index-wuWZBO9y.js +37 -0
  61. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +1 -0
  62. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +2 -0
  63. package/dist/web/assets/input-Brjz2Vv-.js +41 -0
  64. package/dist/web/assets/{isEmpty-DXomfd7J.js → isEmpty-C0YYdhYj.js} +1 -1
  65. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-cW7SMLa_.js → ishikawaDiagram-PHBUUO56-olazD6dZ.js} +1 -1
  66. package/dist/web/assets/{journeyDiagram-4ABVD52K-DFQXUZsc.js → journeyDiagram-4ABVD52K-CttDH9bb.js} +1 -1
  67. package/dist/web/assets/{kanban-definition-K7BYSVSG-BMUhjxqj.js → kanban-definition-K7BYSVSG-BBXbI37U.js} +1 -1
  68. package/dist/web/assets/keybindings-store-mkBHnWN1.js +1 -0
  69. package/dist/web/assets/{line--xyfYP3x.js → line-DBLLF7lH.js} +1 -1
  70. package/dist/web/assets/{linear-BdqW7iQu.js → linear-BLFWatDe.js} +1 -1
  71. package/dist/web/assets/{markdown-renderer-BCjJbGP8.js → markdown-renderer-CxWxvrzT.js} +5 -5
  72. package/dist/web/assets/{mermaid-parser.core-BY8JfkE_.js → mermaid-parser.core-BKiGOTjR.js} +2 -2
  73. package/dist/web/assets/{mindmap-definition-YRQLILUH-DIv-LMXG.js → mindmap-definition-YRQLILUH-DoT7m4Sz.js} +1 -1
  74. package/dist/web/assets/{ordinal-CIoJK3nc.js → ordinal-CCj7PWgZ.js} +1 -1
  75. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +1 -0
  76. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +1 -0
  77. package/dist/web/assets/{pieDiagram-SKSYHLDU-seSK40d1.js → pieDiagram-SKSYHLDU-Bkh2E4zE.js} +1 -1
  78. package/dist/web/assets/postgres-viewer-UP3yv9Yh.js +1 -0
  79. package/dist/web/assets/{quadrantDiagram-337W2JSQ-BaRFqlsA.js → quadrantDiagram-337W2JSQ-B7zgALOL.js} +1 -1
  80. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +1 -0
  81. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-1WWjMQB_.js → requirementDiagram-Z7DCOOCP-D_5GXNRo.js} +1 -1
  82. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DEGGYsk7.js → sankeyDiagram-WA2Y5GQK-BA9EFAAe.js} +1 -1
  83. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-BtRvoUTC.js → sequenceDiagram-2WXFIKYE-fyWIrHiG.js} +1 -1
  84. package/dist/web/assets/{settings-store-D3dJqGhB.js → settings-store-Bbhg_ptG.js} +2 -2
  85. package/dist/web/assets/settings-tab-BoBXlVHe.js +1 -0
  86. package/dist/web/assets/sqlite-viewer-lzRVvM5j.js +1 -0
  87. package/dist/web/assets/{stateDiagram-RAJIS63D-C16aO8tn.js → stateDiagram-RAJIS63D-DfRBcaBu.js} +1 -1
  88. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +1 -0
  89. package/dist/web/assets/{tab-store-DSz5PQI0.js → tab-store-DcIBZTD4.js} +1 -1
  90. package/dist/web/assets/{terminal-tab-MRg8y1xF.js → terminal-tab-CAZtLK6i.js} +2 -2
  91. package/dist/web/assets/{timeline-definition-YZTLITO2-DrjxCpEM.js → timeline-definition-YZTLITO2-DYfwJ1jM.js} +1 -1
  92. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +1 -0
  93. package/dist/web/assets/{use-monaco-theme-BQzvItNE.js → use-monaco-theme-vwto-Vlf.js} +1 -1
  94. package/dist/web/assets/{vennDiagram-LZ73GAT5-DfYFnniI.js → vennDiagram-LZ73GAT5-DqbKNRD9.js} +1 -1
  95. package/dist/web/assets/{xychartDiagram-JWTSCODW-BRvXOVlG.js → xychartDiagram-JWTSCODW-DhUL86qT.js} +1 -1
  96. package/dist/web/index.html +10 -12
  97. package/dist/web/sw.js +1 -1
  98. package/package.json +1 -1
  99. package/src/providers/claude-agent-sdk.ts +212 -76
  100. package/src/server/index.ts +4 -1
  101. package/src/server/routes/browser-preview.ts +135 -65
  102. package/src/server/ws/chat.ts +103 -73
  103. package/src/types/api.ts +1 -1
  104. package/src/types/chat.ts +2 -0
  105. package/src/web/components/browser/browser-tab.tsx +105 -224
  106. package/src/web/components/chat/chat-tab.tsx +3 -3
  107. package/src/web/components/chat/message-input.tsx +42 -4
  108. package/src/web/hooks/use-chat.ts +21 -9
  109. package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +0 -1
  110. package/dist/web/assets/arrow-left-C_j9Ki73.js +0 -1
  111. package/dist/web/assets/browser-tab-BhTdeeZd.js +0 -1
  112. package/dist/web/assets/channel-CKNZAqoN.js +0 -1
  113. package/dist/web/assets/chat-tab-ZiiUVOxM.js +0 -7
  114. package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +0 -2
  115. package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +0 -1
  116. package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +0 -1
  117. package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +0 -1
  118. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +0 -1
  119. package/dist/web/assets/clone-DNDy9Sms.js +0 -1
  120. package/dist/web/assets/database-viewer-CEoDpzPz.js +0 -1
  121. package/dist/web/assets/git-graph-DMQzw4Sp.js +0 -1
  122. package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +0 -1
  123. package/dist/web/assets/index-B4Iz1Wbi.css +0 -2
  124. package/dist/web/assets/index-QiSWS6f-.js +0 -37
  125. package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +0 -1
  126. package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +0 -2
  127. package/dist/web/assets/input-DGlv6gt_.js +0 -41
  128. package/dist/web/assets/keybindings-store-BplH-yiN.js +0 -1
  129. package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +0 -1
  130. package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +0 -1
  131. package/dist/web/assets/postgres-viewer-s0snZ9CL.js +0 -1
  132. package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +0 -1
  133. package/dist/web/assets/settings-tab-2YkgmrY0.js +0 -1
  134. package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +0 -1
  135. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +0 -1
  136. package/dist/web/assets/switch-mjGtIVDJ.js +0 -1
  137. package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +0 -1
  138. /package/dist/web/assets/{api-client-icCZ-07C.js → api-client-DpGMOZNf.js} +0 -0
  139. /package/dist/web/assets/{array-CLwNaqU1.js → array-BGFCBI0e.js} +0 -0
  140. /package/dist/web/assets/{columns-2-Bcg3QJBg.js → columns-2-ChOTgl3e.js} +0 -0
  141. /package/dist/web/assets/{cytoscape.esm-B-QQuWwK.js → cytoscape.esm-Ccan6xou.js} +0 -0
  142. /package/dist/web/assets/{defaultLocale-D_VMtRaY.js → defaultLocale-CRZydyG6.js} +0 -0
  143. /package/dist/web/assets/{dist-Ckxnw5rl.js → dist-Cce3efmT.js} +0 -0
  144. /package/dist/web/assets/{dist-CMmNEgEP.js → dist-T0Vhi0Mh.js} +0 -0
  145. /package/dist/web/assets/{init-vVpfz1D6.js → init-B8gtcn7T.js} +0 -0
  146. /package/dist/web/assets/{isArrayLikeObject-DvHDmeBe.js → isArrayLikeObject-B4pdpV8V.js} +0 -0
  147. /package/dist/web/assets/{katex-C3cZrCvP.js → katex-Bbu770d9.js} +0 -0
  148. /package/dist/web/assets/{math-a44lmFDa.js → math-DwgHI-Cu.js} +0 -0
  149. /package/dist/web/assets/{path-CuyvWNAH.js → path-DZF-JdEe.js} +0 -0
  150. /package/dist/web/assets/{preload-helper-CsoeaaUJ.js → preload-helper-qlgyTAkD.js} +0 -0
  151. /package/dist/web/assets/{react-BPIfZRKM.js → react-BGf7KNLk.js} +0 -0
  152. /package/dist/web/assets/{rough.esm-c4PR5shF.js → rough.esm-VLpapkIG.js} +0 -0
  153. /package/dist/web/assets/{src-CLWraeNW.js → src-BoSBNdA_.js} +0 -0
  154. /package/dist/web/assets/{table-C9jDaRl2.js → table-Yo02WRH-.js} +0 -0
  155. /package/dist/web/assets/{tag-CENGyt_L.js → tag-CaC1ng2E.js} +0 -0
  156. /package/dist/web/assets/{utils-Bslrbb-G.js → utils-btZ8C8-R.js} +0 -0
@@ -1,77 +1,7 @@
1
- import { useState, useRef, useCallback, useEffect } from "react";
2
- import {
3
- ArrowLeft,
4
- ArrowRight,
5
- RotateCcw,
6
- ExternalLink,
7
- Globe,
8
- } from "lucide-react";
1
+ import { useState, useRef, useCallback } from "react";
2
+ import { ExternalLink, Globe, Loader2, RefreshCw, X } from "lucide-react";
9
3
  import { useTabStore } from "@/stores/tab-store";
10
-
11
- /** Parse a URL string — returns normalized URL or null if invalid */
12
- function parseUrl(input: string): string | null {
13
- let url = input.trim();
14
- if (!url) return null;
15
-
16
- // If just a port number, treat as localhost
17
- if (/^\d+$/.test(url)) return `http://localhost:${url}`;
18
-
19
- // If host:port without scheme, add http://
20
- if (/^localhost(:\d+)?/.test(url)) url = `http://${url}`;
21
- if (/^[\w.-]+:\d+/.test(url) && !url.includes("://")) url = `http://${url}`;
22
-
23
- // If no scheme at all, add https:// for external, http:// for localhost
24
- if (!url.includes("://")) {
25
- url = url.includes("localhost") ? `http://${url}` : `https://${url}`;
26
- }
27
-
28
- try {
29
- new URL(url);
30
- return url;
31
- } catch {
32
- return null;
33
- }
34
- }
35
-
36
- /** Check if a URL is a localhost address */
37
- function isLocalhost(url: string): boolean {
38
- try {
39
- const u = new URL(url);
40
- return (
41
- u.hostname === "localhost" ||
42
- u.hostname === "127.0.0.1" ||
43
- u.hostname === "0.0.0.0" ||
44
- u.hostname === "::1"
45
- );
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
- /** Convert URL to iframe src — proxy localhost through backend */
52
- function toIframeSrc(url: string): string {
53
- if (!isLocalhost(url)) return url;
54
-
55
- try {
56
- const u = new URL(url);
57
- const port = u.port || "80";
58
- const path = u.pathname + u.search + u.hash;
59
- return `/api/preview/${port}${path}`;
60
- } catch {
61
- return url;
62
- }
63
- }
64
-
65
- /** Extract display URL from iframe src (reverse of toIframeSrc) */
66
- function fromIframeSrc(src: string): string {
67
- const match = src.match(/^\/api\/preview\/(\d+)(\/.*)?$/);
68
- if (match) {
69
- const port = match[1];
70
- const path = match[2] || "/";
71
- return `http://localhost:${port}${path}`;
72
- }
73
- return src;
74
- }
4
+ import { api } from "@/lib/api-client";
75
5
 
76
6
  interface BrowserTabProps {
77
7
  metadata?: Record<string, unknown>;
@@ -79,188 +9,139 @@ interface BrowserTabProps {
79
9
  }
80
10
 
81
11
  export function BrowserTab({ metadata, tabId }: BrowserTabProps) {
82
- const initialUrl = (metadata?.url as string) || "http://localhost:3000";
83
- const [addressBar, setAddressBar] = useState(initialUrl);
84
- const [currentUrl, setCurrentUrl] = useState(initialUrl);
85
- const [iframeSrc, setIframeSrc] = useState(toIframeSrc(initialUrl));
86
- const [canGoBack, setCanGoBack] = useState(false);
87
- const [canGoForward, setCanGoForward] = useState(false);
88
- const [loading, setLoading] = useState(true);
12
+ const initialPort = (metadata?.port as number) || 0;
13
+ const [portInput, setPortInput] = useState(initialPort ? String(initialPort) : "");
14
+ const [tunnelUrl, setTunnelUrl] = useState<string | null>(null);
15
+ const [loading, setLoading] = useState(false);
89
16
  const [error, setError] = useState<string | null>(null);
90
17
  const iframeRef = useRef<HTMLIFrameElement>(null);
91
18
  const updateTab = useTabStore((s) => s.updateTab);
92
19
 
93
- // Navigation history (iframe same-origin only)
94
- const historyRef = useRef<string[]>([initialUrl]);
95
- const historyIdxRef = useRef(0);
96
-
97
- const navigate = useCallback(
98
- (url: string, addToHistory = true) => {
99
- const parsed = parseUrl(url);
100
- if (!parsed) {
101
- setError("Invalid URL");
102
- return;
103
- }
104
-
105
- setError(null);
106
- setCurrentUrl(parsed);
107
- setAddressBar(parsed);
108
- setIframeSrc(toIframeSrc(parsed));
109
- setLoading(true);
110
-
111
- if (addToHistory) {
112
- const h = historyRef.current;
113
- const idx = historyIdxRef.current;
114
- // Truncate forward history
115
- historyRef.current = h.slice(0, idx + 1);
116
- historyRef.current.push(parsed);
117
- historyIdxRef.current = historyRef.current.length - 1;
118
- }
119
-
120
- setCanGoBack(historyIdxRef.current > 0);
121
- setCanGoForward(
122
- historyIdxRef.current < historyRef.current.length - 1,
123
- );
124
-
125
- // Update tab title
126
- if (tabId) {
127
- try {
128
- const u = new URL(parsed);
129
- const title = isLocalhost(parsed)
130
- ? `localhost:${u.port || "80"}`
131
- : u.hostname;
132
- updateTab(tabId, { title });
133
- } catch {}
134
- }
135
- },
136
- [tabId, updateTab],
137
- );
138
-
139
- const goBack = useCallback(() => {
140
- if (historyIdxRef.current > 0) {
141
- historyIdxRef.current--;
142
- navigate(historyRef.current[historyIdxRef.current]!, false);
143
- }
144
- }, [navigate]);
145
-
146
- const goForward = useCallback(() => {
147
- if (historyIdxRef.current < historyRef.current.length - 1) {
148
- historyIdxRef.current++;
149
- navigate(historyRef.current[historyIdxRef.current]!, false);
150
- }
151
- }, [navigate]);
152
-
153
- const reload = useCallback(() => {
20
+ const startTunnel = useCallback(async (port: number) => {
154
21
  setLoading(true);
155
22
  setError(null);
23
+ setTunnelUrl(null);
24
+
25
+ try {
26
+ const res = await api.post<{ port: number; url: string }>("/api/preview/tunnel", { port });
27
+ setTunnelUrl(res.url);
28
+ if (tabId) updateTab(tabId, { title: `localhost:${port}`, metadata: { ...metadata, port } });
29
+ } catch (e: any) {
30
+ setError(e.message || `Failed to start tunnel for port ${port}`);
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ }, [tabId, metadata, updateTab]);
35
+
36
+ const stopTunnel = useCallback(async () => {
37
+ const port = parseInt(portInput, 10);
38
+ if (!port) return;
39
+ try { await api.del(`/api/preview/tunnel/${port}`); } catch {}
40
+ setTunnelUrl(null);
41
+ if (tabId) updateTab(tabId, { title: "Browser" });
42
+ }, [portInput, tabId, updateTab]);
43
+
44
+ const handleSubmit = (e: React.FormEvent) => {
45
+ e.preventDefault();
46
+ const port = parseInt(portInput, 10);
47
+ if (port >= 1 && port <= 65535) startTunnel(port);
48
+ else setError("Port must be 1-65535");
49
+ };
50
+
51
+ const reload = () => {
156
52
  if (iframeRef.current) {
157
- // Force reload by re-setting src
158
53
  const src = iframeRef.current.src;
159
54
  iframeRef.current.src = "";
160
- requestAnimationFrame(() => {
161
- if (iframeRef.current) iframeRef.current.src = src;
162
- });
163
- }
164
- }, []);
165
-
166
- const openExternal = useCallback(() => {
167
- window.open(currentUrl, "_blank");
168
- }, [currentUrl]);
169
-
170
- const handleAddressKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
171
- if (e.key === "Enter") {
172
- e.preventDefault();
173
- navigate(addressBar);
55
+ requestAnimationFrame(() => { if (iframeRef.current) iframeRef.current.src = src; });
174
56
  }
175
57
  };
176
58
 
177
- // Navigate when metadata.url changes (e.g. opened from command palette)
178
- useEffect(() => {
179
- const metaUrl = metadata?.url as string | undefined;
180
- if (metaUrl && metaUrl !== currentUrl) {
181
- navigate(metaUrl);
182
- }
183
- }, [metadata?.url]);
59
+ // No tunnel yet show port input
60
+ if (!tunnelUrl) {
61
+ return (
62
+ <div className="flex flex-col items-center justify-center h-full gap-4 p-6">
63
+ <Globe className="size-12 text-text-subtle" />
64
+ <h2 className="text-lg font-medium text-text-primary">Open Localhost</h2>
65
+ <p className="text-sm text-text-secondary text-center max-w-sm">
66
+ Enter the port of your local dev server to preview it here.
67
+ </p>
68
+ <form onSubmit={handleSubmit} className="flex items-center gap-2 w-full max-w-xs">
69
+ <div className="flex-1 flex items-center gap-2 px-3 py-2.5 rounded-lg bg-surface border border-border focus-within:border-accent/50 transition-colors">
70
+ <span className="text-sm text-text-subtle shrink-0">localhost:</span>
71
+ <input
72
+ type="number"
73
+ value={portInput}
74
+ onChange={(e) => setPortInput(e.target.value)}
75
+ placeholder="3000"
76
+ min={1}
77
+ max={65535}
78
+ autoFocus
79
+ className="flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-subtle min-w-0 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
80
+ />
81
+ </div>
82
+ <button
83
+ type="submit"
84
+ disabled={loading || !portInput}
85
+ className="px-4 py-2.5 rounded-lg bg-accent text-white text-sm font-medium hover:bg-accent/90 disabled:opacity-50 transition-colors shrink-0"
86
+ >
87
+ {loading ? <Loader2 className="size-4 animate-spin" /> : "Open"}
88
+ </button>
89
+ </form>
90
+ {error && <p className="text-sm text-red-400">{error}</p>}
91
+ {loading && (
92
+ <div className="flex items-center gap-2 text-sm text-text-secondary">
93
+ <Loader2 className="size-4 animate-spin" />
94
+ <span>Starting tunnel... (may take a few seconds)</span>
95
+ </div>
96
+ )}
97
+ </div>
98
+ );
99
+ }
184
100
 
101
+ // Tunnel active — show iframe
185
102
  return (
186
103
  <div className="flex flex-col h-full w-full bg-background">
187
104
  {/* Toolbar */}
188
- <div className="flex items-center gap-1 px-2 py-1.5 border-b border-border bg-surface shrink-0">
189
- {/* Nav buttons */}
190
- <button
191
- onClick={goBack}
192
- disabled={!canGoBack}
193
- className="p-1.5 rounded hover:bg-surface-elevated disabled:opacity-30 transition-colors"
194
- title="Back"
195
- >
196
- <ArrowLeft className="size-4" />
197
- </button>
198
- <button
199
- onClick={goForward}
200
- disabled={!canGoForward}
201
- className="p-1.5 rounded hover:bg-surface-elevated disabled:opacity-30 transition-colors"
202
- title="Forward"
203
- >
204
- <ArrowRight className="size-4" />
205
- </button>
105
+ <div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border bg-surface shrink-0">
106
+ <Globe className="size-4 text-text-subtle shrink-0" />
107
+ <span className="text-xs text-text-primary font-medium">localhost:{portInput}</span>
108
+ <span className="text-xs text-text-subtle truncate ml-1">({tunnelUrl})</span>
109
+ <div className="flex-1" />
206
110
  <button
207
111
  onClick={reload}
208
112
  className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
209
113
  title="Reload"
210
114
  >
211
- <RotateCcw className={`size-4 ${loading ? "animate-spin" : ""}`} />
115
+ <RefreshCw className="size-3.5" />
212
116
  </button>
213
-
214
- {/* Address bar */}
215
- <div className="flex-1 flex items-center gap-2 mx-1 px-2.5 py-1.5 rounded-md bg-background border border-border focus-within:border-accent/50 transition-colors">
216
- <Globe className="size-3.5 text-text-subtle shrink-0" />
217
- <input
218
- type="text"
219
- value={addressBar}
220
- onChange={(e) => setAddressBar(e.target.value)}
221
- onKeyDown={handleAddressKeyDown}
222
- placeholder="Enter URL or port (e.g. 3000, localhost:8080)"
223
- className="flex-1 bg-transparent text-xs text-text-primary outline-none placeholder:text-text-subtle min-w-0"
224
- />
225
- </div>
226
-
227
- {/* Open external */}
228
117
  <button
229
- onClick={openExternal}
118
+ onClick={() => window.open(tunnelUrl, "_blank")}
230
119
  className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
231
120
  title="Open in browser"
232
121
  >
233
- <ExternalLink className="size-4" />
122
+ <ExternalLink className="size-3.5" />
123
+ </button>
124
+ <button
125
+ onClick={stopTunnel}
126
+ className="p-1.5 rounded hover:bg-surface-elevated text-red-400 transition-colors"
127
+ title="Stop tunnel"
128
+ >
129
+ <X className="size-3.5" />
234
130
  </button>
235
131
  </div>
236
132
 
237
- {/* Content */}
133
+ {/* iframe */}
238
134
  <div className="flex-1 relative min-h-0">
239
- {error ? (
240
- <div className="flex items-center justify-center h-full text-text-secondary text-sm">
241
- <p>{error}</p>
242
- </div>
243
- ) : (
244
- <iframe
245
- ref={iframeRef}
246
- src={iframeSrc}
247
- className="w-full h-full border-0"
248
- sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-modals"
249
- onLoad={() => setLoading(false)}
250
- onError={() => {
251
- setLoading(false);
252
- setError(`Failed to load ${currentUrl}`);
253
- }}
254
- />
255
- )}
256
-
257
- {/* Loading overlay */}
258
- {loading && !error && (
135
+ <iframe
136
+ ref={iframeRef}
137
+ src={tunnelUrl}
138
+ className="w-full h-full border-0"
139
+ sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-modals"
140
+ onLoad={() => setLoading(false)}
141
+ />
142
+ {loading && (
259
143
  <div className="absolute inset-0 flex items-center justify-center bg-background/50">
260
- <div className="flex items-center gap-2 text-sm text-text-secondary">
261
- <RotateCcw className="size-4 animate-spin" />
262
- <span>Loading...</span>
263
- </div>
144
+ <Loader2 className="size-5 animate-spin text-text-secondary" />
264
145
  </div>
265
146
  )}
266
147
  </div>
@@ -10,7 +10,7 @@ import { useNotificationStore } from "@/stores/notification-store";
10
10
  import { openBugReportPopup } from "@/lib/report-bug";
11
11
  import { getAISettings } from "@/lib/api-settings";
12
12
  import { MessageList } from "./message-list";
13
- import { MessageInput, type ChatAttachment } from "./message-input";
13
+ import { MessageInput, type ChatAttachment, type MessagePriority } from "./message-input";
14
14
  import { SlashCommandPicker, type SlashItem } from "./slash-command-picker";
15
15
  import { FilePicker } from "./file-picker";
16
16
  import { ChatHistoryBar } from "./chat-history-bar";
@@ -205,7 +205,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
205
205
  );
206
206
 
207
207
  const handleSend = useCallback(
208
- async (content: string, attachments: ChatAttachment[] = []) => {
208
+ async (content: string, attachments: ChatAttachment[] = [], priority?: MessagePriority) => {
209
209
  const fullContent = buildMessageWithAttachments(content, attachments);
210
210
  if (!fullContent.trim()) return;
211
211
 
@@ -227,7 +227,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
227
227
  return;
228
228
  }
229
229
  }
230
- sendMessage(fullContent, { permissionMode });
230
+ sendMessage(fullContent, { permissionMode, priority });
231
231
  },
232
232
  [sessionId, providerId, projectName, sendMessage, buildMessageWithAttachments, permissionMode],
233
233
  );
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useCallback, useEffect, memo, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react";
2
- import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff } from "lucide-react";
2
+ import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff, Zap, ListOrdered, Clock } from "lucide-react";
3
3
  import { useVoiceInput } from "@/hooks/use-voice-input";
4
4
  import { api, projectUrl, getAuthToken } from "@/lib/api-client";
5
5
  import { randomId } from "@/lib/utils";
@@ -22,8 +22,10 @@ export interface ChatAttachment {
22
22
  status: "uploading" | "ready" | "error";
23
23
  }
24
24
 
25
+ export type MessagePriority = 'now' | 'next' | 'later';
26
+
25
27
  interface MessageInputProps {
26
- onSend: (content: string, attachments: ChatAttachment[]) => void;
28
+ onSend: (content: string, attachments: ChatAttachment[], priority?: MessagePriority) => void;
27
29
  isStreaming?: boolean;
28
30
  onCancel?: () => void;
29
31
  disabled?: boolean;
@@ -76,6 +78,7 @@ export const MessageInput = memo(function MessageInput({
76
78
  const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
77
79
  const [modeSelectorOpen, setModeSelectorOpen] = useState(false);
78
80
  const [pendingSend, setPendingSend] = useState(false);
81
+ const [priority, setPriority] = useState<MessagePriority>('next');
79
82
  const textareaRef = useRef<HTMLTextAreaElement>(null);
80
83
  const mobileTextareaRef = useRef<HTMLTextAreaElement>(null);
81
84
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -329,7 +332,8 @@ export const MessageInput = memo(function MessageInput({
329
332
 
330
333
  onSlashStateChange?.(false, "");
331
334
  onFileStateChange?.(false, "");
332
- onSend(trimmed, readyAttachments);
335
+ if (voice.isListening) voice.stop();
336
+ onSend(trimmed, readyAttachments, isStreaming ? priority : undefined);
333
337
  setValue("");
334
338
  // Revoke preview URLs
335
339
  for (const att of attachments) {
@@ -337,9 +341,10 @@ export const MessageInput = memo(function MessageInput({
337
341
  }
338
342
  setAttachments([]);
339
343
  setPendingSend(false);
344
+ setPriority('next');
340
345
  if (textareaRef.current) textareaRef.current.style.height = "auto";
341
346
  if (mobileTextareaRef.current) mobileTextareaRef.current.style.height = "auto";
342
- }, [value, attachments, onSend, onSlashStateChange, onFileStateChange]);
347
+ }, [value, attachments, onSend, onSlashStateChange, onFileStateChange, isStreaming, priority]);
343
348
 
344
349
  const handleSend = useCallback(() => {
345
350
  if (disabled) return;
@@ -514,6 +519,7 @@ export const MessageInput = memo(function MessageInput({
514
519
  projectName={projectName}
515
520
  />
516
521
  )}
522
+ {isStreaming && <PriorityToggle value={priority} onChange={setPriority} />}
517
523
  </div>
518
524
  {/* Mobile: single row — attach + mic + textarea + send */}
519
525
  <div className="flex items-end gap-1 md:hidden px-2 py-2">
@@ -636,6 +642,7 @@ export const MessageInput = memo(function MessageInput({
636
642
  projectName={projectName}
637
643
  />
638
644
  )}
645
+ {isStreaming && <PriorityToggle value={priority} onChange={setPriority} />}
639
646
  </div>
640
647
  <div className="flex items-center gap-1">
641
648
  {showCancel ? (
@@ -682,3 +689,34 @@ function ModeChip({ mode, onClick }: { mode: string; onClick: () => void }) {
682
689
  </button>
683
690
  );
684
691
  }
692
+
693
+ const PRIORITY_OPTIONS: { value: MessagePriority; label: string; Icon: typeof Zap }[] = [
694
+ { value: 'now', label: 'Interrupt', Icon: Zap },
695
+ { value: 'next', label: 'Queue', Icon: ListOrdered },
696
+ { value: 'later', label: 'Later', Icon: Clock },
697
+ ];
698
+
699
+ /** Compact priority toggle — visible only during streaming */
700
+ function PriorityToggle({ value, onChange }: { value: MessagePriority; onChange: (v: MessagePriority) => void }) {
701
+ const cycle = useCallback(() => {
702
+ const order: MessagePriority[] = ['next', 'later', 'now'];
703
+ const idx = order.indexOf(value);
704
+ onChange(order[(idx + 1) % order.length]!);
705
+ }, [value, onChange]);
706
+
707
+ const current = PRIORITY_OPTIONS.find((o) => o.value === value) ?? PRIORITY_OPTIONS[1]!;
708
+ const Icon = current.Icon;
709
+
710
+ return (
711
+ <button
712
+ type="button"
713
+ onClick={(e) => { e.stopPropagation(); cycle(); }}
714
+ className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-[11px] text-text-subtle hover:text-text-primary hover:bg-surface-elevated transition-colors border border-transparent hover:border-border"
715
+ aria-label={`Message priority: ${current.label}`}
716
+ title={`Priority: ${current.label} (click to cycle)`}
717
+ >
718
+ <Icon className="size-3" />
719
+ <span>{current.label}</span>
720
+ </button>
721
+ );
722
+ }
@@ -25,7 +25,7 @@ interface UseChatReturn {
25
25
  sessionTitle: string | null;
26
26
  /** When CLI provider assigns a different session ID, this holds the new ID */
27
27
  migratedSessionId: string | null;
28
- sendMessage: (content: string, opts?: { permissionMode?: string }) => void;
28
+ sendMessage: (content: string, opts?: { permissionMode?: string; priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }> }) => void;
29
29
  respondToApproval: (requestId: string, approved: boolean, data?: unknown) => void;
30
30
  cancelStreaming: () => void;
31
31
  reconnect: () => void;
@@ -385,11 +385,13 @@ export function useChat(sessionId: string | null, providerId = "claude", project
385
385
  }, [sessionId, providerId, projectName]);
386
386
 
387
387
  const sendMessage = useCallback(
388
- (content: string, opts?: { permissionMode?: string }) => {
388
+ (content: string, opts?: { permissionMode?: string; priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }> }) => {
389
389
  if (!content.trim()) return;
390
390
 
391
- // If streaming, cancel current stream first then send immediately
392
- if (phaseRef.current !== "idle") {
391
+ const isFollowUp = phaseRef.current !== "idle";
392
+
393
+ if (isFollowUp) {
394
+ // Streaming follow-up: finalize current assistant message, then send
393
395
  const finalContent = streamingContentRef.current;
394
396
  const finalEvents = [...streamingEventsRef.current];
395
397
  setMessages((prev) => {
@@ -402,7 +404,6 @@ export function useChat(sessionId: string | null, providerId = "claude", project
402
404
  }
403
405
  return prev;
404
406
  });
405
- send(JSON.stringify({ type: "cancel" }));
406
407
  }
407
408
 
408
409
  // Add user message
@@ -416,15 +417,26 @@ export function useChat(sessionId: string | null, providerId = "claude", project
416
417
  },
417
418
  ]);
418
419
 
419
- // Reset streaming state
420
+ // Reset streaming state for new turn
420
421
  streamingContentRef.current = "";
421
422
  streamingEventsRef.current = [];
422
423
  pendingMessageRef.current = null;
423
- setPhase("initializing");
424
- phaseRef.current = "initializing";
424
+ if (!isFollowUp) {
425
+ setPhase("initializing");
426
+ phaseRef.current = "initializing";
427
+ } else {
428
+ setPhase("thinking");
429
+ phaseRef.current = "thinking";
430
+ }
425
431
  setPendingApproval(null);
426
432
 
427
- send(JSON.stringify({ type: "message", content, permissionMode: opts?.permissionMode }));
433
+ send(JSON.stringify({
434
+ type: "message",
435
+ content,
436
+ permissionMode: opts?.permissionMode,
437
+ priority: opts?.priority,
438
+ images: opts?.images,
439
+ }));
428
440
  },
429
441
  [send],
430
442
  );
@@ -1 +0,0 @@
1
- import"./chunk-XZSTWKYB-5W2emiq4.js";import{n as e}from"./chunk-R5LLSJPH-LdG7RqsM.js";export{e as createArchitectureServices};
@@ -1 +0,0 @@
1
- import{n as e}from"./jsx-runtime-BRW_vwa9.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};
@@ -1 +0,0 @@
1
- import{o as e}from"./chunk-CFjPhJqf.js";import{t}from"./react-nm2Ru1Pt.js";import{t as n}from"./jsx-runtime-BRW_vwa9.js";import{t as r}from"./arrow-left-C_j9Ki73.js";import{o as i,t as a}from"./tab-store-DSz5PQI0.js";import{$ as o,A as s,L as c}from"./index-QiSWS6f-.js";var l=e(t(),1),u=n();function d(e){let t=e.trim();if(!t)return null;if(/^\d+$/.test(t))return`http://localhost:${t}`;/^localhost(:\d+)?/.test(t)&&(t=`http://${t}`),/^[\w.-]+:\d+/.test(t)&&!t.includes(`://`)&&(t=`http://${t}`),t.includes(`://`)||(t=t.includes(`localhost`)?`http://${t}`:`https://${t}`);try{return new URL(t),t}catch{return null}}function f(e){try{let t=new URL(e);return t.hostname===`localhost`||t.hostname===`127.0.0.1`||t.hostname===`0.0.0.0`||t.hostname===`::1`}catch{return!1}}function p(e){if(!f(e))return e;try{let t=new URL(e);return`/api/preview/${t.port||`80`}${t.pathname+t.search+t.hash}`}catch{return e}}function m({metadata:e,tabId:t}){let n=e?.url||`http://localhost:3000`,[m,h]=(0,l.useState)(n),[g,_]=(0,l.useState)(n),[v,y]=(0,l.useState)(p(n)),[b,x]=(0,l.useState)(!1),[S,C]=(0,l.useState)(!1),[w,T]=(0,l.useState)(!0),[E,D]=(0,l.useState)(null),O=(0,l.useRef)(null),k=a(e=>e.updateTab),A=(0,l.useRef)([n]),j=(0,l.useRef)(0),M=(0,l.useCallback)((e,n=!0)=>{let r=d(e);if(!r){D(`Invalid URL`);return}if(D(null),_(r),h(r),y(p(r)),T(!0),n){let e=A.current,t=j.current;A.current=e.slice(0,t+1),A.current.push(r),j.current=A.current.length-1}if(x(j.current>0),C(j.current<A.current.length-1),t)try{let e=new URL(r);k(t,{title:f(r)?`localhost:${e.port||`80`}`:e.hostname})}catch{}},[t,k]),N=(0,l.useCallback)(()=>{j.current>0&&(j.current--,M(A.current[j.current],!1))},[M]),P=(0,l.useCallback)(()=>{j.current<A.current.length-1&&(j.current++,M(A.current[j.current],!1))},[M]),F=(0,l.useCallback)(()=>{if(T(!0),D(null),O.current){let e=O.current.src;O.current.src=``,requestAnimationFrame(()=>{O.current&&(O.current.src=e)})}},[]),I=(0,l.useCallback)(()=>{window.open(g,`_blank`)},[g]);return(0,l.useEffect)(()=>{let t=e?.url;t&&t!==g&&M(t)},[e?.url]),(0,u.jsxs)(`div`,{className:`flex flex-col h-full w-full bg-background`,children:[(0,u.jsxs)(`div`,{className:`flex items-center gap-1 px-2 py-1.5 border-b border-border bg-surface shrink-0`,children:[(0,u.jsx)(`button`,{onClick:N,disabled:!b,className:`p-1.5 rounded hover:bg-surface-elevated disabled:opacity-30 transition-colors`,title:`Back`,children:(0,u.jsx)(r,{className:`size-4`})}),(0,u.jsx)(`button`,{onClick:P,disabled:!S,className:`p-1.5 rounded hover:bg-surface-elevated disabled:opacity-30 transition-colors`,title:`Forward`,children:(0,u.jsx)(o,{className:`size-4`})}),(0,u.jsx)(`button`,{onClick:F,className:`p-1.5 rounded hover:bg-surface-elevated transition-colors`,title:`Reload`,children:(0,u.jsx)(s,{className:`size-4 ${w?`animate-spin`:``}`})}),(0,u.jsxs)(`div`,{className:`flex-1 flex items-center gap-2 mx-1 px-2.5 py-1.5 rounded-md bg-background border border-border focus-within:border-accent/50 transition-colors`,children:[(0,u.jsx)(c,{className:`size-3.5 text-text-subtle shrink-0`}),(0,u.jsx)(`input`,{type:`text`,value:m,onChange:e=>h(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),M(m))},placeholder:`Enter URL or port (e.g. 3000, localhost:8080)`,className:`flex-1 bg-transparent text-xs text-text-primary outline-none placeholder:text-text-subtle min-w-0`})]}),(0,u.jsx)(`button`,{onClick:I,className:`p-1.5 rounded hover:bg-surface-elevated transition-colors`,title:`Open in browser`,children:(0,u.jsx)(i,{className:`size-4`})})]}),(0,u.jsxs)(`div`,{className:`flex-1 relative min-h-0`,children:[E?(0,u.jsx)(`div`,{className:`flex items-center justify-center h-full text-text-secondary text-sm`,children:(0,u.jsx)(`p`,{children:E})}):(0,u.jsx)(`iframe`,{ref:O,src:v,className:`w-full h-full border-0`,sandbox:`allow-scripts allow-forms allow-same-origin allow-popups allow-modals`,onLoad:()=>T(!1),onError:()=>{T(!1),D(`Failed to load ${g}`)}}),w&&!E&&(0,u.jsx)(`div`,{className:`absolute inset-0 flex items-center justify-center bg-background/50`,children:(0,u.jsxs)(`div`,{className:`flex items-center gap-2 text-sm text-text-secondary`,children:[(0,u.jsx)(s,{className:`size-4 animate-spin`}),(0,u.jsx)(`span`,{children:`Loading...`})]})})]})]})}export{m as BrowserTab};
@@ -1 +0,0 @@
1
- import{it as e,rt as t}from"./chunk-7R4GIKGN-DvbvLUIN.js";var n=(n,r)=>e.lang.round(t.parse(n)[r]);export{n as t};