@hienlh/ppm 0.8.86 → 0.8.87
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 +193 -5
- package/bun.lock +5 -0
- package/dist/web/assets/{_basePickBy-5eBmZ_lt.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-DimLlN0y.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/api-settings-Bx1GaNmQ.js +1 -0
- package/dist/web/assets/{arc-D4SasZrA.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-nv0WbM7d.js → architectureDiagram-2XIMDMQ5-DWBCPMLF.js} +1 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-C1XvYrb8.js → blockDiagram-WCTKOSBZ-TEF8Ally.js} +1 -1
- package/dist/web/assets/browser-tab-DaHGm_0i.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-CygDrbWJ.js → c4Diagram-IC4MRINW-dV22iAsY.js} +1 -1
- package/dist/web/assets/channel-wrd-NHWf.js +1 -0
- package/dist/web/assets/chat-tab-BDYE0KHF.js +8 -0
- package/dist/web/assets/chevron-right-DeV0ehiG.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C2FDgsgT.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-jF4w6cat.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-BVCECZFi.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DXTbeu5d.js → chunk-7R4GIKGN-BbIFzsIv.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-BaZqOsTs.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Bky2tcH7.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-Cp4BK9A8.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-BosFEH7G.js → chunk-GEFDOKGD-BbQkJu8C.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-H5Gbjsbr.js → chunk-JSJVCQXG-23tyvw8k.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-CWerSUwS.js → chunk-KX2RTZJC-sQ0o-39C.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-FvwP7jUy.js → chunk-KYZI473N-BcUZNnwd.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-D1PI_ORP.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C7Vzk_AI.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-BceYBGYX.js → chunk-NQ4KR5QH-wMgTlP7f.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-WPtzgxql.js → chunk-O4XLMI2P-JC6EGoUz.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-DlHXDeLY.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-Ci_Prygb.js → chunk-PQ6SQG4A-D6BTbCQw.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-CO0zMN-z.js → chunk-PU5JKC2W-Dw8ClWch.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-IAEEzfpM.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-BLXalOgc.js → chunk-WL4C6EOR-DfofndiH.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Dx1Ri_p2.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-m9pPGKn7.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-B_08ExbI.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-DqSOVcYe.js → chunk-YBOYWFTD-CeU4Q-xC.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +1 -0
- package/dist/web/assets/clone-LRxlvnMj.js +1 -0
- package/dist/web/assets/code-editor-DTA3c9Y8.js +2 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-DlL82QHu.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/csv-preview-DLqYtXxt.js +10 -0
- package/dist/web/assets/{dagre-BmVoh2At.js → dagre-Dbb5k38K.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-sDrRW9MQ.js → dagre-KLK3FWXG-BH7aWGRP.js} +1 -1
- package/dist/web/assets/database-viewer-DXk79Nel.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-ChnAhgni.js → diagram-E7M64L7V-B1Qz70Do.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-DW1J1uJd.js → diagram-IFDJBPK2-k55eVqVU.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-CQ32hyG_.js → diagram-P4PSJMXO-BkfNRc9U.js} +1 -1
- package/dist/web/assets/diff-viewer-HhIcsOQE.js +4 -0
- package/dist/web/assets/dist-DylI9XxN.js +13 -0
- package/dist/web/assets/dist-lF8CoYII.js +41 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-6CHo6nOw.js → erDiagram-INFDFZHY-CKzVujYI.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-DroDiNT0.js → flowDiagram-PKNHOUZH-DIqcTrDV.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DP0QBh8w.js → ganttDiagram-A5KZAMGK-D4v7ZbVE.js} +1 -1
- package/dist/web/assets/git-graph-CQtWu8yE.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js → gitGraphDiagram-K3NZZRJ6-BTXo57mF.js} +1 -1
- package/dist/web/assets/{graphlib-CQBb2thr.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-CgQXpBb_.css +2 -0
- package/dist/web/assets/index-DEeeRoka.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +2 -0
- package/dist/web/assets/input-BglMT33g.js +1 -0
- package/dist/web/assets/{isEmpty-B4kqZBtn.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-46yibrV5.js → ishikawaDiagram-PHBUUO56-BOyvKMmB.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-BcmRwjK-.js → journeyDiagram-4ABVD52K-ufoasAy6.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-B619K53y.js → kanban-definition-K7BYSVSG-Bi0UTUeN.js} +1 -1
- package/dist/web/assets/keybindings-store-1CJ7VX57.js +1 -0
- package/dist/web/assets/lib-BQ34Db2e.js +4 -0
- package/dist/web/assets/{line-1gcO63_w.js → line-B78g-52T.js} +1 -1
- package/dist/web/assets/{linear-DfRqDoVd.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/markdown-renderer-Brj8_LQM.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-XtjZQOeM.js → mermaid-parser.core-DMIWdgEW.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-CifOFo_q.js → mindmap-definition-YRQLILUH-BsfWvIoO.js} +1 -1
- package/dist/web/assets/{ordinal-BJYw-iDX.js → ordinal-_K3x1fkz.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-BuHUh_fO.js → pieDiagram-SKSYHLDU-WP0XXw51.js} +1 -1
- package/dist/web/assets/postgres-viewer-CwkTGmqy.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-Bau_hj6Z.js → quadrantDiagram-337W2JSQ-FHMogtsh.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-Cq2b-uwp.js → requirementDiagram-Z7DCOOCP-BatTxyWb.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js → sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-qPxiTUcS.js → sequenceDiagram-2WXFIKYE-ByxQqGgs.js} +1 -1
- package/dist/web/assets/settings-tab-BDE1MsIh.js +1 -0
- package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-Dulj2oa8.js → stateDiagram-RAJIS63D-f8opcZNY.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +1 -0
- package/dist/web/assets/tab-store-BJw7OCmy.js +1 -0
- package/dist/web/assets/{terminal-tab-wKgpSPAT.js → terminal-tab-CCDLZA5Y.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-BWyDnCYq.js → timeline-definition-YZTLITO2-58BlOSf9.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
- package/dist/web/assets/use-monaco-theme-CNzekTN3.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-B9Iv2bNV.js → vennDiagram-LZ73GAT5-BOSy9ma9.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-ChXcMzBQ.js → xychartDiagram-JWTSCODW-z5MVJauZ.js} +1 -1
- package/dist/web/index.html +12 -11
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +232 -7
- package/docs/codebase-summary.md +9 -3
- package/docs/design-guidelines.md +21 -0
- package/docs/project-changelog.md +115 -1
- package/docs/project-roadmap.md +41 -19
- package/docs/system-architecture.md +212 -15
- package/package.json +3 -2
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/restart.ts +9 -1
- package/src/cli/commands/status.ts +19 -0
- package/src/index.ts +2 -3
- package/src/providers/claude-agent-sdk.ts +92 -25
- package/src/providers/mock-provider.ts +6 -1
- package/src/server/index.ts +38 -166
- package/src/server/routes/browser-preview.ts +159 -0
- package/src/server/routes/chat.ts +52 -3
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/proxy.ts +46 -53
- package/src/server/routes/tunnel.ts +0 -32
- package/src/server/routes/workspace.ts +35 -0
- package/src/server/ws/chat.ts +207 -146
- package/src/services/account-selector.service.ts +16 -8
- package/src/services/account.service.ts +19 -13
- package/src/services/claude-usage.service.ts +48 -11
- package/src/services/cloud-ws.service.ts +227 -0
- package/src/services/cloud.service.ts +10 -6
- package/src/services/db.service.ts +111 -6
- package/src/services/proxy.service.ts +4 -19
- package/src/services/supervisor.ts +285 -25
- package/src/types/api.ts +9 -1
- package/src/types/chat.ts +3 -1
- package/src/web/app.tsx +41 -35
- package/src/web/components/browser/browser-tab.tsx +106 -97
- package/src/web/components/chat/account-rotation-settings.tsx +163 -0
- package/src/web/components/chat/chat-history-bar.tsx +72 -6
- package/src/web/components/chat/chat-tab.tsx +32 -16
- package/src/web/components/chat/chat-welcome.tsx +148 -0
- package/src/web/components/chat/message-input.tsx +107 -13
- package/src/web/components/chat/message-list.tsx +27 -15
- package/src/web/components/chat/session-picker.tsx +78 -31
- package/src/web/components/chat/usage-badge.tsx +11 -1
- package/src/web/components/editor/code-editor.tsx +36 -26
- package/src/web/components/editor/csv-preview.tsx +228 -0
- package/src/web/components/editor/editor-breadcrumb.tsx +216 -0
- package/src/web/components/editor/editor-toolbar.tsx +74 -0
- package/src/web/components/layout/command-palette.tsx +3 -1
- package/src/web/components/layout/editor-panel.tsx +162 -18
- package/src/web/components/layout/panel-layout.tsx +17 -1
- package/src/web/components/settings/proxy-settings-section.tsx +40 -42
- package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
- package/src/web/hooks/use-chat.ts +211 -201
- package/src/web/hooks/use-global-keybindings.ts +25 -2
- package/src/web/hooks/use-server-reload.ts +9 -0
- package/src/web/hooks/use-url-sync.ts +173 -21
- package/src/web/hooks/use-voice-input.ts +111 -0
- package/src/web/lib/csv-parser.ts +134 -0
- package/src/web/stores/connection-store.ts +39 -0
- package/src/web/stores/keybindings-store.ts +1 -0
- package/src/web/stores/panel-store.ts +73 -19
- package/src/web/stores/panel-utils.ts +145 -3
- package/dist/web/assets/api-settings-CFw-lh5k.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +0 -1
- package/dist/web/assets/browser-tab-CmsL5eny.js +0 -1
- package/dist/web/assets/channel-DmKoFTd_.js +0 -1
- package/dist/web/assets/chat-tab-CFWsf13Z.js +0 -7
- package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +0 -1
- package/dist/web/assets/clone-CijCFRT5.js +0 -1
- package/dist/web/assets/code-editor-H_dAh_fJ.js +0 -1
- package/dist/web/assets/database-viewer-DBzsgEJ8.js +0 -1
- package/dist/web/assets/diff-viewer-DzS-OnAR.js +0 -4
- package/dist/web/assets/dist-0Va_2L7G.js +0 -16
- package/dist/web/assets/dist-D9irYETY.js +0 -41
- package/dist/web/assets/git-graph-D3C7F8o3.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +0 -1
- package/dist/web/assets/index-CIkjfera.js +0 -31
- package/dist/web/assets/index-WKLuYsBY.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +0 -2
- package/dist/web/assets/input-CGp1nFIg.js +0 -1
- package/dist/web/assets/keybindings-store-BdaoLwSo.js +0 -1
- package/dist/web/assets/markdown-renderer-DH49Zag7.js +0 -69
- package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +0 -1
- package/dist/web/assets/postgres-viewer-B9FYk8sD.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +0 -1
- package/dist/web/assets/settings-store-DWXGVHsE.js +0 -2
- package/dist/web/assets/settings-tab-D-q8pd-5.js +0 -1
- package/dist/web/assets/sqlite-viewer-CDqcTePw.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +0 -1
- package/dist/web/assets/tab-store-BPeiymiH.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +0 -1
- package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +0 -11
- package/src/services/port-tunnel.service.ts +0 -97
- /package/dist/web/assets/{api-client-DOElml5u.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-CYkMkqnU.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DpsNbZOc.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-HeHO0VhB.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-BUYzeuKe.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-Rr1s_RiX.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-BB-mzMLb.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-CKoArbIw.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{math-B7b0HgJF.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-BAQ3hXlG.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{preload-helper-DeiOTZKJ.js → preload-helper-uTix4PVD.js} +0 -0
- /package/dist/web/assets/{react-Dev-wu-s.js → react-ER-4DN55.js} +0 -0
- /package/dist/web/assets/{rough.esm-Dwml_la6.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-B_cC68fH.js → src-BqX54PbV.js} +0 -0
- /package/dist/web/assets/{table-COiJDPRA.js → table-C7X5UAEI.js} +0 -0
- /package/dist/web/assets/{tag-LMq02LfE.js → tag-CCtdV063.js} +0 -0
- /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BNytJOb1.js} +0 -0
|
@@ -1,141 +1,150 @@
|
|
|
1
|
-
import { useState, useRef } from "react";
|
|
2
|
-
import { Globe, Loader2,
|
|
3
|
-
import {
|
|
4
|
-
import { Input } from "@/components/ui/input";
|
|
1
|
+
import { useState, useRef, useCallback } from "react";
|
|
2
|
+
import { ExternalLink, Globe, Loader2, RefreshCw, X } from "lucide-react";
|
|
3
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
5
4
|
import { api } from "@/lib/api-client";
|
|
6
5
|
|
|
7
6
|
interface BrowserTabProps {
|
|
8
7
|
metadata?: Record<string, unknown>;
|
|
8
|
+
tabId?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function BrowserTab({ metadata }: BrowserTabProps) {
|
|
12
|
-
const initialPort = metadata?.port
|
|
13
|
-
|
|
14
|
-
const [port, setPort] = useState(initialPort?.toString() ?? "");
|
|
11
|
+
export function BrowserTab({ metadata, tabId }: BrowserTabProps) {
|
|
12
|
+
const initialPort = (metadata?.port as number) || 0;
|
|
13
|
+
const [portInput, setPortInput] = useState(initialPort ? String(initialPort) : "");
|
|
15
14
|
const [tunnelUrl, setTunnelUrl] = useState<string | null>(null);
|
|
16
15
|
const [loading, setLoading] = useState(false);
|
|
17
16
|
const [error, setError] = useState<string | null>(null);
|
|
18
|
-
const [copied, setCopied] = useState(false);
|
|
19
17
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
18
|
+
const updateTab = useTabStore((s) => s.updateTab);
|
|
20
19
|
|
|
21
|
-
const startTunnel = async () => {
|
|
22
|
-
const p = Number(port);
|
|
23
|
-
if (!p || p < 1 || p > 65535) {
|
|
24
|
-
setError("Enter a valid port (1-65535)");
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
20
|
+
const startTunnel = useCallback(async (port: number) => {
|
|
27
21
|
setLoading(true);
|
|
28
22
|
setError(null);
|
|
23
|
+
setTunnelUrl(null);
|
|
24
|
+
|
|
29
25
|
try {
|
|
30
|
-
const
|
|
31
|
-
setTunnelUrl(
|
|
32
|
-
|
|
33
|
-
|
|
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}`);
|
|
34
31
|
} finally {
|
|
35
32
|
setLoading(false);
|
|
36
33
|
}
|
|
37
|
-
};
|
|
34
|
+
}, [tabId, metadata, updateTab]);
|
|
38
35
|
|
|
39
|
-
const stopTunnel = async () => {
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
42
|
-
try {
|
|
43
|
-
await api.post("/api/tunnel/port/stop", { port: p });
|
|
44
|
-
} catch {}
|
|
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 {}
|
|
45
40
|
setTunnelUrl(null);
|
|
46
|
-
|
|
41
|
+
if (tabId) updateTab(tabId, { title: "Browser" });
|
|
42
|
+
}, [portInput, tabId, updateTab]);
|
|
47
43
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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");
|
|
53
49
|
};
|
|
54
50
|
|
|
55
|
-
const
|
|
51
|
+
const reload = () => {
|
|
56
52
|
if (iframeRef.current) {
|
|
57
|
-
|
|
53
|
+
const src = iframeRef.current.src;
|
|
54
|
+
iframeRef.current.src = "";
|
|
55
|
+
requestAnimationFrame(() => { if (iframeRef.current) iframeRef.current.src = src; });
|
|
58
56
|
}
|
|
59
57
|
};
|
|
60
58
|
|
|
61
|
-
//
|
|
59
|
+
// No tunnel yet — show port input
|
|
62
60
|
if (!tunnelUrl) {
|
|
63
61
|
return (
|
|
64
|
-
<div className="flex items-center justify-center h-full p-
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<form
|
|
75
|
-
onSubmit={(e) => { e.preventDefault(); startTunnel(); }}
|
|
76
|
-
className="flex gap-2"
|
|
77
|
-
>
|
|
78
|
-
<Input
|
|
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
|
|
79
72
|
type="number"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
className="h-9 text-sm font-mono flex-1"
|
|
73
|
+
value={portInput}
|
|
74
|
+
onChange={(e) => setPortInput(e.target.value)}
|
|
75
|
+
placeholder="3000"
|
|
84
76
|
min={1}
|
|
85
77
|
max={65535}
|
|
86
78
|
autoFocus
|
|
87
|
-
|
|
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"
|
|
88
80
|
/>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
)}
|
|
103
97
|
</div>
|
|
104
98
|
);
|
|
105
99
|
}
|
|
106
100
|
|
|
107
|
-
// Tunnel active — show
|
|
101
|
+
// Tunnel active — show iframe
|
|
108
102
|
return (
|
|
109
|
-
<div className="flex flex-col h-full">
|
|
103
|
+
<div className="flex flex-col h-full w-full bg-background">
|
|
110
104
|
{/* Toolbar */}
|
|
111
|
-
<div className="flex items-center gap-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
</
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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" />
|
|
110
|
+
<button
|
|
111
|
+
onClick={reload}
|
|
112
|
+
className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
|
|
113
|
+
title="Reload"
|
|
114
|
+
>
|
|
115
|
+
<RefreshCw className="size-3.5" />
|
|
116
|
+
</button>
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => window.open(tunnelUrl, "_blank")}
|
|
119
|
+
className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
|
|
120
|
+
title="Open in browser"
|
|
121
|
+
>
|
|
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" />
|
|
130
|
+
</button>
|
|
129
131
|
</div>
|
|
130
132
|
|
|
131
|
-
{/*
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
{/* iframe */}
|
|
134
|
+
<div className="flex-1 relative min-h-0">
|
|
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 && (
|
|
143
|
+
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
|
|
144
|
+
<Loader2 className="size-5 animate-spin text-text-secondary" />
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
139
148
|
</div>
|
|
140
149
|
);
|
|
141
150
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useState, useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import { Settings, X } from "lucide-react";
|
|
3
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
4
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
getAccountSettings,
|
|
8
|
+
updateAccountSettings,
|
|
9
|
+
type AccountSettings,
|
|
10
|
+
} from "../../lib/api-settings";
|
|
11
|
+
|
|
12
|
+
interface AccountRotationSettingsProps {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const mdQuery = typeof window !== "undefined" ? window.matchMedia("(min-width: 768px)") : null;
|
|
18
|
+
function subscribeMedia(cb: () => void) {
|
|
19
|
+
mdQuery?.addEventListener("change", cb);
|
|
20
|
+
return () => mdQuery?.removeEventListener("change", cb);
|
|
21
|
+
}
|
|
22
|
+
function getIsDesktop() {
|
|
23
|
+
return mdQuery?.matches ?? true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SettingsContent() {
|
|
27
|
+
const [settings, setSettings] = useState<AccountSettings | null>(null);
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setLoading(true);
|
|
32
|
+
getAccountSettings()
|
|
33
|
+
.then(setSettings)
|
|
34
|
+
.finally(() => setLoading(false));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
if (loading) {
|
|
38
|
+
return <p className="text-xs text-text-subtle py-4 text-center">Loading...</p>;
|
|
39
|
+
}
|
|
40
|
+
if (!settings) {
|
|
41
|
+
return <p className="text-xs text-text-subtle py-4 text-center">Failed to load settings</p>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-4">
|
|
46
|
+
{/* Strategy */}
|
|
47
|
+
<div className="space-y-1.5">
|
|
48
|
+
<label className="text-xs font-medium text-text-primary">Rotation Strategy</label>
|
|
49
|
+
<Select
|
|
50
|
+
value={settings.strategy}
|
|
51
|
+
onValueChange={async (v) => {
|
|
52
|
+
const updated = await updateAccountSettings({ strategy: v as AccountSettings["strategy"] });
|
|
53
|
+
setSettings(updated);
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<SelectTrigger className="w-full h-9 text-xs">
|
|
57
|
+
<SelectValue />
|
|
58
|
+
</SelectTrigger>
|
|
59
|
+
<SelectContent>
|
|
60
|
+
<SelectItem value="round-robin">Round-robin</SelectItem>
|
|
61
|
+
<SelectItem value="fill-first">Fill-first</SelectItem>
|
|
62
|
+
<SelectItem value="lowest-usage">Lowest usage</SelectItem>
|
|
63
|
+
</SelectContent>
|
|
64
|
+
</Select>
|
|
65
|
+
<p className="text-[10px] text-text-subtle">
|
|
66
|
+
{settings.strategy === "round-robin" && "Cycles through accounts evenly"}
|
|
67
|
+
{settings.strategy === "fill-first" && "Uses one account until its limit, then moves on"}
|
|
68
|
+
{settings.strategy === "lowest-usage" && "Picks the account with the lowest current usage"}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Max Retry */}
|
|
73
|
+
<div className="space-y-1.5">
|
|
74
|
+
<label className="text-xs font-medium text-text-primary">Max Retry</label>
|
|
75
|
+
<input
|
|
76
|
+
type="number"
|
|
77
|
+
min={0}
|
|
78
|
+
value={settings.maxRetry}
|
|
79
|
+
className="w-full h-9 text-xs border rounded-md px-3 bg-background"
|
|
80
|
+
onChange={async (e) => {
|
|
81
|
+
const v = parseInt(e.target.value, 10);
|
|
82
|
+
if (!isNaN(v) && v >= 0) {
|
|
83
|
+
const updated = await updateAccountSettings({ maxRetry: v });
|
|
84
|
+
setSettings(updated);
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
<p className="text-[10px] text-text-subtle">
|
|
89
|
+
How many accounts to try on failure. 0 = try all available accounts.
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Active accounts */}
|
|
94
|
+
<div className="flex items-center justify-between text-xs border-t border-border pt-3">
|
|
95
|
+
<span className="text-text-subtle">Active accounts</span>
|
|
96
|
+
<span className="font-medium text-text-primary">{settings.activeCount}</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function AccountRotationSettings({ open, onOpenChange }: AccountRotationSettingsProps) {
|
|
103
|
+
const isDesktop = useSyncExternalStore(subscribeMedia, getIsDesktop);
|
|
104
|
+
|
|
105
|
+
if (!open) return null;
|
|
106
|
+
|
|
107
|
+
// Desktop: Dialog
|
|
108
|
+
if (isDesktop) {
|
|
109
|
+
return (
|
|
110
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
111
|
+
<DialogContent className="sm:max-w-sm">
|
|
112
|
+
<DialogHeader>
|
|
113
|
+
<DialogTitle className="text-sm flex items-center gap-2">
|
|
114
|
+
<Settings className="size-4" /> Rotation & Retry
|
|
115
|
+
</DialogTitle>
|
|
116
|
+
</DialogHeader>
|
|
117
|
+
<SettingsContent />
|
|
118
|
+
</DialogContent>
|
|
119
|
+
</Dialog>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Mobile: Bottom sheet
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
<div
|
|
127
|
+
className="fixed inset-0 z-50 transition-opacity duration-200 opacity-100"
|
|
128
|
+
onClick={() => onOpenChange(false)}
|
|
129
|
+
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
|
130
|
+
/>
|
|
131
|
+
<div
|
|
132
|
+
className={cn(
|
|
133
|
+
"fixed bottom-0 left-0 right-0 z-50 bg-background rounded-t-2xl border-t border-border shadow-2xl",
|
|
134
|
+
"transition-transform duration-300 ease-out max-h-[85vh] overflow-y-auto",
|
|
135
|
+
"translate-y-0",
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{/* Drag handle */}
|
|
139
|
+
<div className="flex justify-center pt-3 pb-1">
|
|
140
|
+
<div className="w-10 h-1 rounded-full bg-border" />
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Header */}
|
|
144
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
|
145
|
+
<span className="text-sm font-semibold flex items-center gap-2">
|
|
146
|
+
<Settings className="size-4" /> Rotation & Retry
|
|
147
|
+
</span>
|
|
148
|
+
<button
|
|
149
|
+
onClick={() => onOpenChange(false)}
|
|
150
|
+
className="flex items-center justify-center size-7 rounded-md hover:bg-surface-elevated transition-colors"
|
|
151
|
+
>
|
|
152
|
+
<X className="size-4" />
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Content */}
|
|
157
|
+
<div className="px-4 py-4 pb-8">
|
|
158
|
+
<SettingsContent />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff } from "lucide-react";
|
|
2
|
+
import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff, Bug, ClipboardCheck, Pin, PinOff } from "lucide-react";
|
|
3
3
|
import { Activity } from "lucide-react";
|
|
4
4
|
import { api, projectUrl } from "@/lib/api-client";
|
|
5
5
|
import { useTabStore } from "@/stores/tab-store";
|
|
@@ -22,6 +22,7 @@ interface ChatHistoryBarProps {
|
|
|
22
22
|
onSelectSession?: (session: SessionInfo) => void;
|
|
23
23
|
onBugReport?: () => void;
|
|
24
24
|
isConnected?: boolean;
|
|
25
|
+
streamingAccountLabel?: string | null;
|
|
25
26
|
onReconnect?: () => void;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -48,9 +49,37 @@ function pctColor(pct: number): string {
|
|
|
48
49
|
return "text-green-500";
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function DebugCopyButton({ sessionId, projectName }: { sessionId: string; projectName: string }) {
|
|
53
|
+
const [copied, setCopied] = useState(false);
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
onClick={async () => {
|
|
57
|
+
try {
|
|
58
|
+
const data = await api.get<{ ppmSessionId: string; sdkSessionId: string; jsonlPath: string | null; projectPath: string }>(
|
|
59
|
+
`${projectUrl(projectName)}/chat/sessions/${sessionId}/debug?project=${encodeURIComponent(projectName)}`,
|
|
60
|
+
);
|
|
61
|
+
const info = [
|
|
62
|
+
`PPM Session: ${data.ppmSessionId}`,
|
|
63
|
+
`SDK Session: ${data.sdkSessionId}`,
|
|
64
|
+
data.jsonlPath ? `JSONL: ${data.jsonlPath}` : `JSONL: not found`,
|
|
65
|
+
data.projectPath ? `Project: ${data.projectPath}` : null,
|
|
66
|
+
].filter(Boolean).join("\n");
|
|
67
|
+
await navigator.clipboard.writeText(info);
|
|
68
|
+
setCopied(true);
|
|
69
|
+
setTimeout(() => setCopied(false), 1500);
|
|
70
|
+
} catch { /* silent */ }
|
|
71
|
+
}}
|
|
72
|
+
className={`p-1 rounded transition-colors ${copied ? "text-green-500 bg-green-500/10" : "text-text-subtle hover:text-text-secondary hover:bg-surface-elevated"}`}
|
|
73
|
+
title={copied ? "Copied!" : "Copy session debug info"}
|
|
74
|
+
>
|
|
75
|
+
{copied ? <ClipboardCheck className="size-3" /> : <Bug className="size-3" />}
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
51
80
|
export function ChatHistoryBar({
|
|
52
81
|
projectName, usageInfo, contextWindowPct, usageLoading, refreshUsage, lastFetchedAt,
|
|
53
|
-
sessionId, onSelectSession, onBugReport, isConnected, onReconnect,
|
|
82
|
+
sessionId, onSelectSession, onBugReport, isConnected, streamingAccountLabel, onReconnect,
|
|
54
83
|
}: ChatHistoryBarProps) {
|
|
55
84
|
const [activePanel, setActivePanel] = useState<PanelType>(null);
|
|
56
85
|
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
@@ -121,6 +150,27 @@ export function ChatHistoryBar({
|
|
|
121
150
|
|
|
122
151
|
const cancelEditing = useCallback(() => setEditingId(null), []);
|
|
123
152
|
|
|
153
|
+
const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
|
|
154
|
+
e.stopPropagation();
|
|
155
|
+
if (!projectName) return;
|
|
156
|
+
const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
|
|
157
|
+
try {
|
|
158
|
+
if (session.pinned) {
|
|
159
|
+
await api.del(url);
|
|
160
|
+
} else {
|
|
161
|
+
await api.put(url);
|
|
162
|
+
}
|
|
163
|
+
setSessions((prev) => {
|
|
164
|
+
const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
|
|
165
|
+
return updated.sort((a, b) => {
|
|
166
|
+
if (a.pinned && !b.pinned) return -1;
|
|
167
|
+
if (!a.pinned && b.pinned) return 1;
|
|
168
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
} catch { /* silent */ }
|
|
172
|
+
}, [projectName]);
|
|
173
|
+
|
|
124
174
|
// Filter sessions by search query
|
|
125
175
|
const filteredSessions = searchQuery.trim()
|
|
126
176
|
? sessions.filter((s) => (s.title || "").toLowerCase().includes(searchQuery.toLowerCase()))
|
|
@@ -167,8 +217,8 @@ export function ChatHistoryBar({
|
|
|
167
217
|
title="Usage limits"
|
|
168
218
|
>
|
|
169
219
|
<Activity className="size-3" />
|
|
170
|
-
{usageInfo.activeAccountLabel && (
|
|
171
|
-
<span className="text-text-secondary font-normal truncate max-w-[60px]">[{usageInfo.activeAccountLabel}]</span>
|
|
220
|
+
{(streamingAccountLabel || usageInfo.activeAccountLabel) && (
|
|
221
|
+
<span className="text-text-secondary font-normal truncate max-w-[60px]">[{streamingAccountLabel || usageInfo.activeAccountLabel}]</span>
|
|
172
222
|
)}
|
|
173
223
|
<span>5h:{fiveHourPct != null ? `${fiveHourPct}%` : "--%"}</span>
|
|
174
224
|
<span className="text-text-subtle">·</span>
|
|
@@ -195,6 +245,11 @@ export function ChatHistoryBar({
|
|
|
195
245
|
</button>
|
|
196
246
|
)}
|
|
197
247
|
|
|
248
|
+
{/* Debug info — copy session IDs + JSONL path */}
|
|
249
|
+
{sessionId && (
|
|
250
|
+
<DebugCopyButton sessionId={sessionId} projectName={projectName} />
|
|
251
|
+
)}
|
|
252
|
+
|
|
198
253
|
{/* Connection indicator */}
|
|
199
254
|
{onReconnect && (
|
|
200
255
|
<button
|
|
@@ -277,9 +332,20 @@ export function ChatHistoryBar({
|
|
|
277
332
|
>
|
|
278
333
|
{session.title || "Untitled"}
|
|
279
334
|
</button>
|
|
335
|
+
<button
|
|
336
|
+
onClick={(e) => togglePin(e, session)}
|
|
337
|
+
className={`p-0.5 rounded transition-all ${
|
|
338
|
+
session.pinned
|
|
339
|
+
? "text-primary hover:text-primary/70"
|
|
340
|
+
: "text-text-subtle hover:text-text-secondary md:opacity-0 md:group-hover:opacity-100"
|
|
341
|
+
}`}
|
|
342
|
+
title={session.pinned ? "Unpin session" : "Pin session"}
|
|
343
|
+
>
|
|
344
|
+
{session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
345
|
+
</button>
|
|
280
346
|
<button
|
|
281
347
|
onClick={(e) => startEditing(session, e)}
|
|
282
|
-
className="p-0.5 rounded text-text-subtle hover:text-text-secondary opacity-0 group-hover:opacity-100 transition-opacity"
|
|
348
|
+
className="p-0.5 rounded text-text-subtle hover:text-text-secondary md:opacity-0 md:group-hover:opacity-100 transition-opacity"
|
|
283
349
|
title="Rename session"
|
|
284
350
|
>
|
|
285
351
|
<Pencil className="size-3" />
|
|
@@ -287,7 +353,7 @@ export function ChatHistoryBar({
|
|
|
287
353
|
</>
|
|
288
354
|
)}
|
|
289
355
|
{editingId !== session.id && session.updatedAt && (
|
|
290
|
-
<span className="text-[10px] text-text-subtle shrink-0">{formatDate(session.updatedAt)}</span>
|
|
356
|
+
<span className="text-[10px] text-text-subtle shrink-0 w-10 text-right">{formatDate(session.updatedAt)}</span>
|
|
291
357
|
)}
|
|
292
358
|
</div>
|
|
293
359
|
))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
-
import { Upload, X } from "lucide-react";
|
|
2
|
+
import { Loader2, Upload, X } from "lucide-react";
|
|
3
3
|
import { api, projectUrl } from "@/lib/api-client";
|
|
4
4
|
import { useChat } from "@/hooks/use-chat";
|
|
5
5
|
import { useUsage } from "@/hooks/use-usage";
|
|
@@ -14,6 +14,7 @@ import { MessageInput, type ChatAttachment } 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";
|
|
17
|
+
import { ChatWelcome } from "./chat-welcome";
|
|
17
18
|
import type { DragEvent } from "react";
|
|
18
19
|
import type { FileNode } from "../../../types/project";
|
|
19
20
|
import type { Session, SessionInfo } from "../../../types/chat";
|
|
@@ -83,12 +84,13 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
83
84
|
messages,
|
|
84
85
|
messagesLoading,
|
|
85
86
|
isStreaming,
|
|
86
|
-
|
|
87
|
+
phase,
|
|
88
|
+
isReconnecting,
|
|
87
89
|
connectingElapsed,
|
|
88
|
-
thinkingWarningThreshold,
|
|
89
90
|
pendingApproval,
|
|
90
91
|
contextWindowPct,
|
|
91
92
|
sessionTitle,
|
|
93
|
+
streamingAccountLabel,
|
|
92
94
|
sendMessage,
|
|
93
95
|
respondToApproval,
|
|
94
96
|
cancelStreaming,
|
|
@@ -311,19 +313,32 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
311
313
|
</div>
|
|
312
314
|
)}
|
|
313
315
|
|
|
314
|
-
{/*
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
316
|
+
{/* Reconnect overlay */}
|
|
317
|
+
{isReconnecting && (
|
|
318
|
+
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background/60 backdrop-blur-sm">
|
|
319
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
320
|
+
<Loader2 className="size-4 animate-spin" />
|
|
321
|
+
<span>Reconnecting...</span>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
{/* Messages or Welcome screen */}
|
|
327
|
+
{!sessionId ? (
|
|
328
|
+
<ChatWelcome projectName={projectName} onSelectSession={handleSelectSession} />
|
|
329
|
+
) : (
|
|
330
|
+
<MessageList
|
|
331
|
+
messages={messages}
|
|
332
|
+
messagesLoading={messagesLoading}
|
|
333
|
+
pendingApproval={pendingApproval}
|
|
334
|
+
onApprovalResponse={respondToApproval}
|
|
335
|
+
isStreaming={isStreaming}
|
|
336
|
+
phase={phase}
|
|
337
|
+
connectingElapsed={connectingElapsed}
|
|
338
|
+
projectName={projectName}
|
|
339
|
+
onFork={!isStreaming ? handleFork : undefined}
|
|
340
|
+
/>
|
|
341
|
+
)}
|
|
327
342
|
|
|
328
343
|
{/* Bottom toolbar */}
|
|
329
344
|
<div className="border-t border-border bg-background shrink-0">
|
|
@@ -339,6 +354,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
339
354
|
onSelectSession={handleSelectSession}
|
|
340
355
|
onBugReport={sessionId ? () => openBugReportPopup(version, { sessionId, projectName }) : undefined}
|
|
341
356
|
isConnected={isConnected}
|
|
357
|
+
streamingAccountLabel={streamingAccountLabel}
|
|
342
358
|
onReconnect={() => {
|
|
343
359
|
if (!isConnected) reconnect();
|
|
344
360
|
refetchMessages();
|