@hienlh/ppm 0.8.86 → 0.8.88
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 +216 -4
- package/bun.lock +5 -0
- package/dist/web/assets/{_basePickBy-5eBmZ_lt.js → _basePickBy-3Xe18azI.js} +1 -1
- package/dist/web/assets/{_baseUniq-DimLlN0y.js → _baseUniq-Yy35llnn.js} +1 -1
- package/dist/web/assets/api-settings-Dh4oFOpX.js +1 -0
- package/dist/web/assets/{arc-D4SasZrA.js → arc-B9n1Gvb5.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-nv0WbM7d.js → architectureDiagram-2XIMDMQ5-DqAZP_F6.js} +1 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-C1XvYrb8.js → blockDiagram-WCTKOSBZ-h3cDF2vI.js} +1 -1
- package/dist/web/assets/browser-tab-DJLH0eDY.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-CygDrbWJ.js → c4Diagram-IC4MRINW--pF1r5lr.js} +1 -1
- package/dist/web/assets/channel-C2fMafck.js +1 -0
- package/dist/web/assets/chat-tab-C8HFXqGS.js +8 -0
- package/dist/web/assets/chevron-right-CHnjJt4E.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C2FDgsgT.js → chunk-4BX2VUAB-C3aZvW7B.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-jF4w6cat.js → chunk-55IACEB6-D5cABeB9.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-BVCECZFi.js → chunk-7E7YKBS2-CkFGv6Zs.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DXTbeu5d.js → chunk-7R4GIKGN-Dvbyu4Zw.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-BaZqOsTs.js → chunk-C72U2L5F-CtqKiH4q.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Bky2tcH7.js → chunk-EGIJ26TM-Cpr87sBR.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-Cp4BK9A8.js → chunk-FMBD7UC4-D23YVTOU.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-BosFEH7G.js → chunk-GEFDOKGD-tDjHsAUs.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-H5Gbjsbr.js → chunk-JSJVCQXG-BBmymCjA.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-CWerSUwS.js → chunk-KX2RTZJC-DP36BDiU.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-FvwP7jUy.js → chunk-KYZI473N-Djw13C-3.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-D1PI_ORP.js → chunk-L3YUKLVL-HG_eMj_C.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C7Vzk_AI.js → chunk-MX3YWQON-C2UEioMs.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-BceYBGYX.js → chunk-NQ4KR5QH-DXUTQ-BL.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-WPtzgxql.js → chunk-O4XLMI2P-BsUWb9d0.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-DlHXDeLY.js → chunk-OZEHJAEY-rG0P22U9.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-Ci_Prygb.js → chunk-PQ6SQG4A-DX0xW7kO.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-CO0zMN-z.js → chunk-PU5JKC2W-C7Gry6md.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-IAEEzfpM.js → chunk-R5LLSJPH-CMY0PkRK.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-BLXalOgc.js → chunk-WL4C6EOR-CXuQvlyu.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Dx1Ri_p2.js → chunk-XIRO2GV7-DRJEb7Zb.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-m9pPGKn7.js → chunk-XPW4576I-BPEX8KhL.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-B_08ExbI.js → chunk-XZSTWKYB-Cb0iqycX.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-DqSOVcYe.js → chunk-YBOYWFTD-av5aeHLq.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +1 -0
- package/dist/web/assets/clone-B2hUek6n.js +1 -0
- package/dist/web/assets/code-editor-CaGdx-lS.js +2 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-DlL82QHu.js → cose-bilkent-S5V4N54A-qudEiMCT.js} +1 -1
- package/dist/web/assets/csv-preview-DUbHtTAS.js +10 -0
- package/dist/web/assets/{dagre-BmVoh2At.js → dagre-BFcnKyBF.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-sDrRW9MQ.js → dagre-KLK3FWXG-C3O-MTLf.js} +1 -1
- package/dist/web/assets/database-viewer-i4Ddk6mO.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-ChnAhgni.js → diagram-E7M64L7V-DxPjK7_c.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-DW1J1uJd.js → diagram-IFDJBPK2-sqTog_XV.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-CQ32hyG_.js → diagram-P4PSJMXO-hzmp0GHK.js} +1 -1
- package/dist/web/assets/diff-viewer-DQDS7yjv.js +4 -0
- package/dist/web/assets/dist-CALwEtco.js +41 -0
- package/dist/web/assets/dist-DGDPTxs1.js +13 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-6CHo6nOw.js → erDiagram-INFDFZHY-DLeYhAAT.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-DroDiNT0.js → flowDiagram-PKNHOUZH-CRxlE9Sr.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DP0QBh8w.js → ganttDiagram-A5KZAMGK-BdjmoMLS.js} +1 -1
- package/dist/web/assets/git-graph-DUs-TN1u.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js → gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js} +1 -1
- package/dist/web/assets/{graphlib-CQBb2thr.js → graphlib-Duh_bWLa.js} +1 -1
- package/dist/web/assets/index-DhtLEnPD.css +2 -0
- package/dist/web/assets/index-Dm6RN1A1.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +2 -0
- package/dist/web/assets/{isEmpty-B4kqZBtn.js → isEmpty-B9L-Ge-H.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-46yibrV5.js → ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-BcmRwjK-.js → journeyDiagram-4ABVD52K-CgDI-UG4.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-B619K53y.js → kanban-definition-K7BYSVSG-h4g10UHL.js} +1 -1
- package/dist/web/assets/keybindings-store-qVLDZz97.js +1 -0
- package/dist/web/assets/lib-BeaDXEkP.js +4 -0
- package/dist/web/assets/{line-1gcO63_w.js → line-B75-Rx70.js} +1 -1
- package/dist/web/assets/{linear-DfRqDoVd.js → linear-Bcjv9FQt.js} +1 -1
- package/dist/web/assets/markdown-renderer-L1NgC2Rw.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-XtjZQOeM.js → mermaid-parser.core-8u2leTXI.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-CifOFo_q.js → mindmap-definition-YRQLILUH-BaOBwb-W.js} +1 -1
- package/dist/web/assets/{ordinal-BJYw-iDX.js → ordinal-LFEjVtwQ.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-BuHUh_fO.js → pieDiagram-SKSYHLDU-At5Kz0KK.js} +1 -1
- package/dist/web/assets/postgres-viewer-_uDispGW.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-Bau_hj6Z.js → quadrantDiagram-337W2JSQ-CdjGIDfw.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +1 -0
- package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-Cq2b-uwp.js → requirementDiagram-Z7DCOOCP-B9F_Cx_p.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js → sankeyDiagram-WA2Y5GQK-RolPi8bU.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-qPxiTUcS.js → sequenceDiagram-2WXFIKYE-DM-tMAhx.js} +1 -1
- package/dist/web/assets/settings-tab-Bp4041i6.js +1 -0
- package/dist/web/assets/sqlite-viewer-GW-QCjHn.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-Dulj2oa8.js → stateDiagram-RAJIS63D-C4EMl6jf.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +1 -0
- package/dist/web/assets/tab-store--SlERlDs.js +1 -0
- package/dist/web/assets/{terminal-tab-wKgpSPAT.js → terminal-tab-E4cWujj4.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-BWyDnCYq.js → timeline-definition-YZTLITO2-A4PN_Efm.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +1 -0
- package/dist/web/assets/use-monaco-theme-zABXAAla.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-B9Iv2bNV.js → vennDiagram-LZ73GAT5-ywK7LMaH.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-ChXcMzBQ.js → xychartDiagram-JWTSCODW-DylHYNtJ.js} +1 -1
- package/dist/web/index.html +11 -11
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +386 -6
- package/docs/codebase-summary.md +270 -98
- package/docs/design-guidelines.md +21 -0
- package/docs/project-changelog.md +150 -1
- package/docs/project-roadmap.md +41 -19
- package/docs/system-architecture.md +363 -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 +316 -107
- 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 +7 -2
- package/src/providers/provider.interface.ts +1 -0
- package/src/providers/registry.ts +43 -4
- package/src/server/index.ts +44 -166
- package/src/server/routes/browser-preview.ts +159 -0
- package/src/server/routes/chat.ts +66 -6
- package/src/server/routes/mcp.ts +84 -0
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/proxy.ts +46 -53
- package/src/server/routes/settings.ts +14 -0
- package/src/server/routes/tunnel.ts +0 -32
- package/src/server/routes/workspace.ts +35 -0
- package/src/server/ws/chat.ts +302 -195
- package/src/services/account-selector.service.ts +16 -8
- package/src/services/account.service.ts +19 -13
- package/src/services/chat.service.ts +10 -15
- 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 +119 -6
- package/src/services/mcp-config.service.ts +102 -0
- package/src/services/proxy.service.ts +4 -19
- package/src/services/supervisor.ts +285 -25
- package/src/types/api.ts +10 -2
- package/src/types/chat.ts +25 -2
- package/src/types/config.ts +33 -11
- package/src/types/mcp.ts +47 -0
- package/src/utils/ndjson-line-parser.ts +36 -0
- 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 +116 -31
- package/src/web/components/chat/chat-tab.tsx +31 -10
- package/src/web/components/chat/chat-welcome.tsx +148 -0
- package/src/web/components/chat/message-input.tsx +169 -16
- package/src/web/components/chat/message-list.tsx +27 -15
- package/src/web/components/chat/provider-selector.tsx +150 -0
- package/src/web/components/chat/session-picker.tsx +80 -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/ai-settings-section.tsx +196 -137
- package/src/web/components/settings/mcp-server-dialog.tsx +208 -0
- package/src/web/components/settings/mcp-settings-section.tsx +143 -0
- package/src/web/components/settings/proxy-settings-section.tsx +40 -42
- package/src/web/components/settings/settings-tab.tsx +5 -2
- package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
- package/src/web/hooks/use-chat.ts +234 -207
- 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/api-mcp.ts +38 -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-BKIT_Qeg.js} +0 -0
- /package/dist/web/assets/{array-CYkMkqnU.js → array-DqLCdDFv.js} +0 -0
- /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DbesTfa7.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-HeHO0VhB.js → cytoscape.esm-CWPXKqbJ.js} +0 -0
- /package/dist/web/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-CrJzLgRD.js} +0 -0
- /package/dist/web/assets/{dist-BUYzeuKe.js → dist-Cep75xXf.js} +0 -0
- /package/dist/web/assets/{init-Rr1s_RiX.js → init-C0r9Gk5G.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-BB-mzMLb.js → isArrayLikeObject-CGBoxvCD.js} +0 -0
- /package/dist/web/assets/{katex-CKoArbIw.js → katex-DzXRfQ_m.js} +0 -0
- /package/dist/web/assets/{math-B7b0HgJF.js → math-y9zN1W-N.js} +0 -0
- /package/dist/web/assets/{path-BAQ3hXlG.js → path-DIKpVbHL.js} +0 -0
- /package/dist/web/assets/{preload-helper-DeiOTZKJ.js → preload-helper-Bf_JiD2A.js} +0 -0
- /package/dist/web/assets/{react-Dev-wu-s.js → react-SKk5z-bm.js} +0 -0
- /package/dist/web/assets/{rough.esm-Dwml_la6.js → rough.esm-nHaDi0Kw.js} +0 -0
- /package/dist/web/assets/{src-B_cC68fH.js → src-Dw4QhedI.js} +0 -0
- /package/dist/web/assets/{table-COiJDPRA.js → table-CQVQM2SB.js} +0 -0
- /package/dist/web/assets/{tag-LMq02LfE.js → tag-Q2dZiSPX.js} +0 -0
- /package/dist/web/assets/{utils-btZ8C8-R.js → utils-DMiycH3O.js} +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { useMemo, useRef, useEffect } from "react";
|
|
2
|
+
import { ChevronRight, Folder, File, FileCode, FileJson, FileText, FileType } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
DropdownMenuSub,
|
|
9
|
+
DropdownMenuSubTrigger,
|
|
10
|
+
DropdownMenuSubContent,
|
|
11
|
+
} from "@/components/ui/dropdown-menu";
|
|
12
|
+
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
13
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
14
|
+
import { basename } from "@/lib/utils";
|
|
15
|
+
|
|
16
|
+
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
17
|
+
ts: FileCode, tsx: FileCode, js: FileCode, jsx: FileCode,
|
|
18
|
+
py: FileCode, rs: FileCode, go: FileCode, html: FileCode,
|
|
19
|
+
css: FileCode, scss: FileCode,
|
|
20
|
+
json: FileJson,
|
|
21
|
+
md: FileText, txt: FileText,
|
|
22
|
+
yaml: FileType, yml: FileType,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getIcon(name: string, isDir: boolean) {
|
|
26
|
+
if (isDir) return Folder;
|
|
27
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
28
|
+
return ICON_MAP[ext] ?? File;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface BreadcrumbSegment {
|
|
32
|
+
name: string;
|
|
33
|
+
fullPath: string;
|
|
34
|
+
node: FileNode | null;
|
|
35
|
+
siblings: FileNode[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function walkTree(tree: FileNode[], segments: string[]): BreadcrumbSegment[] {
|
|
39
|
+
const result: BreadcrumbSegment[] = [];
|
|
40
|
+
let current: FileNode[] = tree;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < segments.length; i++) {
|
|
43
|
+
const seg = segments[i]!;
|
|
44
|
+
const fullPath = segments.slice(0, i + 1).join("/");
|
|
45
|
+
const match = current.find((n) => n.name === seg);
|
|
46
|
+
result.push({
|
|
47
|
+
name: seg,
|
|
48
|
+
fullPath,
|
|
49
|
+
node: match ?? null,
|
|
50
|
+
siblings: current,
|
|
51
|
+
});
|
|
52
|
+
if (match?.children) {
|
|
53
|
+
current = match.children;
|
|
54
|
+
} else {
|
|
55
|
+
// Remaining segments have no tree data — add as plain
|
|
56
|
+
for (let j = i + 1; j < segments.length; j++) {
|
|
57
|
+
result.push({
|
|
58
|
+
name: segments[j]!,
|
|
59
|
+
fullPath: segments.slice(0, j + 1).join("/"),
|
|
60
|
+
node: null,
|
|
61
|
+
siblings: [],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sortNodes(nodes: FileNode[]): FileNode[] {
|
|
71
|
+
return [...nodes].sort((a, b) => {
|
|
72
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
73
|
+
return a.name.localeCompare(b.name);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface EditorBreadcrumbProps {
|
|
78
|
+
filePath: string;
|
|
79
|
+
projectName: string;
|
|
80
|
+
tabId: string;
|
|
81
|
+
className?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function EditorBreadcrumb({ filePath, projectName, tabId, className }: EditorBreadcrumbProps) {
|
|
85
|
+
const tree = useFileStore((s) => s.tree);
|
|
86
|
+
const { updateTab, openTab } = useTabStore();
|
|
87
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
88
|
+
|
|
89
|
+
const segments = useMemo(
|
|
90
|
+
() => walkTree(tree, filePath.split("/").filter(Boolean)),
|
|
91
|
+
[tree, filePath],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Auto-scroll to rightmost segment
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (scrollRef.current) {
|
|
97
|
+
scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
|
|
98
|
+
}
|
|
99
|
+
}, [segments]);
|
|
100
|
+
|
|
101
|
+
function handleFileClick(path: string, e: React.MouseEvent) {
|
|
102
|
+
const name = basename(path);
|
|
103
|
+
if (e.metaKey || e.ctrlKey) {
|
|
104
|
+
openTab({ type: "editor", title: name, metadata: { filePath: path, projectName }, projectId: projectName, closable: true });
|
|
105
|
+
} else {
|
|
106
|
+
updateTab(tabId, { title: name, metadata: { filePath: path, projectName } });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div ref={scrollRef} className={className}>
|
|
112
|
+
{segments.map((seg, i) => (
|
|
113
|
+
<div key={seg.fullPath} className="flex items-center shrink-0">
|
|
114
|
+
{i > 0 && <ChevronRight className="size-3 text-muted-foreground shrink-0 mx-0.5" />}
|
|
115
|
+
{seg.siblings.length > 0 ? (
|
|
116
|
+
<SegmentDropdown
|
|
117
|
+
segment={seg}
|
|
118
|
+
isLast={i === segments.length - 1}
|
|
119
|
+
projectName={projectName}
|
|
120
|
+
onFileClick={handleFileClick}
|
|
121
|
+
/>
|
|
122
|
+
) : (
|
|
123
|
+
<span className="text-xs text-muted-foreground px-1 py-0.5">{seg.name}</span>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface SegmentDropdownProps {
|
|
132
|
+
segment: BreadcrumbSegment;
|
|
133
|
+
isLast: boolean;
|
|
134
|
+
projectName: string;
|
|
135
|
+
onFileClick: (path: string, e: React.MouseEvent) => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function SegmentDropdown({ segment, isLast, projectName, onFileClick }: SegmentDropdownProps) {
|
|
139
|
+
const sorted = useMemo(() => sortNodes(segment.siblings), [segment.siblings]);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<DropdownMenu>
|
|
143
|
+
<DropdownMenuTrigger asChild>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
className={`text-xs px-1 py-0.5 rounded hover:bg-muted transition-colors truncate max-w-[120px] ${
|
|
147
|
+
isLast ? "text-foreground font-medium" : "text-muted-foreground"
|
|
148
|
+
}`}
|
|
149
|
+
>
|
|
150
|
+
{segment.name}
|
|
151
|
+
</button>
|
|
152
|
+
</DropdownMenuTrigger>
|
|
153
|
+
<DropdownMenuContent align="start" className="max-h-[300px] p-1">
|
|
154
|
+
{sorted.map((node) => (
|
|
155
|
+
<NodeMenuItem
|
|
156
|
+
key={node.path}
|
|
157
|
+
node={node}
|
|
158
|
+
projectName={projectName}
|
|
159
|
+
activePath={segment.fullPath}
|
|
160
|
+
onFileClick={onFileClick}
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</DropdownMenuContent>
|
|
164
|
+
</DropdownMenu>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface NodeMenuItemProps {
|
|
169
|
+
node: FileNode;
|
|
170
|
+
projectName: string;
|
|
171
|
+
activePath: string;
|
|
172
|
+
onFileClick: (path: string, e: React.MouseEvent) => void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function NodeMenuItem({ node, projectName, activePath, onFileClick }: NodeMenuItemProps) {
|
|
176
|
+
const Icon = getIcon(node.name, node.type === "directory");
|
|
177
|
+
const isActive = node.path === activePath;
|
|
178
|
+
|
|
179
|
+
if (node.type === "directory" && node.children && node.children.length > 0) {
|
|
180
|
+
return (
|
|
181
|
+
<DropdownMenuSub>
|
|
182
|
+
<DropdownMenuSubTrigger className={`text-xs gap-1.5 ${isActive ? "bg-muted" : ""}`}>
|
|
183
|
+
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
184
|
+
<span className="truncate">{node.name}</span>
|
|
185
|
+
</DropdownMenuSubTrigger>
|
|
186
|
+
<DropdownMenuSubContent className="max-h-[300px] overflow-y-auto p-1">
|
|
187
|
+
{sortNodes(node.children).map((child) => (
|
|
188
|
+
<NodeMenuItem
|
|
189
|
+
key={child.path}
|
|
190
|
+
node={child}
|
|
191
|
+
projectName={projectName}
|
|
192
|
+
activePath={activePath}
|
|
193
|
+
onFileClick={onFileClick}
|
|
194
|
+
/>
|
|
195
|
+
))}
|
|
196
|
+
</DropdownMenuSubContent>
|
|
197
|
+
</DropdownMenuSub>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<DropdownMenuItem
|
|
203
|
+
className={`text-xs gap-1.5 cursor-pointer ${isActive ? "bg-muted" : ""}`}
|
|
204
|
+
onSelect={(e) => {
|
|
205
|
+
// onSelect doesn't give MouseEvent, use click handler for Ctrl detection
|
|
206
|
+
}}
|
|
207
|
+
onClick={(e) => {
|
|
208
|
+
if (node.type === "directory") return;
|
|
209
|
+
onFileClick(node.path, e);
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
213
|
+
<span className="truncate">{node.name}</span>
|
|
214
|
+
</DropdownMenuItem>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Code, Eye, WrapText, Table } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
interface EditorToolbarProps {
|
|
4
|
+
ext: string;
|
|
5
|
+
mdMode?: "edit" | "preview";
|
|
6
|
+
onMdModeChange?: (mode: "edit" | "preview") => void;
|
|
7
|
+
csvMode?: "table" | "raw";
|
|
8
|
+
onCsvModeChange?: (mode: "table" | "raw") => void;
|
|
9
|
+
wordWrap: boolean;
|
|
10
|
+
onToggleWordWrap: () => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ToolbarButton({
|
|
15
|
+
active,
|
|
16
|
+
onClick,
|
|
17
|
+
icon: Icon,
|
|
18
|
+
label,
|
|
19
|
+
}: {
|
|
20
|
+
active: boolean;
|
|
21
|
+
onClick: () => void;
|
|
22
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
23
|
+
label: string;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
|
30
|
+
active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"
|
|
31
|
+
}`}
|
|
32
|
+
>
|
|
33
|
+
<Icon className="size-3" />
|
|
34
|
+
<span className="hidden sm:inline">{label}</span>
|
|
35
|
+
</button>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function EditorToolbar({
|
|
40
|
+
ext,
|
|
41
|
+
mdMode,
|
|
42
|
+
onMdModeChange,
|
|
43
|
+
csvMode,
|
|
44
|
+
onCsvModeChange,
|
|
45
|
+
wordWrap,
|
|
46
|
+
onToggleWordWrap,
|
|
47
|
+
className,
|
|
48
|
+
}: EditorToolbarProps) {
|
|
49
|
+
const isMarkdown = ext === "md" || ext === "mdx";
|
|
50
|
+
const isCsv = ext === "csv";
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className={className}>
|
|
54
|
+
{isMarkdown && onMdModeChange && (
|
|
55
|
+
<>
|
|
56
|
+
<ToolbarButton active={mdMode === "edit"} onClick={() => onMdModeChange("edit")} icon={Code} label="Edit" />
|
|
57
|
+
<ToolbarButton active={mdMode === "preview"} onClick={() => onMdModeChange("preview")} icon={Eye} label="Preview" />
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
{isCsv && onCsvModeChange && (
|
|
61
|
+
<>
|
|
62
|
+
<ToolbarButton active={csvMode === "table"} onClick={() => onCsvModeChange("table")} icon={Table} label="Table" />
|
|
63
|
+
<ToolbarButton active={csvMode === "raw"} onClick={() => onCsvModeChange("raw")} icon={Code} label="Raw" />
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
<ToolbarButton
|
|
67
|
+
active={wordWrap}
|
|
68
|
+
onClick={onToggleWordWrap}
|
|
69
|
+
icon={WrapText}
|
|
70
|
+
label="Wrap"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
FolderOpen,
|
|
12
12
|
Loader2,
|
|
13
13
|
Globe,
|
|
14
|
+
Mic,
|
|
14
15
|
} from "lucide-react";
|
|
15
16
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
16
17
|
import { useProjectStore } from "@/stores/project-store";
|
|
@@ -157,8 +158,9 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
157
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")) },
|
|
158
159
|
{ id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
|
|
159
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" },
|
|
160
162
|
{ id: "postgres", label: "PostgreSQL", icon: Database, action: openNewTab("postgres", "PostgreSQL"), keywords: "database pg sql query", group: "action" },
|
|
161
|
-
{ id: "
|
|
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")) },
|
|
162
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")) },
|
|
163
165
|
{
|
|
164
166
|
id: "settings", label: "Settings", icon: Settings,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Suspense, lazy } from "react";
|
|
2
|
-
import { Loader2, Terminal, MessageSquare, GitBranch } from "lucide-react";
|
|
1
|
+
import { Suspense, lazy, useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { ChevronDown, ChevronUp, Loader2, Terminal, MessageSquare, GitBranch, Pin, PinOff } from "lucide-react";
|
|
3
3
|
import { usePanelStore } from "@/stores/panel-store";
|
|
4
4
|
import { useProjectStore } from "@/stores/project-store";
|
|
5
5
|
import type { TabType } from "@/stores/tab-store";
|
|
6
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
7
|
+
import type { SessionInfo } from "../../../types/chat";
|
|
6
8
|
import { TabBar } from "./tab-bar";
|
|
7
9
|
import { SplitDropOverlay } from "./split-drop-overlay";
|
|
8
10
|
import { cn } from "@/lib/utils";
|
|
@@ -74,8 +76,70 @@ export function EditorPanel({ panelId, projectName }: EditorPanelProps) {
|
|
|
74
76
|
);
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
function formatRelativeDate(iso: string): string {
|
|
80
|
+
try {
|
|
81
|
+
const date = new Date(iso);
|
|
82
|
+
const now = new Date();
|
|
83
|
+
const diffMs = now.getTime() - date.getTime();
|
|
84
|
+
const diffMin = Math.floor(diffMs / 60_000);
|
|
85
|
+
if (diffMin < 1) return "Just now";
|
|
86
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
87
|
+
const diffHr = Math.floor(diffMin / 60);
|
|
88
|
+
if (diffHr < 24) return `${diffHr}h ago`;
|
|
89
|
+
const diffDay = Math.floor(diffHr / 24);
|
|
90
|
+
if (diffDay < 7) return `${diffDay}d ago`;
|
|
91
|
+
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
92
|
+
} catch {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const MAX_RECENT_SESSIONS = 5;
|
|
98
|
+
const FETCH_SESSIONS_LIMIT = 20;
|
|
99
|
+
|
|
77
100
|
function EmptyPanel({ panelId }: { panelId: string }) {
|
|
78
101
|
const activeProject = useProjectStore((s) => s.activeProject);
|
|
102
|
+
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
103
|
+
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
104
|
+
const [showAll, setShowAll] = useState(false);
|
|
105
|
+
|
|
106
|
+
const loadSessions = useCallback(async () => {
|
|
107
|
+
if (!activeProject?.name) return;
|
|
108
|
+
setLoadingSessions(true);
|
|
109
|
+
try {
|
|
110
|
+
const data = await api.get<SessionInfo[]>(`${projectUrl(activeProject.name)}/chat/sessions`);
|
|
111
|
+
setSessions(data.slice(0, FETCH_SESSIONS_LIMIT));
|
|
112
|
+
} catch {
|
|
113
|
+
// silently ignore — empty state still functional without sessions
|
|
114
|
+
} finally {
|
|
115
|
+
setLoadingSessions(false);
|
|
116
|
+
}
|
|
117
|
+
}, [activeProject?.name]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => { loadSessions(); }, [loadSessions]);
|
|
120
|
+
|
|
121
|
+
const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
|
|
122
|
+
e.stopPropagation();
|
|
123
|
+
if (!activeProject?.name) return;
|
|
124
|
+
const url = `${projectUrl(activeProject.name)}/chat/sessions/${session.id}/pin`;
|
|
125
|
+
try {
|
|
126
|
+
if (session.pinned) {
|
|
127
|
+
await api.del(url);
|
|
128
|
+
} else {
|
|
129
|
+
await api.put(url);
|
|
130
|
+
}
|
|
131
|
+
setSessions((prev) => {
|
|
132
|
+
const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
|
|
133
|
+
return updated.sort((a, b) => {
|
|
134
|
+
if (a.pinned && !b.pinned) return -1;
|
|
135
|
+
if (!a.pinned && b.pinned) return 1;
|
|
136
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
// silently ignore
|
|
141
|
+
}
|
|
142
|
+
}, [activeProject?.name]);
|
|
79
143
|
|
|
80
144
|
function openTab(type: TabType) {
|
|
81
145
|
const needsProject = type !== "settings";
|
|
@@ -86,23 +150,103 @@ function EmptyPanel({ panelId }: { panelId: string }) {
|
|
|
86
150
|
);
|
|
87
151
|
}
|
|
88
152
|
|
|
153
|
+
function openSession(session: SessionInfo) {
|
|
154
|
+
usePanelStore.getState().openTab(
|
|
155
|
+
{
|
|
156
|
+
type: "chat",
|
|
157
|
+
title: session.title || "Chat",
|
|
158
|
+
projectId: activeProject?.name ?? null,
|
|
159
|
+
metadata: { projectName: activeProject?.name, sessionId: session.id, providerId: session.providerId },
|
|
160
|
+
closable: true,
|
|
161
|
+
},
|
|
162
|
+
panelId,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const pinnedSessions = sessions.filter((s) => s.pinned);
|
|
167
|
+
const allRecentSessions = sessions.filter((s) => !s.pinned);
|
|
168
|
+
const recentSessions = showAll ? allRecentSessions : allRecentSessions.slice(0, MAX_RECENT_SESSIONS);
|
|
169
|
+
const hasMore = allRecentSessions.length > MAX_RECENT_SESSIONS;
|
|
170
|
+
|
|
171
|
+
function renderSessionRow(session: SessionInfo) {
|
|
172
|
+
return (
|
|
173
|
+
<button
|
|
174
|
+
key={session.id}
|
|
175
|
+
onClick={() => openSession(session)}
|
|
176
|
+
className="group flex items-center gap-2.5 w-full px-3 py-2.5 text-left hover:bg-surface-elevated active:bg-surface-elevated transition-colors border-b border-border/50 last:border-0"
|
|
177
|
+
>
|
|
178
|
+
<MessageSquare className="size-3.5 shrink-0 text-text-subtle" />
|
|
179
|
+
<span className="flex-1 min-w-0 text-xs font-medium truncate text-text-primary">
|
|
180
|
+
{session.title || "Untitled"}
|
|
181
|
+
</span>
|
|
182
|
+
{session.updatedAt && (
|
|
183
|
+
<span className="text-[10px] text-text-subtle shrink-0">
|
|
184
|
+
{formatRelativeDate(session.updatedAt)}
|
|
185
|
+
</span>
|
|
186
|
+
)}
|
|
187
|
+
<span
|
|
188
|
+
role="button"
|
|
189
|
+
tabIndex={0}
|
|
190
|
+
onClick={(e) => togglePin(e, session)}
|
|
191
|
+
className={`p-1 rounded transition-colors shrink-0 ${
|
|
192
|
+
session.pinned
|
|
193
|
+
? "text-primary hover:text-primary/70"
|
|
194
|
+
: "text-text-subtle md:opacity-0 md:group-hover:opacity-100 hover:text-text-primary"
|
|
195
|
+
}`}
|
|
196
|
+
aria-label={session.pinned ? "Unpin session" : "Pin session"}
|
|
197
|
+
>
|
|
198
|
+
{session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
199
|
+
</span>
|
|
200
|
+
</button>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
89
204
|
return (
|
|
90
|
-
<div className="flex flex-col
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
205
|
+
<div className="flex flex-col h-full overflow-y-auto text-text-secondary">
|
|
206
|
+
<div className="flex flex-col items-center justify-center gap-6 px-4 flex-1">
|
|
207
|
+
<p className="text-sm">Open a tab to get started</p>
|
|
208
|
+
<div className="grid grid-cols-3 gap-2 w-full max-w-sm">
|
|
209
|
+
{QUICK_OPEN_TABS.map((opt) => {
|
|
210
|
+
const Icon = opt.icon;
|
|
211
|
+
return (
|
|
212
|
+
<button
|
|
213
|
+
key={opt.type}
|
|
214
|
+
onClick={() => openTab(opt.type)}
|
|
215
|
+
className="flex flex-col items-center justify-center gap-1.5 px-2 py-3 rounded-md border border-border bg-surface hover:bg-surface-elevated active:bg-surface-elevated text-xs text-foreground transition-colors"
|
|
216
|
+
>
|
|
217
|
+
<Icon className="size-5" />
|
|
218
|
+
{opt.label}
|
|
219
|
+
</button>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{activeProject && !loadingSessions && pinnedSessions.length > 0 && (
|
|
225
|
+
<div className="flex flex-col gap-2 w-full max-w-sm">
|
|
226
|
+
<p className="text-xs text-text-subtle text-center">Pinned</p>
|
|
227
|
+
<div className="w-full rounded-md border border-border bg-surface overflow-hidden">
|
|
228
|
+
{pinnedSessions.map(renderSessionRow)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{activeProject && !loadingSessions && recentSessions.length > 0 && (
|
|
234
|
+
<div className="flex flex-col gap-2 w-full max-w-sm">
|
|
235
|
+
<p className="text-xs text-text-subtle text-center">Recent chats</p>
|
|
236
|
+
<div className="w-full rounded-md border border-border bg-surface overflow-hidden">
|
|
237
|
+
{recentSessions.map(renderSessionRow)}
|
|
238
|
+
</div>
|
|
239
|
+
{hasMore && (
|
|
240
|
+
<button
|
|
241
|
+
onClick={() => setShowAll(!showAll)}
|
|
242
|
+
className="flex items-center justify-center gap-1 text-[11px] text-text-subtle hover:text-text-primary transition-colors py-1"
|
|
243
|
+
>
|
|
244
|
+
{showAll ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
|
|
245
|
+
{showAll ? "Show less" : `Show more (${allRecentSessions.length - MAX_RECENT_SESSIONS})`}
|
|
246
|
+
</button>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
106
250
|
</div>
|
|
107
251
|
</div>
|
|
108
252
|
);
|
|
@@ -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
|
|