@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
|
@@ -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,7 +23,9 @@ interface UseChatReturn {
|
|
|
23
23
|
pendingApproval: ApprovalRequest | null;
|
|
24
24
|
contextWindowPct: number | null;
|
|
25
25
|
sessionTitle: string | null;
|
|
26
|
-
|
|
26
|
+
/** When CLI provider assigns a different session ID, this holds the new ID */
|
|
27
|
+
migratedSessionId: string | null;
|
|
28
|
+
sendMessage: (content: string, opts?: { permissionMode?: string }) => void;
|
|
27
29
|
respondToApproval: (requestId: string, approved: boolean, data?: unknown) => void;
|
|
28
30
|
cancelStreaming: () => void;
|
|
29
31
|
reconnect: () => 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);
|
|
@@ -375,13 +385,11 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
375
385
|
}, [sessionId, providerId, projectName]);
|
|
376
386
|
|
|
377
387
|
const sendMessage = useCallback(
|
|
378
|
-
(content: string, opts?: { permissionMode?: string
|
|
388
|
+
(content: string, opts?: { permissionMode?: string }) => {
|
|
379
389
|
if (!content.trim()) return;
|
|
380
390
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (isFollowUp) {
|
|
384
|
-
// Streaming follow-up: finalize current assistant message, then send
|
|
391
|
+
// If streaming, cancel current stream first then send immediately
|
|
392
|
+
if (phaseRef.current !== "idle") {
|
|
385
393
|
const finalContent = streamingContentRef.current;
|
|
386
394
|
const finalEvents = [...streamingEventsRef.current];
|
|
387
395
|
setMessages((prev) => {
|
|
@@ -394,6 +402,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
394
402
|
}
|
|
395
403
|
return prev;
|
|
396
404
|
});
|
|
405
|
+
send(JSON.stringify({ type: "cancel" }));
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
// Add user message
|
|
@@ -407,26 +416,15 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
407
416
|
},
|
|
408
417
|
]);
|
|
409
418
|
|
|
410
|
-
// Reset streaming state
|
|
419
|
+
// Reset streaming state
|
|
411
420
|
streamingContentRef.current = "";
|
|
412
421
|
streamingEventsRef.current = [];
|
|
413
422
|
pendingMessageRef.current = null;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
phaseRef.current = "initializing";
|
|
417
|
-
} else {
|
|
418
|
-
setPhase("thinking");
|
|
419
|
-
phaseRef.current = "thinking";
|
|
420
|
-
}
|
|
423
|
+
setPhase("initializing");
|
|
424
|
+
phaseRef.current = "initializing";
|
|
421
425
|
setPendingApproval(null);
|
|
422
426
|
|
|
423
|
-
send(JSON.stringify({
|
|
424
|
-
type: "message",
|
|
425
|
-
content,
|
|
426
|
-
permissionMode: opts?.permissionMode,
|
|
427
|
-
priority: opts?.priority,
|
|
428
|
-
images: opts?.images,
|
|
429
|
-
}));
|
|
427
|
+
send(JSON.stringify({ type: "message", content, permissionMode: opts?.permissionMode }));
|
|
430
428
|
},
|
|
431
429
|
[send],
|
|
432
430
|
);
|
|
@@ -530,6 +528,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
530
528
|
pendingApproval,
|
|
531
529
|
contextWindowPct,
|
|
532
530
|
sessionTitle,
|
|
531
|
+
migratedSessionId,
|
|
533
532
|
sendMessage,
|
|
534
533
|
respondToApproval,
|
|
535
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}`,
|