@hienlh/ppm 0.9.0-beta.2 → 0.9.0-beta.4
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 +10 -26
- package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-COwDPZl_.js} +1 -1
- package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-DCb0mkTp.js} +1 -1
- package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CuUkz5gb.js} +1 -1
- package/dist/web/assets/{arc-C2Qaz-ch.js → arc-D0bJaFyD.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-BVEUkQYB.js} +1 -1
- package/dist/web/assets/arrow-left-C_j9Ki73.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-CU2t4NHJ.js} +1 -1
- package/dist/web/assets/browser-tab-BhTdeeZd.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-DzjR91sM.js} +1 -1
- package/dist/web/assets/channel-CKNZAqoN.js +1 -0
- package/dist/web/assets/chat-tab-ZiiUVOxM.js +7 -0
- package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-0YMkpW2S.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-Dp0pTM5r.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CuYKSUgJ.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-DvbvLUIN.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CcEW1AMZ.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cgt-qg75.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-JCLgVcaC.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-B82RP9ow.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-Pb-JMOgO.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-BRj-ZEvL.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-CBRPKraG.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-DNFj84V6.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BnPzQK-O.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-BRj25yO7.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BdXwVXjJ.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-LfXT4p8B.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-EdgQyTqa.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-D3thuSok.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-LdG7RqsM.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-BHFnnXOt.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DUmQrLsF.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-CsGTseUr.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-5W2emiq4.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-COdZIaX4.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +1 -0
- package/dist/web/assets/clone-DNDy9Sms.js +1 -0
- package/dist/web/assets/{code-editor-CQ7gq0Vj.js → code-editor-BRMOypkX.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-C1QJ6GPW.js} +1 -1
- package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-CWo8w9wK.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-Br4t5TRV.js} +1 -1
- package/dist/web/assets/database-viewer-CEoDpzPz.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-CkDC2uAj.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-NvhckwcA.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO--nUaNiyB.js} +1 -1
- package/dist/web/assets/{diff-viewer-BjtTemkK.js → diff-viewer-jDU2bcGj.js} +1 -1
- package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-DK4QEZYh.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-B9h_Ba-v.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BVlftqyZ.js} +1 -1
- package/dist/web/assets/git-graph-DMQzw4Sp.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js} +1 -1
- package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BbbiUImY.js} +1 -1
- package/dist/web/assets/index-B4Iz1Wbi.css +2 -0
- package/dist/web/assets/index-QiSWS6f-.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +2 -0
- package/dist/web/assets/input-DGlv6gt_.js +41 -0
- package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-DXomfd7J.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-cW7SMLa_.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-DFQXUZsc.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-BMUhjxqj.js} +1 -1
- package/dist/web/assets/keybindings-store-BplH-yiN.js +1 -0
- package/dist/web/assets/{line-DBLLF7lH.js → line--xyfYP3x.js} +1 -1
- package/dist/web/assets/{linear-BLFWatDe.js → linear-BdqW7iQu.js} +1 -1
- package/dist/web/assets/{markdown-renderer-BtPXdzTv.js → markdown-renderer-BCjJbGP8.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-BY8JfkE_.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-DIv-LMXG.js} +1 -1
- package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-CIoJK3nc.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-seSK40d1.js} +1 -1
- package/dist/web/assets/postgres-viewer-s0snZ9CL.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-BaRFqlsA.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-1WWjMQB_.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-DEGGYsk7.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BtRvoUTC.js} +1 -1
- package/dist/web/assets/{settings-store-Bbhg_ptG.js → settings-store-D3dJqGhB.js} +2 -2
- package/dist/web/assets/settings-tab-2YkgmrY0.js +1 -0
- package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-C16aO8tn.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +1 -0
- package/dist/web/assets/switch-mjGtIVDJ.js +1 -0
- package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DSz5PQI0.js} +1 -1
- package/dist/web/assets/{terminal-tab--Gw14HP3.js → terminal-tab-MRg8y1xF.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DrjxCpEM.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +1 -0
- package/dist/web/assets/{use-monaco-theme-DHbyUrzJ.js → use-monaco-theme-BQzvItNE.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-DfYFnniI.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-BRvXOVlG.js} +1 -1
- package/dist/web/index.html +12 -10
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +260 -7
- package/docs/codebase-summary.md +255 -95
- package/docs/project-changelog.md +88 -1
- package/docs/system-architecture.md +177 -12
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +9 -0
- package/src/providers/cli-provider-base.ts +238 -0
- package/src/providers/cursor-cli/cursor-event-mapper.ts +85 -0
- package/src/providers/cursor-cli/cursor-history.ts +207 -0
- package/src/providers/cursor-cli/cursor-provider.ts +146 -0
- package/src/providers/mock-provider.ts +1 -1
- package/src/providers/provider.interface.ts +1 -0
- package/src/providers/registry.ts +43 -4
- package/src/server/index.ts +8 -0
- package/src/server/routes/browser-preview.ts +89 -0
- package/src/server/routes/chat.ts +14 -3
- package/src/server/routes/settings.ts +14 -0
- package/src/server/ws/chat.ts +24 -8
- package/src/services/chat.service.ts +10 -15
- package/src/types/chat.ts +21 -2
- package/src/types/config.ts +33 -11
- package/src/utils/ndjson-line-parser.ts +36 -0
- package/src/web/components/browser/browser-tab.tsx +269 -0
- package/src/web/components/chat/chat-history-bar.tsx +49 -29
- package/src/web/components/chat/chat-tab.tsx +14 -2
- package/src/web/components/chat/message-input.tsx +91 -3
- package/src/web/components/chat/provider-selector.tsx +150 -0
- package/src/web/components/chat/session-picker.tsx +3 -1
- package/src/web/components/layout/command-palette.tsx +4 -0
- package/src/web/components/layout/editor-panel.tsx +1 -0
- package/src/web/components/layout/mobile-nav.tsx +2 -2
- package/src/web/components/layout/panel-layout.tsx +17 -1
- package/src/web/components/layout/tab-bar.tsx +2 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/settings/ai-settings-section.tsx +196 -137
- package/src/web/hooks/use-chat.ts +11 -0
- package/src/web/hooks/use-global-keybindings.ts +7 -0
- package/src/web/hooks/use-voice-input.ts +111 -0
- package/src/web/stores/keybindings-store.ts +1 -0
- package/src/web/stores/panel-store.ts +10 -10
- package/src/web/stores/tab-store.ts +2 -1
- package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
- package/dist/web/assets/channel-w7yboq56.js +0 -1
- package/dist/web/assets/chat-tab-DmF14O6G.js +0 -7
- package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
- package/dist/web/assets/clone-BSi6cgDh.js +0 -1
- package/dist/web/assets/database-viewer-B27aRtdQ.js +0 -1
- package/dist/web/assets/git-graph-BGXo0o-J.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
- package/dist/web/assets/index-BAioKo_2.css +0 -2
- package/dist/web/assets/index-CfClIVo2.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
- package/dist/web/assets/input-Brjz2Vv-.js +0 -41
- package/dist/web/assets/keybindings-store-nDbczFnq.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
- package/dist/web/assets/postgres-viewer-BMg-qFcO.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
- package/dist/web/assets/settings-tab-NPuwQHzs.js +0 -1
- package/dist/web/assets/sqlite-viewer-CAsUczio.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
- package/snapshot-state.md +0 -1526
- package/test-tokens.mjs +0 -212
- /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-icCZ-07C.js} +0 -0
- /package/dist/web/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
- /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-Bcg3QJBg.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-B-QQuWwK.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
- /package/dist/web/assets/{dist-T0Vhi0Mh.js → dist-CMmNEgEP.js} +0 -0
- /package/dist/web/assets/{dist-Cce3efmT.js → dist-Ckxnw5rl.js} +0 -0
- /package/dist/web/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-DvHDmeBe.js} +0 -0
- /package/dist/web/assets/{katex-Bbu770d9.js → katex-C3cZrCvP.js} +0 -0
- /package/dist/web/assets/{math-DwgHI-Cu.js → math-a44lmFDa.js} +0 -0
- /package/dist/web/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
- /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-CsoeaaUJ.js} +0 -0
- /package/dist/web/assets/{react-BGf7KNLk.js → react-BPIfZRKM.js} +0 -0
- /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-c4PR5shF.js} +0 -0
- /package/dist/web/assets/{src-BoSBNdA_.js → src-CLWraeNW.js} +0 -0
- /package/dist/web/assets/{table-Yo02WRH-.js → table-C9jDaRl2.js} +0 -0
- /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CENGyt_L.js} +0 -0
- /package/dist/web/assets/{utils-btZ8C8-R.js → utils-Bslrbb-G.js} +0 -0
|
@@ -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
|
+
|
|
17
18
|
import type { DragEvent } from "react";
|
|
18
19
|
import type { FileNode } from "../../../types/project";
|
|
19
20
|
import type { Session, SessionInfo } from "../../../types/chat";
|
|
@@ -89,6 +90,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
89
90
|
pendingApproval,
|
|
90
91
|
contextWindowPct,
|
|
91
92
|
sessionTitle,
|
|
93
|
+
migratedSessionId,
|
|
92
94
|
sendMessage,
|
|
93
95
|
respondToApproval,
|
|
94
96
|
cancelStreaming,
|
|
@@ -97,6 +99,13 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
97
99
|
isConnected,
|
|
98
100
|
} = useChat(sessionId, providerId, projectName);
|
|
99
101
|
|
|
102
|
+
// When CLI provider assigns a different session ID, update our state
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (migratedSessionId && migratedSessionId !== sessionId) {
|
|
105
|
+
setSessionId(migratedSessionId);
|
|
106
|
+
}
|
|
107
|
+
}, [migratedSessionId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
108
|
+
|
|
100
109
|
// Auto-clear notification badge when this tab is active and document is visible.
|
|
101
110
|
// Handles the case where notification arrived while browser tab was hidden.
|
|
102
111
|
useEffect(() => {
|
|
@@ -140,11 +149,11 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
140
149
|
useTabStore.getState().openTab({
|
|
141
150
|
type: "chat",
|
|
142
151
|
title: "AI Chat",
|
|
143
|
-
metadata: { projectName },
|
|
152
|
+
metadata: { projectName, providerId },
|
|
144
153
|
projectId: projectName || null,
|
|
145
154
|
closable: true,
|
|
146
155
|
});
|
|
147
|
-
}, [projectName]);
|
|
156
|
+
}, [projectName, providerId]);
|
|
148
157
|
|
|
149
158
|
const handleSelectSession = useCallback((session: SessionInfo) => {
|
|
150
159
|
setSessionId(session.id);
|
|
@@ -345,6 +354,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
345
354
|
refreshUsage={refreshUsage}
|
|
346
355
|
lastFetchedAt={lastFetchedAt}
|
|
347
356
|
sessionId={sessionId}
|
|
357
|
+
providerId={providerId}
|
|
348
358
|
onSelectSession={handleSelectSession}
|
|
349
359
|
onBugReport={sessionId ? () => openBugReportPopup(version, { sessionId, projectName }) : undefined}
|
|
350
360
|
isConnected={isConnected}
|
|
@@ -386,6 +396,8 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
386
396
|
externalFiles={externalFiles}
|
|
387
397
|
permissionMode={permissionMode}
|
|
388
398
|
onModeChange={setPermissionMode}
|
|
399
|
+
providerId={providerId}
|
|
400
|
+
onProviderChange={!sessionId ? setProviderId : undefined}
|
|
389
401
|
/>
|
|
390
402
|
</div>
|
|
391
403
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect, memo, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react";
|
|
2
|
-
import { ArrowUp, Square, Paperclip, Loader2 } from "lucide-react";
|
|
2
|
+
import { ArrowUp, Square, Paperclip, Loader2, Mic, MicOff } from "lucide-react";
|
|
3
|
+
import { useVoiceInput } from "@/hooks/use-voice-input";
|
|
3
4
|
import { api, projectUrl, getAuthToken } from "@/lib/api-client";
|
|
4
5
|
import { randomId } from "@/lib/utils";
|
|
5
6
|
import { isSupportedFile, isImageFile } from "@/lib/file-support";
|
|
6
7
|
import { AttachmentChips } from "./attachment-chips";
|
|
7
8
|
import { ModeSelector, getModeLabel, getModeIcon } from "./mode-selector";
|
|
9
|
+
import { ProviderSelector } from "./provider-selector";
|
|
8
10
|
import type { SlashItem } from "./slash-command-picker";
|
|
9
11
|
import type { FileNode } from "../../../types/project";
|
|
10
12
|
import { flattenFileTree } from "./file-picker";
|
|
@@ -44,6 +46,10 @@ interface MessageInputProps {
|
|
|
44
46
|
permissionMode?: string;
|
|
45
47
|
/** Permission mode change handler */
|
|
46
48
|
onModeChange?: (mode: string) => void;
|
|
49
|
+
/** Current provider ID */
|
|
50
|
+
providerId?: string;
|
|
51
|
+
/** Provider change handler — undefined when session is active (locked) */
|
|
52
|
+
onProviderChange?: (providerId: string) => void;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
export const MessageInput = memo(function MessageInput({
|
|
@@ -63,6 +69,8 @@ export const MessageInput = memo(function MessageInput({
|
|
|
63
69
|
autoFocus,
|
|
64
70
|
permissionMode,
|
|
65
71
|
onModeChange,
|
|
72
|
+
providerId,
|
|
73
|
+
onProviderChange,
|
|
66
74
|
}: MessageInputProps) {
|
|
67
75
|
const [value, setValue] = useState(initialValue ?? "");
|
|
68
76
|
const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
|
|
@@ -74,6 +82,41 @@ export const MessageInput = memo(function MessageInput({
|
|
|
74
82
|
const slashItemsRef = useRef<SlashItem[]>([]);
|
|
75
83
|
const fileItemsRef = useRef<FileNode[]>([]);
|
|
76
84
|
|
|
85
|
+
// Voice input (Web Speech API)
|
|
86
|
+
const voice = useVoiceInput();
|
|
87
|
+
// Store pre-voice text so voice appends to existing input
|
|
88
|
+
const preVoiceTextRef = useRef("");
|
|
89
|
+
const voiceResultCb = useCallback((text: string) => {
|
|
90
|
+
const prefix = preVoiceTextRef.current;
|
|
91
|
+
const newValue = prefix ? prefix + " " + text : text;
|
|
92
|
+
setValue(newValue);
|
|
93
|
+
// Auto-resize textarea
|
|
94
|
+
requestAnimationFrame(() => {
|
|
95
|
+
const ta = window.matchMedia("(min-width: 768px)").matches
|
|
96
|
+
? textareaRef.current
|
|
97
|
+
: mobileTextareaRef.current;
|
|
98
|
+
if (ta) {
|
|
99
|
+
ta.style.height = "auto";
|
|
100
|
+
ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}, []);
|
|
104
|
+
const handleVoiceToggle = useCallback(() => {
|
|
105
|
+
if (voice.isListening) {
|
|
106
|
+
voice.stop();
|
|
107
|
+
} else {
|
|
108
|
+
preVoiceTextRef.current = value.trim();
|
|
109
|
+
voice.start(voiceResultCb);
|
|
110
|
+
}
|
|
111
|
+
}, [voice.isListening, voice.start, voice.stop, value, voiceResultCb]);
|
|
112
|
+
|
|
113
|
+
// Listen for global keyboard shortcut (Cmd+Shift+V) to toggle voice
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const handler = () => { if (voice.supported) handleVoiceToggle(); };
|
|
116
|
+
window.addEventListener("toggle-voice-input", handler);
|
|
117
|
+
return () => window.removeEventListener("toggle-voice-input", handler);
|
|
118
|
+
}, [voice.supported, handleVoiceToggle]);
|
|
119
|
+
|
|
77
120
|
// Apply initialValue when it changes (e.g. "Ask AI" from command palette)
|
|
78
121
|
useEffect(() => {
|
|
79
122
|
if (initialValue) {
|
|
@@ -452,7 +495,7 @@ export const MessageInput = memo(function MessageInput({
|
|
|
452
495
|
>
|
|
453
496
|
{/* Attachment chips (inside container, aligned with input) */}
|
|
454
497
|
<AttachmentChips attachments={attachments} onRemove={removeAttachment} />
|
|
455
|
-
{/* Mobile: mode chip row */}
|
|
498
|
+
{/* Mobile: mode chip + provider selector row */}
|
|
456
499
|
<div className="flex items-center gap-1 px-2 pt-2 md:hidden relative">
|
|
457
500
|
<ModeChip
|
|
458
501
|
mode={permissionMode ?? "bypassPermissions"}
|
|
@@ -464,8 +507,15 @@ export const MessageInput = memo(function MessageInput({
|
|
|
464
507
|
open={modeSelectorOpen}
|
|
465
508
|
onOpenChange={setModeSelectorOpen}
|
|
466
509
|
/>
|
|
510
|
+
{onProviderChange && projectName && (
|
|
511
|
+
<ProviderSelector
|
|
512
|
+
value={providerId ?? "claude"}
|
|
513
|
+
onChange={onProviderChange}
|
|
514
|
+
projectName={projectName}
|
|
515
|
+
/>
|
|
516
|
+
)}
|
|
467
517
|
</div>
|
|
468
|
-
{/* Mobile: single row — attach + textarea + send */}
|
|
518
|
+
{/* Mobile: single row — attach + mic + textarea + send */}
|
|
469
519
|
<div className="flex items-end gap-1 md:hidden px-2 py-2">
|
|
470
520
|
<button
|
|
471
521
|
type="button"
|
|
@@ -476,6 +526,21 @@ export const MessageInput = memo(function MessageInput({
|
|
|
476
526
|
>
|
|
477
527
|
<Paperclip className="size-4" />
|
|
478
528
|
</button>
|
|
529
|
+
{voice.supported && (
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
|
|
533
|
+
disabled={disabled}
|
|
534
|
+
className={`flex items-center justify-center size-7 shrink-0 rounded-full transition-colors disabled:opacity-50 ${
|
|
535
|
+
voice.isListening
|
|
536
|
+
? "bg-red-600 text-white animate-pulse"
|
|
537
|
+
: "text-text-subtle hover:text-text-primary"
|
|
538
|
+
}`}
|
|
539
|
+
aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
|
|
540
|
+
>
|
|
541
|
+
{voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
|
|
542
|
+
</button>
|
|
543
|
+
)}
|
|
479
544
|
<textarea
|
|
480
545
|
ref={mobileTextareaRef}
|
|
481
546
|
value={value}
|
|
@@ -535,6 +600,21 @@ export const MessageInput = memo(function MessageInput({
|
|
|
535
600
|
>
|
|
536
601
|
<Paperclip className="size-4" />
|
|
537
602
|
</button>
|
|
603
|
+
{voice.supported && (
|
|
604
|
+
<button
|
|
605
|
+
type="button"
|
|
606
|
+
onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
|
|
607
|
+
disabled={disabled}
|
|
608
|
+
className={`flex items-center justify-center size-8 rounded-full transition-colors disabled:opacity-50 ${
|
|
609
|
+
voice.isListening
|
|
610
|
+
? "bg-red-600 text-white animate-pulse"
|
|
611
|
+
: "text-text-subtle hover:text-text-primary hover:bg-surface-elevated"
|
|
612
|
+
}`}
|
|
613
|
+
aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
|
|
614
|
+
>
|
|
615
|
+
{voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
|
|
616
|
+
</button>
|
|
617
|
+
)}
|
|
538
618
|
{/* Mode indicator chip */}
|
|
539
619
|
<div className="relative">
|
|
540
620
|
<ModeChip
|
|
@@ -548,6 +628,14 @@ export const MessageInput = memo(function MessageInput({
|
|
|
548
628
|
onOpenChange={setModeSelectorOpen}
|
|
549
629
|
/>
|
|
550
630
|
</div>
|
|
631
|
+
{/* Provider selector — only when no active session */}
|
|
632
|
+
{onProviderChange && projectName && (
|
|
633
|
+
<ProviderSelector
|
|
634
|
+
value={providerId ?? "claude"}
|
|
635
|
+
onChange={onProviderChange}
|
|
636
|
+
projectName={projectName}
|
|
637
|
+
/>
|
|
638
|
+
)}
|
|
551
639
|
</div>
|
|
552
640
|
<div className="flex items-center gap-1">
|
|
553
641
|
{showCancel ? (
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, type KeyboardEvent } from "react";
|
|
2
|
+
import { Check } from "lucide-react";
|
|
3
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
4
|
+
|
|
5
|
+
interface ProviderInfo {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ProviderSelectorProps {
|
|
11
|
+
value: string;
|
|
12
|
+
onChange: (providerId: string) => void;
|
|
13
|
+
projectName: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PROVIDER_ICONS: Record<string, string> = {
|
|
17
|
+
claude: "C",
|
|
18
|
+
cursor: "▶",
|
|
19
|
+
codex: "◆",
|
|
20
|
+
gemini: "G",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Provider selector chip + popup — matches ModeSelector style.
|
|
25
|
+
* Hidden when only 1 provider available.
|
|
26
|
+
*/
|
|
27
|
+
export function ProviderSelector({ value, onChange, projectName }: ProviderSelectorProps) {
|
|
28
|
+
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
29
|
+
const [open, setOpen] = useState(false);
|
|
30
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
const focusedRef = useRef(0);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!projectName) return;
|
|
35
|
+
api.get<ProviderInfo[]>(`${projectUrl(projectName)}/chat/providers`)
|
|
36
|
+
.then(setProviders)
|
|
37
|
+
.catch(() => {});
|
|
38
|
+
}, [projectName]);
|
|
39
|
+
|
|
40
|
+
// Close on click outside
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!open) return;
|
|
43
|
+
const handler = (e: MouseEvent) => {
|
|
44
|
+
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
|
|
45
|
+
setOpen(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
document.addEventListener("mousedown", handler);
|
|
49
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
50
|
+
}, [open]);
|
|
51
|
+
|
|
52
|
+
// Focus current on open
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (open) {
|
|
55
|
+
focusedRef.current = Math.max(0, providers.findIndex((p) => p.id === value));
|
|
56
|
+
}
|
|
57
|
+
}, [open, value, providers]);
|
|
58
|
+
|
|
59
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
60
|
+
if (e.key === "Escape") { setOpen(false); return; }
|
|
61
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
const dir = e.key === "ArrowDown" ? 1 : -1;
|
|
64
|
+
focusedRef.current = (focusedRef.current + dir + providers.length) % providers.length;
|
|
65
|
+
const el = panelRef.current?.querySelector(`[data-idx="${focusedRef.current}"]`) as HTMLElement;
|
|
66
|
+
el?.focus();
|
|
67
|
+
}
|
|
68
|
+
if (e.key === "Enter") {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
const p = providers[focusedRef.current];
|
|
71
|
+
if (p) { onChange(p.id); setOpen(false); }
|
|
72
|
+
}
|
|
73
|
+
}, [onChange, providers]);
|
|
74
|
+
|
|
75
|
+
// Hide when only 1 provider
|
|
76
|
+
if (providers.length <= 1) return null;
|
|
77
|
+
|
|
78
|
+
const current = providers.find((p) => p.id === value);
|
|
79
|
+
const icon = PROVIDER_ICONS[value] || "?";
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="relative">
|
|
83
|
+
{/* Chip — same style as ModeChip */}
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={(e) => { e.stopPropagation(); setOpen((v) => !v); }}
|
|
87
|
+
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"
|
|
88
|
+
aria-label={`AI Provider: ${current?.name ?? value}`}
|
|
89
|
+
>
|
|
90
|
+
<span className="inline-flex h-3.5 w-3.5 items-center justify-center rounded text-[9px] font-bold bg-surface-elevated shrink-0">
|
|
91
|
+
{icon}
|
|
92
|
+
</span>
|
|
93
|
+
<span className="max-w-[80px] truncate capitalize">{current?.name ?? value}</span>
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
{/* Popup panel — same style as ModeSelector */}
|
|
97
|
+
{open && (
|
|
98
|
+
<div
|
|
99
|
+
ref={panelRef}
|
|
100
|
+
role="listbox"
|
|
101
|
+
aria-label="AI Providers"
|
|
102
|
+
onKeyDown={handleKeyDown}
|
|
103
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
104
|
+
onClick={(e) => e.stopPropagation()}
|
|
105
|
+
className="absolute bottom-full left-0 mb-1 z-50 w-56 rounded-lg border border-border bg-surface shadow-lg"
|
|
106
|
+
>
|
|
107
|
+
<div className="px-3 py-2 border-b border-border">
|
|
108
|
+
<span className="text-xs font-medium text-text-secondary">Provider</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="py-1">
|
|
111
|
+
{providers.map((p, idx) => {
|
|
112
|
+
const pIcon = PROVIDER_ICONS[p.id] || "?";
|
|
113
|
+
const isActive = p.id === value;
|
|
114
|
+
return (
|
|
115
|
+
<button
|
|
116
|
+
key={p.id}
|
|
117
|
+
data-idx={idx}
|
|
118
|
+
role="option"
|
|
119
|
+
aria-selected={isActive}
|
|
120
|
+
tabIndex={0}
|
|
121
|
+
onClick={() => { onChange(p.id); setOpen(false); }}
|
|
122
|
+
className={`w-full flex items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-surface-elevated focus:bg-surface-elevated focus:outline-none ${isActive ? "bg-surface-elevated" : ""}`}
|
|
123
|
+
>
|
|
124
|
+
<span className="inline-flex h-5 w-5 items-center justify-center rounded text-[11px] font-bold bg-surface-elevated text-text-subtle shrink-0">
|
|
125
|
+
{pIcon}
|
|
126
|
+
</span>
|
|
127
|
+
<span className="flex-1 text-sm font-medium text-text-primary capitalize">{p.name}</span>
|
|
128
|
+
{isActive && <Check className="size-4 shrink-0 text-primary" />}
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Small provider badge for session lists */
|
|
140
|
+
export function ProviderBadge({ providerId }: { providerId: string }) {
|
|
141
|
+
const icon = PROVIDER_ICONS[providerId] || "?";
|
|
142
|
+
return (
|
|
143
|
+
<span
|
|
144
|
+
className="inline-flex h-4 w-4 items-center justify-center rounded text-[10px] font-bold bg-surface-elevated text-text-subtle shrink-0"
|
|
145
|
+
title={providerId}
|
|
146
|
+
>
|
|
147
|
+
{icon}
|
|
148
|
+
</span>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { api, projectUrl } from "@/lib/api-client";
|
|
3
3
|
import { Plus, Trash2, MessageSquare, ChevronDown } from "lucide-react";
|
|
4
|
+
import { ProviderBadge } from "./provider-selector";
|
|
4
5
|
import type { SessionInfo } from "../../../types/chat";
|
|
5
6
|
|
|
6
7
|
interface SessionPickerProps {
|
|
@@ -114,7 +115,8 @@ export function SessionPicker({
|
|
|
114
115
|
}`}
|
|
115
116
|
>
|
|
116
117
|
<div className="flex flex-col min-w-0 flex-1">
|
|
117
|
-
<span className="truncate text-xs font-medium">
|
|
118
|
+
<span className="flex items-center gap-1.5 truncate text-xs font-medium">
|
|
119
|
+
<ProviderBadge providerId={session.providerId} />
|
|
118
120
|
{session.title}
|
|
119
121
|
</span>
|
|
120
122
|
<span className="text-xs text-text-subtle">
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
FileCode,
|
|
11
11
|
FolderOpen,
|
|
12
12
|
Loader2,
|
|
13
|
+
Globe,
|
|
14
|
+
Mic,
|
|
13
15
|
} from "lucide-react";
|
|
14
16
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
15
17
|
import { useProjectStore } from "@/stores/project-store";
|
|
@@ -156,7 +158,9 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
156
158
|
{ id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
|
|
157
159
|
{ id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
|
|
158
160
|
{ id: "git-graph", label: "Git Graph", icon: GitBranch, action: openNewTab("git-graph", "Git Graph"), keywords: "branch history log", group: "action", shortcut: formatShortcut(getBinding("open-git-graph")) },
|
|
161
|
+
{ id: "browser", label: "Open Browser", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "web preview localhost iframe url", group: "action" },
|
|
159
162
|
{ id: "postgres", label: "PostgreSQL", icon: Database, action: openNewTab("postgres", "PostgreSQL"), keywords: "database pg sql query", group: "action" },
|
|
163
|
+
{ id: "voice-input", label: "Voice Input", icon: Mic, action: () => { window.dispatchEvent(new CustomEvent("toggle-voice-input")); onClose(); }, keywords: "speech microphone dictate voice", group: "action", shortcut: formatShortcut(getBinding("voice-input")) },
|
|
160
164
|
{ id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action: () => { setSidebarActiveTab("git"); onClose(); }, keywords: "changes diff staged", group: "action", shortcut: formatShortcut(getBinding("open-git-status")) },
|
|
161
165
|
{
|
|
162
166
|
id: "settings", label: "Settings", icon: Settings,
|
|
@@ -23,6 +23,7 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
23
23
|
"git-graph": lazy(() => import("@/components/git/git-graph").then((m) => ({ default: m.GitGraph }))),
|
|
24
24
|
"git-diff": lazy(() => import("@/components/editor/diff-viewer").then((m) => ({ default: m.DiffViewer }))),
|
|
25
25
|
settings: lazy(() => import("@/components/settings/settings-tab").then((m) => ({ default: m.SettingsTab }))),
|
|
26
|
+
browser: lazy(() => import("@/components/browser/browser-tab").then((m) => ({ default: m.BrowserTab }))),
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
interface EditorPanelProps {
|
|
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
Terminal, MessageSquare, GitBranch, Database,
|
|
4
4
|
FileDiff, FileCode, Settings, Menu, X, ArrowLeft, ArrowRight, SplitSquareVertical, MoveVertical, Layers, Plus,
|
|
5
|
-
ChevronLeft, ChevronRight,
|
|
5
|
+
ChevronLeft, ChevronRight, Globe,
|
|
6
6
|
} from "lucide-react";
|
|
7
7
|
import { usePanelStore } from "@/stores/panel-store";
|
|
8
8
|
import { useProjectStore, resolveOrder } from "@/stores/project-store";
|
|
@@ -25,7 +25,7 @@ const NEW_TAB_LABELS: Partial<Record<TabType, string>> = Object.fromEntries(NEW_
|
|
|
25
25
|
|
|
26
26
|
const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
27
27
|
terminal: Terminal, chat: MessageSquare, editor: FileCode, database: Database, sqlite: Database, postgres: Database,
|
|
28
|
-
"git-graph": GitBranch, "git-diff": FileDiff, settings: Settings,
|
|
28
|
+
"git-graph": GitBranch, "git-diff": FileDiff, settings: Settings, browser: Globe,
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
interface MobileNavProps { onMenuPress: () => void; onProjectsPress: () => void; }
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { Panel, Group, Separator } from "react-resizable-panels";
|
|
2
3
|
import { GripVertical, GripHorizontal } from "lucide-react";
|
|
3
4
|
import { usePanelStore } from "@/stores/panel-store";
|
|
5
|
+
import { createPanel } from "@/stores/panel-utils";
|
|
4
6
|
import { EditorPanel } from "./editor-panel";
|
|
5
7
|
|
|
6
8
|
interface PanelLayoutProps {
|
|
@@ -13,7 +15,21 @@ export function PanelLayout({ projectName }: PanelLayoutProps) {
|
|
|
13
15
|
);
|
|
14
16
|
const panelCount = grid.flat().length;
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
// Recover from empty grid (corrupt persisted state or edge-case bug)
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (panelCount === 0) {
|
|
21
|
+
const p = createPanel();
|
|
22
|
+
usePanelStore.setState((s) => ({
|
|
23
|
+
panels: { ...s.panels, [p.id]: p },
|
|
24
|
+
grid: [[p.id]],
|
|
25
|
+
focusedPanelId: p.id,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
}, [panelCount]);
|
|
29
|
+
|
|
30
|
+
if (panelCount === 0) return null;
|
|
31
|
+
|
|
32
|
+
if (panelCount === 1 && grid[0]?.[0]) {
|
|
17
33
|
return <EditorPanel panelId={grid[0][0]} projectName={projectName} />;
|
|
18
34
|
}
|
|
19
35
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Database,
|
|
11
11
|
ChevronLeft,
|
|
12
12
|
ChevronRight,
|
|
13
|
+
Globe,
|
|
13
14
|
} from "lucide-react";
|
|
14
15
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
15
16
|
import { usePanelStore } from "@/stores/panel-store";
|
|
@@ -33,6 +34,7 @@ const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
|
33
34
|
"git-graph": GitBranch,
|
|
34
35
|
"git-diff": FileDiff,
|
|
35
36
|
settings: Settings,
|
|
37
|
+
browser: Globe,
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
interface TabBarProps {
|
|
@@ -48,6 +48,11 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
48
48
|
default: m.SettingsTab,
|
|
49
49
|
})),
|
|
50
50
|
),
|
|
51
|
+
browser: lazy(() =>
|
|
52
|
+
import("@/components/browser/browser-tab").then((m) => ({
|
|
53
|
+
default: m.BrowserTab,
|
|
54
|
+
})),
|
|
55
|
+
),
|
|
51
56
|
};
|
|
52
57
|
|
|
53
58
|
function LoadingFallback() {
|