@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
|
@@ -9,12 +9,9 @@ import {
|
|
|
9
9
|
SelectValue,
|
|
10
10
|
} from "@/components/ui/select";
|
|
11
11
|
import { getAISettings, updateAISettings, type AISettings } from "@/lib/api-settings";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{ value: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
16
|
-
{ value: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
17
|
-
];
|
|
12
|
+
import { api } from "@/lib/api-client";
|
|
13
|
+
import { ProviderBadge } from "@/components/chat/provider-selector";
|
|
14
|
+
import type { ModelOption } from "../../../types/chat";
|
|
18
15
|
|
|
19
16
|
const EFFORT_OPTIONS = [
|
|
20
17
|
{ value: "low", label: "Low" },
|
|
@@ -29,19 +26,47 @@ const PERMISSION_MODE_OPTIONS = [
|
|
|
29
26
|
{ value: "plan", label: "Plan mode" },
|
|
30
27
|
];
|
|
31
28
|
|
|
29
|
+
const PROVIDER_NAMES: Record<string, string> = {
|
|
30
|
+
claude: "Claude",
|
|
31
|
+
cursor: "Cursor",
|
|
32
|
+
codex: "Codex",
|
|
33
|
+
gemini: "Gemini",
|
|
34
|
+
};
|
|
35
|
+
|
|
32
36
|
export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
|
|
33
37
|
const [settings, setSettings] = useState<AISettings | null>(null);
|
|
38
|
+
const [activeTab, setActiveTab] = useState<string>("");
|
|
39
|
+
const [models, setModels] = useState<ModelOption[]>([]);
|
|
40
|
+
const [modelsLoading, setModelsLoading] = useState(false);
|
|
34
41
|
const [saving, setSaving] = useState(false);
|
|
35
42
|
const [error, setError] = useState<string | null>(null);
|
|
36
|
-
// Revision counter forces number inputs to re-render with fresh defaultValue after save
|
|
37
43
|
const [revision, setRevision] = useState(0);
|
|
38
44
|
|
|
39
45
|
useEffect(() => {
|
|
40
|
-
getAISettings().then(
|
|
46
|
+
getAISettings().then((s) => {
|
|
47
|
+
setSettings(s);
|
|
48
|
+
setActiveTab(s.default_provider ?? "claude");
|
|
49
|
+
}).catch((e) => setError(e.message));
|
|
41
50
|
}, []);
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
// Fetch models when active tab changes — uses global settings endpoint
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!activeTab) return;
|
|
55
|
+
setModelsLoading(true);
|
|
56
|
+
api.get<ModelOption[]>(`/api/settings/ai/providers/${activeTab}/models`)
|
|
57
|
+
.then(setModels)
|
|
58
|
+
.catch(() => setModels([]))
|
|
59
|
+
.finally(() => setModelsLoading(false));
|
|
60
|
+
}, [activeTab]);
|
|
61
|
+
|
|
62
|
+
const providerTabs = settings
|
|
63
|
+
? Object.keys(settings.providers)
|
|
64
|
+
.filter((k) => k !== "mock")
|
|
65
|
+
.map((id) => ({ id, name: PROVIDER_NAMES[id] ?? id }))
|
|
66
|
+
: [];
|
|
67
|
+
|
|
68
|
+
const config = settings?.providers[activeTab];
|
|
69
|
+
const isSdkProvider = config?.type === "agent-sdk" || (!config?.type && activeTab === "claude");
|
|
45
70
|
|
|
46
71
|
const handleSave = async (field: string, value: unknown) => {
|
|
47
72
|
if (!settings) return;
|
|
@@ -49,7 +74,7 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
|
|
|
49
74
|
setError(null);
|
|
50
75
|
try {
|
|
51
76
|
const updated = await updateAISettings({
|
|
52
|
-
providers: { [
|
|
77
|
+
providers: { [activeTab]: { [field]: value } },
|
|
53
78
|
});
|
|
54
79
|
setSettings(updated);
|
|
55
80
|
setRevision((r) => r + 1);
|
|
@@ -69,7 +94,7 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
|
|
|
69
94
|
if (!settings) {
|
|
70
95
|
return (
|
|
71
96
|
<div className={innerGap}>
|
|
72
|
-
<h3 className={`${headingSize} font-medium text-text-secondary`}>AI
|
|
97
|
+
<h3 className={`${headingSize} font-medium text-text-secondary`}>AI Settings</h3>
|
|
73
98
|
<p className={`${labelSize} text-text-subtle`}>
|
|
74
99
|
{error ? `Error: ${error}` : "Loading..."}
|
|
75
100
|
</p>
|
|
@@ -77,139 +102,173 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
|
|
|
77
102
|
);
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
// Model select options: use fetched models, with "auto" option for non-SDK providers
|
|
106
|
+
const modelOptions = isSdkProvider
|
|
107
|
+
? models
|
|
108
|
+
: [{ value: "__default__", label: "Auto (default)" }, ...models];
|
|
109
|
+
|
|
80
110
|
return (
|
|
81
111
|
<div className={gapSize}>
|
|
82
|
-
<h3 className={`${headingSize} font-medium text-text-secondary`}>AI
|
|
112
|
+
<h3 className={`${headingSize} font-medium text-text-secondary`}>AI Settings</h3>
|
|
83
113
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
</Select>
|
|
114
|
+
{/* Provider tabs */}
|
|
115
|
+
{providerTabs.length > 1 && (
|
|
116
|
+
<div className="flex gap-0.5 border-b border-border/50 -mx-1 px-1">
|
|
117
|
+
{providerTabs.map((p) => (
|
|
118
|
+
<button
|
|
119
|
+
key={p.id}
|
|
120
|
+
onClick={() => setActiveTab(p.id)}
|
|
121
|
+
className={`flex items-center gap-1 px-2 py-1 text-[11px] rounded-t transition-colors ${
|
|
122
|
+
activeTab === p.id
|
|
123
|
+
? "text-primary border-b-2 border-primary font-medium"
|
|
124
|
+
: "text-text-subtle hover:text-text-secondary"
|
|
125
|
+
}`}
|
|
126
|
+
>
|
|
127
|
+
<ProviderBadge providerId={p.id} />
|
|
128
|
+
<span className="capitalize">{p.name}</span>
|
|
129
|
+
</button>
|
|
130
|
+
))}
|
|
102
131
|
</div>
|
|
132
|
+
)}
|
|
103
133
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
<div className={innerGap}>
|
|
135
|
+
{/* Model selector — dynamic, works for all providers */}
|
|
136
|
+
{models.length > 0 && (
|
|
137
|
+
<div className={fieldGap}>
|
|
138
|
+
<Label htmlFor="ai-model" className={compact ? labelSize : undefined}>Model</Label>
|
|
139
|
+
<Select
|
|
140
|
+
value={isSdkProvider ? (config?.model ?? models[0]?.value) : (config?.model || "__default__")}
|
|
141
|
+
onValueChange={(v) => handleSave("model", v === "__default__" ? undefined : v)}
|
|
142
|
+
disabled={modelsLoading}
|
|
143
|
+
>
|
|
144
|
+
<SelectTrigger id="ai-model" className={`w-full ${compact ? "h-7 text-[11px]" : ""}`}>
|
|
145
|
+
<SelectValue placeholder={modelsLoading ? "Loading models..." : "Select model"} />
|
|
146
|
+
</SelectTrigger>
|
|
147
|
+
<SelectContent className="max-h-[300px]">
|
|
148
|
+
{modelOptions.map((opt) => (
|
|
149
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
150
|
+
{opt.label}
|
|
151
|
+
</SelectItem>
|
|
152
|
+
))}
|
|
153
|
+
</SelectContent>
|
|
154
|
+
</Select>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
119
157
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
</p>
|
|
139
|
-
</div>
|
|
158
|
+
{/* SDK-specific fields */}
|
|
159
|
+
{isSdkProvider && (
|
|
160
|
+
<>
|
|
161
|
+
<div className={fieldGap}>
|
|
162
|
+
<Label htmlFor="ai-base-url" className={compact ? labelSize : undefined}>Base URL</Label>
|
|
163
|
+
<Input
|
|
164
|
+
key={`baseurl-${activeTab}-${revision}`}
|
|
165
|
+
id="ai-base-url"
|
|
166
|
+
type="url"
|
|
167
|
+
defaultValue={config?.base_url ?? ""}
|
|
168
|
+
placeholder="https://api.anthropic.com (default)"
|
|
169
|
+
className={compact ? "h-7 text-[11px]" : undefined}
|
|
170
|
+
onBlur={(e) => {
|
|
171
|
+
const val = e.target.value.trim();
|
|
172
|
+
handleSave("base_url", val || undefined);
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
140
176
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
<div className={fieldGap}>
|
|
178
|
+
<Label htmlFor="ai-api-key" className={compact ? labelSize : undefined}>API Key / Token</Label>
|
|
179
|
+
<Input
|
|
180
|
+
key={`apikey-${activeTab}-${revision}`}
|
|
181
|
+
id="ai-api-key"
|
|
182
|
+
type="password"
|
|
183
|
+
defaultValue={config?.api_key ?? ""}
|
|
184
|
+
placeholder="sk-ant-... (optional, overrides accounts)"
|
|
185
|
+
className={compact ? "h-7 text-[11px] font-mono" : "font-mono"}
|
|
186
|
+
onBlur={(e) => {
|
|
187
|
+
const val = e.target.value.trim();
|
|
188
|
+
if (val.startsWith("••••")) return;
|
|
189
|
+
handleSave("api_key", val || undefined);
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
<p className={`${compact ? "text-[9px]" : "text-[11px]"} text-muted-foreground`}>
|
|
193
|
+
Direct API key or OAuth token. Leave empty to use connected accounts.
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
159
196
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
197
|
+
<div className={fieldGap}>
|
|
198
|
+
<Label htmlFor="ai-effort" className={compact ? labelSize : undefined}>Effort</Label>
|
|
199
|
+
<Select
|
|
200
|
+
value={config?.effort ?? "high"}
|
|
201
|
+
onValueChange={(v) => handleSave("effort", v)}
|
|
202
|
+
>
|
|
203
|
+
<SelectTrigger id="ai-effort" className={`w-full ${compact ? "h-7 text-[11px]" : ""}`}>
|
|
204
|
+
<SelectValue />
|
|
205
|
+
</SelectTrigger>
|
|
206
|
+
<SelectContent>
|
|
207
|
+
{EFFORT_OPTIONS.map((opt) => (
|
|
208
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
209
|
+
{opt.label}
|
|
210
|
+
</SelectItem>
|
|
211
|
+
))}
|
|
212
|
+
</SelectContent>
|
|
213
|
+
</Select>
|
|
214
|
+
</div>
|
|
176
215
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
/>
|
|
194
|
-
</div>
|
|
216
|
+
<div className={fieldGap}>
|
|
217
|
+
<Label htmlFor="ai-max-turns" className={compact ? labelSize : undefined}>Max Turns (1-500)</Label>
|
|
218
|
+
<Input
|
|
219
|
+
key={`turns-${activeTab}-${revision}`}
|
|
220
|
+
id="ai-max-turns"
|
|
221
|
+
type="number"
|
|
222
|
+
min={1}
|
|
223
|
+
max={500}
|
|
224
|
+
defaultValue={config?.max_turns ?? 100}
|
|
225
|
+
className={compact ? "h-7 text-[11px]" : undefined}
|
|
226
|
+
onBlur={(e) => {
|
|
227
|
+
const val = parseInt(e.target.value);
|
|
228
|
+
if (!isNaN(val)) handleSave("max_turns", val);
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
195
232
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
233
|
+
<div className={fieldGap}>
|
|
234
|
+
<Label htmlFor="ai-budget" className={compact ? labelSize : undefined}>Max Budget (USD)</Label>
|
|
235
|
+
<Input
|
|
236
|
+
key={`budget-${activeTab}-${revision}`}
|
|
237
|
+
id="ai-budget"
|
|
238
|
+
type="number"
|
|
239
|
+
step={0.1}
|
|
240
|
+
min={0.01}
|
|
241
|
+
max={50}
|
|
242
|
+
defaultValue={config?.max_budget_usd ?? ""}
|
|
243
|
+
placeholder="No limit"
|
|
244
|
+
className={compact ? "h-7 text-[11px]" : undefined}
|
|
245
|
+
onBlur={(e) => {
|
|
246
|
+
const val = parseFloat(e.target.value);
|
|
247
|
+
handleSave("max_budget_usd", isNaN(val) ? undefined : val);
|
|
248
|
+
}}
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div className={fieldGap}>
|
|
253
|
+
<Label htmlFor="ai-thinking" className={compact ? labelSize : undefined}>Thinking Budget (tokens)</Label>
|
|
254
|
+
<Input
|
|
255
|
+
key={`thinking-${activeTab}-${revision}`}
|
|
256
|
+
id="ai-thinking"
|
|
257
|
+
type="number"
|
|
258
|
+
min={0}
|
|
259
|
+
defaultValue={config?.thinking_budget_tokens ?? ""}
|
|
260
|
+
placeholder="Disabled"
|
|
261
|
+
className={compact ? "h-7 text-[11px]" : undefined}
|
|
262
|
+
onBlur={(e) => {
|
|
263
|
+
const val = parseInt(e.target.value);
|
|
264
|
+
handleSave("thinking_budget_tokens", isNaN(val) ? undefined : val);
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
</>
|
|
269
|
+
)}
|
|
212
270
|
|
|
271
|
+
{/* Common fields: permission mode + system prompt (all providers) */}
|
|
213
272
|
<div className={fieldGap}>
|
|
214
273
|
<Label htmlFor="ai-permission-mode" className={compact ? labelSize : undefined}>Default Permission Mode</Label>
|
|
215
274
|
<Select
|
|
@@ -232,11 +291,11 @@ export function AISettingsSection({ compact }: { compact?: boolean } = {}) {
|
|
|
232
291
|
<div className={fieldGap}>
|
|
233
292
|
<Label htmlFor="ai-system-prompt" className={compact ? labelSize : undefined}>Additional Instructions</Label>
|
|
234
293
|
<textarea
|
|
235
|
-
key={`sysprompt-${revision}`}
|
|
294
|
+
key={`sysprompt-${activeTab}-${revision}`}
|
|
236
295
|
id="ai-system-prompt"
|
|
237
|
-
rows={4}
|
|
296
|
+
rows={compact ? 3 : 4}
|
|
238
297
|
defaultValue={config?.system_prompt ?? ""}
|
|
239
|
-
placeholder=
|
|
298
|
+
placeholder={`Enter additional instructions for ${activeTab}...`}
|
|
240
299
|
className={`w-full rounded-md border border-input bg-background px-3 py-2 ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${compact ? "text-[11px]" : "text-sm"}`}
|
|
241
300
|
onBlur={(e) => {
|
|
242
301
|
const val = e.target.value.trim();
|
|
@@ -23,6 +23,8 @@ interface UseChatReturn {
|
|
|
23
23
|
pendingApproval: ApprovalRequest | null;
|
|
24
24
|
contextWindowPct: number | null;
|
|
25
25
|
sessionTitle: string | null;
|
|
26
|
+
/** When CLI provider assigns a different session ID, this holds the new ID */
|
|
27
|
+
migratedSessionId: string | null;
|
|
26
28
|
sendMessage: (content: string, opts?: { permissionMode?: string }) => void;
|
|
27
29
|
respondToApproval: (requestId: string, approved: boolean, data?: unknown) => void;
|
|
28
30
|
cancelStreaming: () => void;
|
|
@@ -51,6 +53,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
51
53
|
const [contextWindowPct, setContextWindowPct] = useState<number | null>(null);
|
|
52
54
|
const [sessionTitle, setSessionTitle] = useState<string | null>(null);
|
|
53
55
|
const [isConnected, setIsConnected] = useState(false);
|
|
56
|
+
const [migratedSessionId, setMigratedSessionId] = useState<string | null>(null);
|
|
54
57
|
const streamingContentRef = useRef("");
|
|
55
58
|
const streamingEventsRef = useRef<ChatEvent[]>([]);
|
|
56
59
|
const streamingAccountRef = useRef<{ accountId: string; accountLabel: string } | null>(null);
|
|
@@ -243,6 +246,13 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
243
246
|
// Ignore keepalive pings
|
|
244
247
|
if ((data as any).type === "ping") return;
|
|
245
248
|
|
|
249
|
+
// Handle session ID migration (CLI provider assigned different ID)
|
|
250
|
+
if ((data as any).type === "session_migrated") {
|
|
251
|
+
const newId = (data as any).newSessionId as string;
|
|
252
|
+
if (newId) setMigratedSessionId(newId);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
246
256
|
// Handle title updates from SDK summary
|
|
247
257
|
if ((data as any).type === "title_updated") {
|
|
248
258
|
setSessionTitle((data as any).title ?? null);
|
|
@@ -518,6 +528,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
518
528
|
pendingApproval,
|
|
519
529
|
contextWindowPct,
|
|
520
530
|
sessionTitle,
|
|
531
|
+
migratedSessionId,
|
|
521
532
|
sendMessage,
|
|
522
533
|
respondToApproval,
|
|
523
534
|
cancelStreaming,
|
|
@@ -124,6 +124,13 @@ export function useGlobalKeybindings() {
|
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// Toggle voice input in chat
|
|
128
|
+
if (match(e, "voice-input")) {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
window.dispatchEvent(new CustomEvent("toggle-voice-input"));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
127
134
|
// Open search (sidebar)
|
|
128
135
|
if (match(e, "open-search")) {
|
|
129
136
|
e.preventDefault();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
// Extend Window for webkit prefix
|
|
4
|
+
interface SpeechRecognitionEvent extends Event {
|
|
5
|
+
results: SpeechRecognitionResultList;
|
|
6
|
+
resultIndex: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type SpeechRecognitionInstance = {
|
|
10
|
+
lang: string;
|
|
11
|
+
continuous: boolean;
|
|
12
|
+
interimResults: boolean;
|
|
13
|
+
start(): void;
|
|
14
|
+
stop(): void;
|
|
15
|
+
abort(): void;
|
|
16
|
+
onresult: ((event: SpeechRecognitionEvent) => void) | null;
|
|
17
|
+
onend: (() => void) | null;
|
|
18
|
+
onerror: ((event: Event & { error: string }) => void) | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
|
|
22
|
+
|
|
23
|
+
function getSpeechRecognition(): SpeechRecognitionConstructor | null {
|
|
24
|
+
const w = window as unknown as {
|
|
25
|
+
SpeechRecognition?: SpeechRecognitionConstructor;
|
|
26
|
+
webkitSpeechRecognition?: SpeechRecognitionConstructor;
|
|
27
|
+
};
|
|
28
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useVoiceInput(options?: { lang?: string }) {
|
|
32
|
+
const [isListening, setIsListening] = useState(false);
|
|
33
|
+
const [interimText, setInterimText] = useState("");
|
|
34
|
+
const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
|
|
35
|
+
// Accumulate finalized text across multiple result events
|
|
36
|
+
const finalizedRef = useRef("");
|
|
37
|
+
|
|
38
|
+
const supported = typeof window !== "undefined" && getSpeechRecognition() !== null;
|
|
39
|
+
|
|
40
|
+
const start = useCallback(
|
|
41
|
+
(onResult: (text: string, isFinal: boolean) => void) => {
|
|
42
|
+
const SR = getSpeechRecognition();
|
|
43
|
+
if (!SR) return;
|
|
44
|
+
|
|
45
|
+
// Stop any existing session
|
|
46
|
+
recognitionRef.current?.abort();
|
|
47
|
+
|
|
48
|
+
const recognition = new SR();
|
|
49
|
+
recognition.lang = options?.lang ?? "vi-VN";
|
|
50
|
+
recognition.continuous = true;
|
|
51
|
+
recognition.interimResults = true;
|
|
52
|
+
|
|
53
|
+
finalizedRef.current = "";
|
|
54
|
+
|
|
55
|
+
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
56
|
+
let interim = "";
|
|
57
|
+
let newFinalized = "";
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < event.results.length; i++) {
|
|
60
|
+
const result = event.results[i]!;
|
|
61
|
+
if (result.isFinal) {
|
|
62
|
+
newFinalized += result[0]!.transcript;
|
|
63
|
+
} else {
|
|
64
|
+
interim += result[0]!.transcript;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Update finalized accumulator
|
|
69
|
+
if (newFinalized) {
|
|
70
|
+
finalizedRef.current = newFinalized;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fullText = (finalizedRef.current + " " + interim).trim();
|
|
74
|
+
setInterimText(interim);
|
|
75
|
+
onResult(fullText, interim.length === 0 && finalizedRef.current.length > 0);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
recognition.onend = () => {
|
|
79
|
+
setIsListening(false);
|
|
80
|
+
setInterimText("");
|
|
81
|
+
// Deliver final text if any
|
|
82
|
+
if (finalizedRef.current) {
|
|
83
|
+
onResult(finalizedRef.current.trim(), true);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
recognition.onerror = (event) => {
|
|
88
|
+
// "no-speech" and "aborted" are expected, not real errors
|
|
89
|
+
if (event.error !== "no-speech" && event.error !== "aborted") {
|
|
90
|
+
console.warn("[voice-input] error:", event.error);
|
|
91
|
+
}
|
|
92
|
+
setIsListening(false);
|
|
93
|
+
setInterimText("");
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
recognitionRef.current = recognition;
|
|
97
|
+
recognition.start();
|
|
98
|
+
setIsListening(true);
|
|
99
|
+
},
|
|
100
|
+
[options?.lang],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const stop = useCallback(() => {
|
|
104
|
+
recognitionRef.current?.stop();
|
|
105
|
+
recognitionRef.current = null;
|
|
106
|
+
setIsListening(false);
|
|
107
|
+
setInterimText("");
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
return { isListening, interimText, start, stop, supported };
|
|
111
|
+
}
|
|
@@ -36,6 +36,7 @@ export const KEY_ACTIONS: KeyAction[] = [
|
|
|
36
36
|
{ id: "open-git-graph", label: "Git Graph", category: "tabs", defaultKey: "Mod+G" },
|
|
37
37
|
{ id: "open-git-status", label: "Git Status (sidebar)", category: "tabs", defaultKey: "Mod+Shift+E" },
|
|
38
38
|
{ id: "open-search", label: "Search Files (sidebar)", category: "tabs", defaultKey: "Mod+Shift+F" },
|
|
39
|
+
{ id: "voice-input", label: "Voice Input", category: "general", defaultKey: "Mod+Shift+V", note: "Toggle speech-to-text in chat" },
|
|
39
40
|
// Projects — Mod+1..9
|
|
40
41
|
...Array.from({ length: 9 }, (_, i) => ({
|
|
41
42
|
id: `switch-project-${i + 1}`,
|
|
@@ -257,9 +257,9 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
257
257
|
: newTabs[newTabs.length - 1]?.id ?? null;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
// Auto-close panel if empty and not the last one
|
|
261
|
-
const
|
|
262
|
-
if (newTabs.length === 0 &&
|
|
260
|
+
// Auto-close panel if empty and not the last one in current grid
|
|
261
|
+
const gridPanelCount = s.grid.flat().length;
|
|
262
|
+
if (newTabs.length === 0 && gridPanelCount > 1) {
|
|
263
263
|
const { [pid]: _, ...rest } = s.panels;
|
|
264
264
|
const newGrid = gridRemovePanel(s.grid, pid);
|
|
265
265
|
const newFocused = s.focusedPanelId === pid ? Object.keys(rest)[0]! : s.focusedPanelId;
|
|
@@ -331,9 +331,9 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
331
331
|
else toTabs.push(tab);
|
|
332
332
|
|
|
333
333
|
set((s) => {
|
|
334
|
-
const
|
|
335
|
-
// Auto-close empty source panel if not last
|
|
336
|
-
if (fromTabs.length === 0 &&
|
|
334
|
+
const gridPanelCount = s.grid.flat().length;
|
|
335
|
+
// Auto-close empty source panel if not last in current grid
|
|
336
|
+
if (fromTabs.length === 0 && gridPanelCount > 1) {
|
|
337
337
|
const { [fromPanelId]: _, ...rest } = s.panels;
|
|
338
338
|
return {
|
|
339
339
|
panels: {
|
|
@@ -405,14 +405,14 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
set((s) => {
|
|
408
|
-
const
|
|
408
|
+
const gridPanelCount = s.grid.flat().length;
|
|
409
409
|
let updatedPanels = {
|
|
410
410
|
...s.panels,
|
|
411
411
|
[newPanel.id]: newPanel,
|
|
412
412
|
};
|
|
413
413
|
|
|
414
|
-
// If source is now empty and not last panel, remove it
|
|
415
|
-
if (srcTabs.length === 0 &&
|
|
414
|
+
// If source is now empty and not last panel in grid, remove it
|
|
415
|
+
if (srcTabs.length === 0 && gridPanelCount > 1) {
|
|
416
416
|
const { [sourcePanelId]: _, ...rest } = updatedPanels;
|
|
417
417
|
updatedPanels = rest;
|
|
418
418
|
newGrid = gridRemovePanel(newGrid, sourcePanelId);
|
|
@@ -428,7 +428,7 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
428
428
|
|
|
429
429
|
closePanel: (panelId) => {
|
|
430
430
|
const { panels, grid } = get();
|
|
431
|
-
if (
|
|
431
|
+
if (grid.flat().length <= 1) return;
|
|
432
432
|
|
|
433
433
|
const panel = panels[panelId];
|
|
434
434
|
if (!panel) return;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import"./chunk-XZSTWKYB-BYxFzZwS.js";import{n as e}from"./chunk-R5LLSJPH-euR2RxLN.js";export{e as createArchitectureServices};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{it as e,rt as t}from"./chunk-7R4GIKGN-DXaGAn_K.js";var n=(n,r)=>e.lang.round(t.parse(n)[r]);export{n as t};
|