@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.
- package/CHANGELOG.md +27 -8
- package/dist/web/assets/{_basePickBy-COwDPZl_.js → _basePickBy-CZovQgWd.js} +1 -1
- package/dist/web/assets/{_baseUniq-DCb0mkTp.js → _baseUniq-ClnvscgW.js} +1 -1
- package/dist/web/assets/{api-settings-CuUkz5gb.js → api-settings--eVrUeZM.js} +1 -1
- package/dist/web/assets/{arc-D0bJaFyD.js → arc-C2Qaz-ch.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-BVEUkQYB.js → architectureDiagram-2XIMDMQ5-Jq91S_rs.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-CU2t4NHJ.js → blockDiagram-WCTKOSBZ-CKGufRTy.js} +1 -1
- package/dist/web/assets/browser-tab-DAvH4mv0.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-DzjR91sM.js → c4Diagram-IC4MRINW-BNP2L9r_.js} +1 -1
- package/dist/web/assets/channel-w7yboq56.js +1 -0
- package/dist/web/assets/chat-tab-WEBXxGgN.js +7 -0
- package/dist/web/assets/{chunk-4BX2VUAB-0YMkpW2S.js → chunk-4BX2VUAB-BptTlTyl.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-Dp0pTM5r.js → chunk-55IACEB6-C4mUdyio.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CuYKSUgJ.js → chunk-7E7YKBS2-6xAQfBwa.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DvbvLUIN.js → chunk-7R4GIKGN-DXaGAn_K.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-CcEW1AMZ.js → chunk-C72U2L5F-DOtEiN5f.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Cgt-qg75.js → chunk-EGIJ26TM-D0KJTa_T.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-JCLgVcaC.js → chunk-FMBD7UC4-C_1aG0eb.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-B82RP9ow.js → chunk-GEFDOKGD-DwVPiYfW.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-Pb-JMOgO.js → chunk-JSJVCQXG-BSrqCL_3.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-BRj-ZEvL.js → chunk-KX2RTZJC-BCxGmbzy.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-CBRPKraG.js → chunk-KYZI473N-BKO5gMeU.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-DNFj84V6.js → chunk-L3YUKLVL-3wBgkSvL.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BnPzQK-O.js → chunk-MX3YWQON-BgjSEzus.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-BRj25yO7.js → chunk-NQ4KR5QH-DLrZwBEm.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BdXwVXjJ.js → chunk-O4XLMI2P-BurQy8tt.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-LfXT4p8B.js → chunk-OZEHJAEY-YTn24bGg.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-EdgQyTqa.js → chunk-PQ6SQG4A-BxtUGYhW.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-D3thuSok.js → chunk-PU5JKC2W-B66ELkQm.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-LdG7RqsM.js → chunk-R5LLSJPH-euR2RxLN.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-BHFnnXOt.js → chunk-WL4C6EOR-_2CBOJdI.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-DUmQrLsF.js → chunk-XIRO2GV7-kqQ0g6wW.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-CsGTseUr.js → chunk-XPW4576I-CtcaMb09.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-5W2emiq4.js → chunk-XZSTWKYB-BYxFzZwS.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-COdZIaX4.js → chunk-YBOYWFTD-Dx_fX35n.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +1 -0
- package/dist/web/assets/clone-BSi6cgDh.js +1 -0
- package/dist/web/assets/{code-editor-BRMOypkX.js → code-editor-B5sg_uJQ.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-C1QJ6GPW.js → cose-bilkent-S5V4N54A-CHHjH2dV.js} +1 -1
- package/dist/web/assets/{dagre-CWo8w9wK.js → dagre-CNtSxiE_.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-Br4t5TRV.js → dagre-KLK3FWXG-ChenfPp1.js} +1 -1
- package/dist/web/assets/database-viewer-CwtyWCkE.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-CkDC2uAj.js → diagram-E7M64L7V-CzKYZM0Y.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-NvhckwcA.js → diagram-IFDJBPK2-ChB_paPo.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO--nUaNiyB.js → diagram-P4PSJMXO-D1eW1dkL.js} +1 -1
- package/dist/web/assets/{diff-viewer-jDU2bcGj.js → diff-viewer-CzE5M-Wd.js} +1 -1
- package/dist/web/assets/{erDiagram-INFDFZHY-DK4QEZYh.js → erDiagram-INFDFZHY-mCvUFSn6.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-B9h_Ba-v.js → flowDiagram-PKNHOUZH-14ohZ1M1.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-BVlftqyZ.js → ganttDiagram-A5KZAMGK-DIX0pLbk.js} +1 -1
- package/dist/web/assets/git-graph-6yxCeeN9.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js → gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js} +1 -1
- package/dist/web/assets/{graphlib-BbbiUImY.js → graphlib-DhOZxqsh.js} +1 -1
- package/dist/web/assets/index-DE8b9u8F.css +2 -0
- package/dist/web/assets/index-wuWZBO9y.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +2 -0
- package/dist/web/assets/input-Brjz2Vv-.js +41 -0
- package/dist/web/assets/{isEmpty-DXomfd7J.js → isEmpty-C0YYdhYj.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-cW7SMLa_.js → ishikawaDiagram-PHBUUO56-olazD6dZ.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-DFQXUZsc.js → journeyDiagram-4ABVD52K-CttDH9bb.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-BMUhjxqj.js → kanban-definition-K7BYSVSG-BBXbI37U.js} +1 -1
- package/dist/web/assets/keybindings-store-mkBHnWN1.js +1 -0
- package/dist/web/assets/{line--xyfYP3x.js → line-DBLLF7lH.js} +1 -1
- package/dist/web/assets/{linear-BdqW7iQu.js → linear-BLFWatDe.js} +1 -1
- package/dist/web/assets/{markdown-renderer-BCjJbGP8.js → markdown-renderer-CxWxvrzT.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-BY8JfkE_.js → mermaid-parser.core-BKiGOTjR.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-DIv-LMXG.js → mindmap-definition-YRQLILUH-DoT7m4Sz.js} +1 -1
- package/dist/web/assets/{ordinal-CIoJK3nc.js → ordinal-CCj7PWgZ.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-seSK40d1.js → pieDiagram-SKSYHLDU-Bkh2E4zE.js} +1 -1
- package/dist/web/assets/postgres-viewer-UP3yv9Yh.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-BaRFqlsA.js → quadrantDiagram-337W2JSQ-B7zgALOL.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-1WWjMQB_.js → requirementDiagram-Z7DCOOCP-D_5GXNRo.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DEGGYsk7.js → sankeyDiagram-WA2Y5GQK-BA9EFAAe.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-BtRvoUTC.js → sequenceDiagram-2WXFIKYE-fyWIrHiG.js} +1 -1
- package/dist/web/assets/{settings-store-D3dJqGhB.js → settings-store-Bbhg_ptG.js} +2 -2
- package/dist/web/assets/settings-tab-BoBXlVHe.js +1 -0
- package/dist/web/assets/sqlite-viewer-lzRVvM5j.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-C16aO8tn.js → stateDiagram-RAJIS63D-DfRBcaBu.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +1 -0
- package/dist/web/assets/{tab-store-DSz5PQI0.js → tab-store-DcIBZTD4.js} +1 -1
- package/dist/web/assets/{terminal-tab-MRg8y1xF.js → terminal-tab-CAZtLK6i.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-DrjxCpEM.js → timeline-definition-YZTLITO2-DYfwJ1jM.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +1 -0
- package/dist/web/assets/{use-monaco-theme-BQzvItNE.js → use-monaco-theme-vwto-Vlf.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-DfYFnniI.js → vennDiagram-LZ73GAT5-DqbKNRD9.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-BRvXOVlG.js → xychartDiagram-JWTSCODW-DhUL86qT.js} +1 -1
- package/dist/web/index.html +10 -12
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +212 -76
- package/src/server/index.ts +4 -1
- package/src/server/routes/browser-preview.ts +135 -65
- package/src/server/ws/chat.ts +103 -73
- package/src/types/api.ts +1 -1
- package/src/types/chat.ts +2 -0
- package/src/web/components/browser/browser-tab.tsx +105 -224
- package/src/web/components/chat/chat-tab.tsx +3 -3
- package/src/web/components/chat/message-input.tsx +42 -4
- package/src/web/hooks/use-chat.ts +21 -9
- package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +0 -1
- package/dist/web/assets/arrow-left-C_j9Ki73.js +0 -1
- package/dist/web/assets/browser-tab-BhTdeeZd.js +0 -1
- package/dist/web/assets/channel-CKNZAqoN.js +0 -1
- package/dist/web/assets/chat-tab-ZiiUVOxM.js +0 -7
- package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +0 -1
- package/dist/web/assets/clone-DNDy9Sms.js +0 -1
- package/dist/web/assets/database-viewer-CEoDpzPz.js +0 -1
- package/dist/web/assets/git-graph-DMQzw4Sp.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +0 -1
- package/dist/web/assets/index-B4Iz1Wbi.css +0 -2
- package/dist/web/assets/index-QiSWS6f-.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +0 -2
- package/dist/web/assets/input-DGlv6gt_.js +0 -41
- package/dist/web/assets/keybindings-store-BplH-yiN.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +0 -1
- package/dist/web/assets/postgres-viewer-s0snZ9CL.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +0 -1
- package/dist/web/assets/settings-tab-2YkgmrY0.js +0 -1
- package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +0 -1
- package/dist/web/assets/switch-mjGtIVDJ.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +0 -1
- /package/dist/web/assets/{api-client-icCZ-07C.js → api-client-DpGMOZNf.js} +0 -0
- /package/dist/web/assets/{array-CLwNaqU1.js → array-BGFCBI0e.js} +0 -0
- /package/dist/web/assets/{columns-2-Bcg3QJBg.js → columns-2-ChOTgl3e.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-B-QQuWwK.js → cytoscape.esm-Ccan6xou.js} +0 -0
- /package/dist/web/assets/{defaultLocale-D_VMtRaY.js → defaultLocale-CRZydyG6.js} +0 -0
- /package/dist/web/assets/{dist-Ckxnw5rl.js → dist-Cce3efmT.js} +0 -0
- /package/dist/web/assets/{dist-CMmNEgEP.js → dist-T0Vhi0Mh.js} +0 -0
- /package/dist/web/assets/{init-vVpfz1D6.js → init-B8gtcn7T.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-DvHDmeBe.js → isArrayLikeObject-B4pdpV8V.js} +0 -0
- /package/dist/web/assets/{katex-C3cZrCvP.js → katex-Bbu770d9.js} +0 -0
- /package/dist/web/assets/{math-a44lmFDa.js → math-DwgHI-Cu.js} +0 -0
- /package/dist/web/assets/{path-CuyvWNAH.js → path-DZF-JdEe.js} +0 -0
- /package/dist/web/assets/{preload-helper-CsoeaaUJ.js → preload-helper-qlgyTAkD.js} +0 -0
- /package/dist/web/assets/{react-BPIfZRKM.js → react-BGf7KNLk.js} +0 -0
- /package/dist/web/assets/{rough.esm-c4PR5shF.js → rough.esm-VLpapkIG.js} +0 -0
- /package/dist/web/assets/{src-CLWraeNW.js → src-BoSBNdA_.js} +0 -0
- /package/dist/web/assets/{table-C9jDaRl2.js → table-Yo02WRH-.js} +0 -0
- /package/dist/web/assets/{tag-CENGyt_L.js → tag-CaC1ng2E.js} +0 -0
- /package/dist/web/assets/{utils-Bslrbb-G.js → utils-btZ8C8-R.js} +0 -0
|
@@ -1,77 +1,7 @@
|
|
|
1
|
-
import { useState, useRef, useCallback
|
|
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
|
|
83
|
-
const [
|
|
84
|
-
const [
|
|
85
|
-
const [
|
|
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
|
-
|
|
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
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
<
|
|
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={
|
|
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-
|
|
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
|
-
{/*
|
|
133
|
+
{/* iframe */}
|
|
238
134
|
<div className="flex-1 relative min-h-0">
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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({
|
|
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};
|