@hienlh/ppm 0.9.0-beta.3 → 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 +9 -44
- 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-D3VJc1tY.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-D5vGZJnH.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-DcGMlbRm.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-DHMITI3S.js → terminal-tab-MRg8y1xF.js} +1 -1
- 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 +85 -212
- 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 +91 -106
- package/src/services/chat.service.ts +10 -15
- package/src/types/api.ts +1 -1
- package/src/types/chat.ts +21 -4
- 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 +17 -5
- package/src/web/components/chat/message-input.tsx +94 -43
- 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 +20 -21
- 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-DxkvWelV.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-qlwORhh0.js +0 -1
- package/dist/web/assets/git-graph-B2fHtKEc.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-Ccq6zi2E.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-e3pqlQbf.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-CZzbMFtb.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
- package/dist/web/assets/settings-tab-BOmLAhkD.js +0 -1
- package/dist/web/assets/sqlite-viewer-CrrzHXqq.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/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
|
@@ -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,
|
|
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";
|
|
@@ -20,10 +22,8 @@ export interface ChatAttachment {
|
|
|
20
22
|
status: "uploading" | "ready" | "error";
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export type MessagePriority = 'now' | 'next' | 'later';
|
|
24
|
-
|
|
25
25
|
interface MessageInputProps {
|
|
26
|
-
onSend: (content: string, attachments: ChatAttachment[]
|
|
26
|
+
onSend: (content: string, attachments: ChatAttachment[]) => void;
|
|
27
27
|
isStreaming?: boolean;
|
|
28
28
|
onCancel?: () => void;
|
|
29
29
|
disabled?: boolean;
|
|
@@ -46,6 +46,10 @@ interface MessageInputProps {
|
|
|
46
46
|
permissionMode?: string;
|
|
47
47
|
/** Permission mode change handler */
|
|
48
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;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export const MessageInput = memo(function MessageInput({
|
|
@@ -65,18 +69,54 @@ export const MessageInput = memo(function MessageInput({
|
|
|
65
69
|
autoFocus,
|
|
66
70
|
permissionMode,
|
|
67
71
|
onModeChange,
|
|
72
|
+
providerId,
|
|
73
|
+
onProviderChange,
|
|
68
74
|
}: MessageInputProps) {
|
|
69
75
|
const [value, setValue] = useState(initialValue ?? "");
|
|
70
76
|
const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
|
|
71
77
|
const [modeSelectorOpen, setModeSelectorOpen] = useState(false);
|
|
72
78
|
const [pendingSend, setPendingSend] = useState(false);
|
|
73
|
-
const [priority, setPriority] = useState<MessagePriority>('next');
|
|
74
79
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
75
80
|
const mobileTextareaRef = useRef<HTMLTextAreaElement>(null);
|
|
76
81
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
77
82
|
const slashItemsRef = useRef<SlashItem[]>([]);
|
|
78
83
|
const fileItemsRef = useRef<FileNode[]>([]);
|
|
79
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
|
+
|
|
80
120
|
// Apply initialValue when it changes (e.g. "Ask AI" from command palette)
|
|
81
121
|
useEffect(() => {
|
|
82
122
|
if (initialValue) {
|
|
@@ -289,7 +329,7 @@ export const MessageInput = memo(function MessageInput({
|
|
|
289
329
|
|
|
290
330
|
onSlashStateChange?.(false, "");
|
|
291
331
|
onFileStateChange?.(false, "");
|
|
292
|
-
onSend(trimmed, readyAttachments
|
|
332
|
+
onSend(trimmed, readyAttachments);
|
|
293
333
|
setValue("");
|
|
294
334
|
// Revoke preview URLs
|
|
295
335
|
for (const att of attachments) {
|
|
@@ -297,10 +337,9 @@ export const MessageInput = memo(function MessageInput({
|
|
|
297
337
|
}
|
|
298
338
|
setAttachments([]);
|
|
299
339
|
setPendingSend(false);
|
|
300
|
-
setPriority('next');
|
|
301
340
|
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
302
341
|
if (mobileTextareaRef.current) mobileTextareaRef.current.style.height = "auto";
|
|
303
|
-
}, [value, attachments, onSend, onSlashStateChange, onFileStateChange
|
|
342
|
+
}, [value, attachments, onSend, onSlashStateChange, onFileStateChange]);
|
|
304
343
|
|
|
305
344
|
const handleSend = useCallback(() => {
|
|
306
345
|
if (disabled) return;
|
|
@@ -456,7 +495,7 @@ export const MessageInput = memo(function MessageInput({
|
|
|
456
495
|
>
|
|
457
496
|
{/* Attachment chips (inside container, aligned with input) */}
|
|
458
497
|
<AttachmentChips attachments={attachments} onRemove={removeAttachment} />
|
|
459
|
-
{/* Mobile: mode chip row */}
|
|
498
|
+
{/* Mobile: mode chip + provider selector row */}
|
|
460
499
|
<div className="flex items-center gap-1 px-2 pt-2 md:hidden relative">
|
|
461
500
|
<ModeChip
|
|
462
501
|
mode={permissionMode ?? "bypassPermissions"}
|
|
@@ -468,9 +507,15 @@ export const MessageInput = memo(function MessageInput({
|
|
|
468
507
|
open={modeSelectorOpen}
|
|
469
508
|
onOpenChange={setModeSelectorOpen}
|
|
470
509
|
/>
|
|
471
|
-
{
|
|
510
|
+
{onProviderChange && projectName && (
|
|
511
|
+
<ProviderSelector
|
|
512
|
+
value={providerId ?? "claude"}
|
|
513
|
+
onChange={onProviderChange}
|
|
514
|
+
projectName={projectName}
|
|
515
|
+
/>
|
|
516
|
+
)}
|
|
472
517
|
</div>
|
|
473
|
-
{/* Mobile: single row — attach + textarea + send */}
|
|
518
|
+
{/* Mobile: single row — attach + mic + textarea + send */}
|
|
474
519
|
<div className="flex items-end gap-1 md:hidden px-2 py-2">
|
|
475
520
|
<button
|
|
476
521
|
type="button"
|
|
@@ -481,6 +526,21 @@ export const MessageInput = memo(function MessageInput({
|
|
|
481
526
|
>
|
|
482
527
|
<Paperclip className="size-4" />
|
|
483
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
|
+
)}
|
|
484
544
|
<textarea
|
|
485
545
|
ref={mobileTextareaRef}
|
|
486
546
|
value={value}
|
|
@@ -540,6 +600,21 @@ export const MessageInput = memo(function MessageInput({
|
|
|
540
600
|
>
|
|
541
601
|
<Paperclip className="size-4" />
|
|
542
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
|
+
)}
|
|
543
618
|
{/* Mode indicator chip */}
|
|
544
619
|
<div className="relative">
|
|
545
620
|
<ModeChip
|
|
@@ -553,7 +628,14 @@ export const MessageInput = memo(function MessageInput({
|
|
|
553
628
|
onOpenChange={setModeSelectorOpen}
|
|
554
629
|
/>
|
|
555
630
|
</div>
|
|
556
|
-
{
|
|
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
|
+
)}
|
|
557
639
|
</div>
|
|
558
640
|
<div className="flex items-center gap-1">
|
|
559
641
|
{showCancel ? (
|
|
@@ -600,34 +682,3 @@ function ModeChip({ mode, onClick }: { mode: string; onClick: () => void }) {
|
|
|
600
682
|
</button>
|
|
601
683
|
);
|
|
602
684
|
}
|
|
603
|
-
|
|
604
|
-
const PRIORITY_OPTIONS: { value: MessagePriority; label: string; Icon: typeof Zap }[] = [
|
|
605
|
-
{ value: 'now', label: 'Interrupt', Icon: Zap },
|
|
606
|
-
{ value: 'next', label: 'Queue', Icon: ListOrdered },
|
|
607
|
-
{ value: 'later', label: 'Later', Icon: Clock },
|
|
608
|
-
];
|
|
609
|
-
|
|
610
|
-
/** Compact priority toggle — visible only during streaming */
|
|
611
|
-
function PriorityToggle({ value, onChange }: { value: MessagePriority; onChange: (v: MessagePriority) => void }) {
|
|
612
|
-
const cycle = useCallback(() => {
|
|
613
|
-
const order: MessagePriority[] = ['next', 'later', 'now'];
|
|
614
|
-
const idx = order.indexOf(value);
|
|
615
|
-
onChange(order[(idx + 1) % order.length]!);
|
|
616
|
-
}, [value, onChange]);
|
|
617
|
-
|
|
618
|
-
const current = PRIORITY_OPTIONS.find((o) => o.value === value) ?? PRIORITY_OPTIONS[1]!;
|
|
619
|
-
const Icon = current.Icon;
|
|
620
|
-
|
|
621
|
-
return (
|
|
622
|
-
<button
|
|
623
|
-
type="button"
|
|
624
|
-
onClick={(e) => { e.stopPropagation(); cycle(); }}
|
|
625
|
-
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"
|
|
626
|
-
aria-label={`Message priority: ${current.label}`}
|
|
627
|
-
title={`Priority: ${current.label} (click to cycle)`}
|
|
628
|
-
>
|
|
629
|
-
<Icon className="size-3" />
|
|
630
|
-
<span>{current.label}</span>
|
|
631
|
-
</button>
|
|
632
|
-
);
|
|
633
|
-
}
|
|
@@ -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() {
|