@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
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
ChatMessage,
|
|
8
8
|
SendMessageOpts,
|
|
9
9
|
} from "../providers/provider.interface.ts";
|
|
10
|
-
import { MockProvider } from "../providers/mock-provider.ts";
|
|
11
10
|
|
|
12
11
|
class ChatService {
|
|
13
12
|
async createSession(
|
|
@@ -34,19 +33,18 @@ class ChatService {
|
|
|
34
33
|
if (providerId) {
|
|
35
34
|
const provider = providerRegistry.get(providerId);
|
|
36
35
|
if (!provider) throw new Error(`Provider "${providerId}" not found`);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return (provider as any).listSessionsByDir(dir);
|
|
36
|
+
if (dir && provider.listSessionsByDir) {
|
|
37
|
+
return provider.listSessionsByDir(dir);
|
|
40
38
|
}
|
|
41
39
|
return provider.listSessions();
|
|
42
40
|
}
|
|
43
41
|
// Aggregate from all providers
|
|
44
42
|
const all: SessionInfo[] = [];
|
|
45
|
-
for (const info of providerRegistry.
|
|
43
|
+
for (const info of providerRegistry.listAll()) {
|
|
46
44
|
const provider = providerRegistry.get(info.id);
|
|
47
45
|
if (provider) {
|
|
48
|
-
if (dir &&
|
|
49
|
-
all.push(...await
|
|
46
|
+
if (dir && provider.listSessionsByDir) {
|
|
47
|
+
all.push(...await provider.listSessionsByDir(dir));
|
|
50
48
|
} else {
|
|
51
49
|
all.push(...await provider.listSessions());
|
|
52
50
|
}
|
|
@@ -83,13 +81,13 @@ class ChatService {
|
|
|
83
81
|
|
|
84
82
|
/** Look up a session across all providers (for WS handler) */
|
|
85
83
|
getSession(sessionId: string): Session | null {
|
|
86
|
-
for (const info of providerRegistry.
|
|
84
|
+
for (const info of providerRegistry.listAll()) {
|
|
87
85
|
const provider = providerRegistry.get(info.id);
|
|
88
86
|
if (!provider) continue;
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
// Use internal sessions Map — SDK stores {meta, sdk}, others store Session directly
|
|
88
|
+
const sessions = (provider as any).sessions ?? (provider as any).activeSessions;
|
|
89
|
+
if (sessions instanceof Map && sessions.has(sessionId)) {
|
|
91
90
|
const entry = sessions.get(sessionId);
|
|
92
|
-
// SDK provider stores {meta, sdk}, others store Session directly
|
|
93
91
|
if (entry && typeof entry === "object" && "meta" in entry) {
|
|
94
92
|
return (entry as { meta: Session }).meta;
|
|
95
93
|
}
|
|
@@ -102,10 +100,7 @@ class ChatService {
|
|
|
102
100
|
async getMessages(providerId: string, sessionId: string): Promise<ChatMessage[]> {
|
|
103
101
|
const provider = providerRegistry.get(providerId);
|
|
104
102
|
if (!provider) return [];
|
|
105
|
-
|
|
106
|
-
return await (provider as any).getMessages(sessionId);
|
|
107
|
-
}
|
|
108
|
-
return [];
|
|
103
|
+
return await provider.getMessages?.(sessionId) ?? [];
|
|
109
104
|
}
|
|
110
105
|
}
|
|
111
106
|
|
package/src/types/chat.ts
CHANGED
|
@@ -5,18 +5,35 @@ export interface SendMessageOpts {
|
|
|
5
5
|
export interface AIProvider {
|
|
6
6
|
id: string;
|
|
7
7
|
name: string;
|
|
8
|
+
|
|
9
|
+
// Session lifecycle (required)
|
|
8
10
|
createSession(config: SessionConfig): Promise<Session>;
|
|
9
11
|
resumeSession(sessionId: string): Promise<Session>;
|
|
10
12
|
listSessions(): Promise<SessionInfo[]>;
|
|
11
13
|
deleteSession(sessionId: string): Promise<void>;
|
|
14
|
+
|
|
15
|
+
// Streaming (required)
|
|
12
16
|
sendMessage(
|
|
13
17
|
sessionId: string,
|
|
14
18
|
message: string,
|
|
15
19
|
opts?: SendMessageOpts,
|
|
16
20
|
): AsyncIterable<ChatEvent>;
|
|
17
|
-
|
|
21
|
+
|
|
22
|
+
// Optional capabilities — providers implement what they support
|
|
18
23
|
resolveApproval?(requestId: string, approved: boolean, data?: unknown): void;
|
|
19
24
|
onToolApproval?: (callback: ToolApprovalHandler) => void;
|
|
25
|
+
abortQuery?(sessionId: string): void;
|
|
26
|
+
getMessages?(sessionId: string): Promise<ChatMessage[]>;
|
|
27
|
+
listSessionsByDir?(dir: string): Promise<SessionInfo[]>;
|
|
28
|
+
ensureProjectPath?(sessionId: string, path: string): void;
|
|
29
|
+
setForkSource?(sessionId: string, sourceSessionId: string): void;
|
|
30
|
+
isAvailable?(): Promise<boolean>;
|
|
31
|
+
listModels?(): Promise<ModelOption[]>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ModelOption {
|
|
35
|
+
value: string;
|
|
36
|
+
label: string;
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
export interface Session {
|
|
@@ -90,7 +107,9 @@ export type ChatEvent =
|
|
|
90
107
|
| { type: "approval_request"; requestId: string; tool: string; input: unknown }
|
|
91
108
|
| { type: "error"; message: string }
|
|
92
109
|
| { type: "done"; sessionId: string; resultSubtype?: ResultSubtype; numTurns?: number; contextWindowPct?: number }
|
|
93
|
-
| { type: "
|
|
110
|
+
| { type: "session_migrated"; oldSessionId: string; newSessionId: string }
|
|
111
|
+
| { type: "account_info"; accountId: string; accountLabel: string }
|
|
112
|
+
| { type: "system"; subtype: string };
|
|
94
113
|
|
|
95
114
|
export type ToolApprovalHandler = (
|
|
96
115
|
tool: string,
|
package/src/types/config.ts
CHANGED
|
@@ -44,18 +44,24 @@ const VALID_PERMISSION_MODES = ["default", "acceptEdits", "plan", "bypassPermiss
|
|
|
44
44
|
export type PermissionMode = typeof VALID_PERMISSION_MODES[number];
|
|
45
45
|
|
|
46
46
|
export interface AIProviderConfig {
|
|
47
|
-
type: "agent-sdk" | "mock";
|
|
47
|
+
type: "agent-sdk" | "cli" | "mock";
|
|
48
|
+
|
|
49
|
+
// Common fields (all providers)
|
|
50
|
+
permission_mode?: PermissionMode;
|
|
51
|
+
system_prompt?: string;
|
|
52
|
+
model?: string;
|
|
53
|
+
|
|
54
|
+
// SDK-specific (Claude)
|
|
48
55
|
api_key_env?: string;
|
|
49
56
|
api_key?: string;
|
|
50
57
|
base_url?: string;
|
|
51
|
-
// Agent SDK-specific settings (ignored by mock provider)
|
|
52
|
-
model?: string;
|
|
53
58
|
effort?: "low" | "medium" | "high" | "max";
|
|
54
59
|
max_turns?: number;
|
|
55
60
|
max_budget_usd?: number;
|
|
56
61
|
thinking_budget_tokens?: number;
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
|
|
63
|
+
// CLI-specific (Cursor, Codex, Gemini)
|
|
64
|
+
cli_command?: string;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
export const DEFAULT_CONFIG: PpmConfig = {
|
|
@@ -80,11 +86,13 @@ export const DEFAULT_CONFIG: PpmConfig = {
|
|
|
80
86
|
},
|
|
81
87
|
};
|
|
82
88
|
|
|
83
|
-
const VALID_TYPES = ["agent-sdk", "mock"] as const;
|
|
89
|
+
const VALID_TYPES = ["agent-sdk", "cli", "mock"] as const;
|
|
84
90
|
const VALID_EFFORTS = ["low", "medium", "high"] as const;
|
|
85
91
|
const VALID_MODELS = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"] as const;
|
|
92
|
+
/** Allowed CLI commands for CLI providers (prevents command injection) */
|
|
93
|
+
const VALID_CLI_COMMANDS = ["cursor-agent", "codex", "gemini"] as const;
|
|
86
94
|
/** Only these values are allowed for default_provider in config */
|
|
87
|
-
export const VALID_PROVIDERS = ["claude"] as const;
|
|
95
|
+
export const VALID_PROVIDERS = ["claude", "cursor"] as const;
|
|
88
96
|
const VALID_THEMES: ThemeConfig[] = ["light", "dark", "system"];
|
|
89
97
|
|
|
90
98
|
/** Validate AI provider config fields. Returns array of error messages (empty = valid). */
|
|
@@ -93,9 +101,22 @@ export function validateAIProviderConfig(config: Partial<AIProviderConfig>): str
|
|
|
93
101
|
if (config.type != null && !VALID_TYPES.includes(config.type as any)) {
|
|
94
102
|
errors.push(`type must be one of: ${VALID_TYPES.join(", ")}`);
|
|
95
103
|
}
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
|
|
105
|
+
// CLI-specific validation
|
|
106
|
+
if (config.type === "cli") {
|
|
107
|
+
if (!config.cli_command) {
|
|
108
|
+
errors.push("cli_command is required for CLI providers");
|
|
109
|
+
} else if (!VALID_CLI_COMMANDS.includes(config.cli_command as any)) {
|
|
110
|
+
errors.push(`cli_command must be one of: ${VALID_CLI_COMMANDS.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
// CLI providers accept any model string — skip VALID_MODELS check
|
|
113
|
+
} else {
|
|
114
|
+
// SDK/mock model validation
|
|
115
|
+
if (config.model != null && !VALID_MODELS.includes(config.model as any)) {
|
|
116
|
+
errors.push(`model must be one of: ${VALID_MODELS.join(", ")}`);
|
|
117
|
+
}
|
|
98
118
|
}
|
|
119
|
+
|
|
99
120
|
if (config.effort && !VALID_EFFORTS.includes(config.effort as any)) {
|
|
100
121
|
errors.push(`effort must be one of: ${VALID_EFFORTS.join(", ")}`);
|
|
101
122
|
}
|
|
@@ -149,8 +170,9 @@ export function sanitizeConfig(config: PpmConfig): boolean {
|
|
|
149
170
|
dirty = true;
|
|
150
171
|
}
|
|
151
172
|
|
|
152
|
-
// Fix invalid default_provider — must be in VALID_PROVIDERS
|
|
153
|
-
if (!VALID_PROVIDERS.includes(config.ai.default_provider as any)
|
|
173
|
+
// Fix invalid default_provider — must be in VALID_PROVIDERS or be a registered provider key
|
|
174
|
+
if (!VALID_PROVIDERS.includes(config.ai.default_provider as any) &&
|
|
175
|
+
!config.ai.providers[config.ai.default_provider]) {
|
|
154
176
|
config.ai.default_provider = DEFAULT_CONFIG.ai.default_provider;
|
|
155
177
|
dirty = true;
|
|
156
178
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Async generator: reads from a stream, buffers partial lines,
|
|
5
|
+
* yields parsed JSON objects one per complete line.
|
|
6
|
+
* Handles TCP packet splitting across JSON boundaries.
|
|
7
|
+
*/
|
|
8
|
+
export async function* parseNdjsonLines(stream: Readable): AsyncIterable<unknown> {
|
|
9
|
+
let buffer = "";
|
|
10
|
+
|
|
11
|
+
for await (const chunk of stream) {
|
|
12
|
+
buffer += chunk.toString();
|
|
13
|
+
const lines = buffer.split(/\r?\n/);
|
|
14
|
+
buffer = lines.pop() || ""; // keep trailing partial
|
|
15
|
+
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed) continue;
|
|
19
|
+
try {
|
|
20
|
+
yield JSON.parse(trimmed);
|
|
21
|
+
} catch {
|
|
22
|
+
// Skip non-JSON lines (e.g. deprecation warnings from CLI)
|
|
23
|
+
console.warn(`[ndjson] skipping non-JSON line: ${trimmed.slice(0, 100)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Flush remaining buffer
|
|
29
|
+
if (buffer.trim()) {
|
|
30
|
+
try {
|
|
31
|
+
yield JSON.parse(buffer.trim());
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore trailing non-JSON
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ArrowLeft,
|
|
4
|
+
ArrowRight,
|
|
5
|
+
RotateCcw,
|
|
6
|
+
ExternalLink,
|
|
7
|
+
Globe,
|
|
8
|
+
} from "lucide-react";
|
|
9
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
10
|
+
|
|
11
|
+
/** Parse a URL string — returns normalized URL or null if invalid */
|
|
12
|
+
function parseUrl(input: string): string | null {
|
|
13
|
+
let url = input.trim();
|
|
14
|
+
if (!url) return null;
|
|
15
|
+
|
|
16
|
+
// If just a port number, treat as localhost
|
|
17
|
+
if (/^\d+$/.test(url)) return `http://localhost:${url}`;
|
|
18
|
+
|
|
19
|
+
// If host:port without scheme, add http://
|
|
20
|
+
if (/^localhost(:\d+)?/.test(url)) url = `http://${url}`;
|
|
21
|
+
if (/^[\w.-]+:\d+/.test(url) && !url.includes("://")) url = `http://${url}`;
|
|
22
|
+
|
|
23
|
+
// If no scheme at all, add https:// for external, http:// for localhost
|
|
24
|
+
if (!url.includes("://")) {
|
|
25
|
+
url = url.includes("localhost") ? `http://${url}` : `https://${url}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
new URL(url);
|
|
30
|
+
return url;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Check if a URL is a localhost address */
|
|
37
|
+
function isLocalhost(url: string): boolean {
|
|
38
|
+
try {
|
|
39
|
+
const u = new URL(url);
|
|
40
|
+
return (
|
|
41
|
+
u.hostname === "localhost" ||
|
|
42
|
+
u.hostname === "127.0.0.1" ||
|
|
43
|
+
u.hostname === "0.0.0.0" ||
|
|
44
|
+
u.hostname === "::1"
|
|
45
|
+
);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Convert URL to iframe src — proxy localhost through backend */
|
|
52
|
+
function toIframeSrc(url: string): string {
|
|
53
|
+
if (!isLocalhost(url)) return url;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const u = new URL(url);
|
|
57
|
+
const port = u.port || "80";
|
|
58
|
+
const path = u.pathname + u.search + u.hash;
|
|
59
|
+
return `/api/preview/${port}${path}`;
|
|
60
|
+
} catch {
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Extract display URL from iframe src (reverse of toIframeSrc) */
|
|
66
|
+
function fromIframeSrc(src: string): string {
|
|
67
|
+
const match = src.match(/^\/api\/preview\/(\d+)(\/.*)?$/);
|
|
68
|
+
if (match) {
|
|
69
|
+
const port = match[1];
|
|
70
|
+
const path = match[2] || "/";
|
|
71
|
+
return `http://localhost:${port}${path}`;
|
|
72
|
+
}
|
|
73
|
+
return src;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface BrowserTabProps {
|
|
77
|
+
metadata?: Record<string, unknown>;
|
|
78
|
+
tabId?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function BrowserTab({ metadata, tabId }: BrowserTabProps) {
|
|
82
|
+
const initialUrl = (metadata?.url as string) || "http://localhost:3000";
|
|
83
|
+
const [addressBar, setAddressBar] = useState(initialUrl);
|
|
84
|
+
const [currentUrl, setCurrentUrl] = useState(initialUrl);
|
|
85
|
+
const [iframeSrc, setIframeSrc] = useState(toIframeSrc(initialUrl));
|
|
86
|
+
const [canGoBack, setCanGoBack] = useState(false);
|
|
87
|
+
const [canGoForward, setCanGoForward] = useState(false);
|
|
88
|
+
const [loading, setLoading] = useState(true);
|
|
89
|
+
const [error, setError] = useState<string | null>(null);
|
|
90
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
91
|
+
const updateTab = useTabStore((s) => s.updateTab);
|
|
92
|
+
|
|
93
|
+
// Navigation history (iframe same-origin only)
|
|
94
|
+
const historyRef = useRef<string[]>([initialUrl]);
|
|
95
|
+
const historyIdxRef = useRef(0);
|
|
96
|
+
|
|
97
|
+
const navigate = useCallback(
|
|
98
|
+
(url: string, addToHistory = true) => {
|
|
99
|
+
const parsed = parseUrl(url);
|
|
100
|
+
if (!parsed) {
|
|
101
|
+
setError("Invalid URL");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setError(null);
|
|
106
|
+
setCurrentUrl(parsed);
|
|
107
|
+
setAddressBar(parsed);
|
|
108
|
+
setIframeSrc(toIframeSrc(parsed));
|
|
109
|
+
setLoading(true);
|
|
110
|
+
|
|
111
|
+
if (addToHistory) {
|
|
112
|
+
const h = historyRef.current;
|
|
113
|
+
const idx = historyIdxRef.current;
|
|
114
|
+
// Truncate forward history
|
|
115
|
+
historyRef.current = h.slice(0, idx + 1);
|
|
116
|
+
historyRef.current.push(parsed);
|
|
117
|
+
historyIdxRef.current = historyRef.current.length - 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setCanGoBack(historyIdxRef.current > 0);
|
|
121
|
+
setCanGoForward(
|
|
122
|
+
historyIdxRef.current < historyRef.current.length - 1,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Update tab title
|
|
126
|
+
if (tabId) {
|
|
127
|
+
try {
|
|
128
|
+
const u = new URL(parsed);
|
|
129
|
+
const title = isLocalhost(parsed)
|
|
130
|
+
? `localhost:${u.port || "80"}`
|
|
131
|
+
: u.hostname;
|
|
132
|
+
updateTab(tabId, { title });
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
[tabId, updateTab],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const goBack = useCallback(() => {
|
|
140
|
+
if (historyIdxRef.current > 0) {
|
|
141
|
+
historyIdxRef.current--;
|
|
142
|
+
navigate(historyRef.current[historyIdxRef.current]!, false);
|
|
143
|
+
}
|
|
144
|
+
}, [navigate]);
|
|
145
|
+
|
|
146
|
+
const goForward = useCallback(() => {
|
|
147
|
+
if (historyIdxRef.current < historyRef.current.length - 1) {
|
|
148
|
+
historyIdxRef.current++;
|
|
149
|
+
navigate(historyRef.current[historyIdxRef.current]!, false);
|
|
150
|
+
}
|
|
151
|
+
}, [navigate]);
|
|
152
|
+
|
|
153
|
+
const reload = useCallback(() => {
|
|
154
|
+
setLoading(true);
|
|
155
|
+
setError(null);
|
|
156
|
+
if (iframeRef.current) {
|
|
157
|
+
// Force reload by re-setting src
|
|
158
|
+
const src = iframeRef.current.src;
|
|
159
|
+
iframeRef.current.src = "";
|
|
160
|
+
requestAnimationFrame(() => {
|
|
161
|
+
if (iframeRef.current) iframeRef.current.src = src;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const openExternal = useCallback(() => {
|
|
167
|
+
window.open(currentUrl, "_blank");
|
|
168
|
+
}, [currentUrl]);
|
|
169
|
+
|
|
170
|
+
const handleAddressKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
171
|
+
if (e.key === "Enter") {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
navigate(addressBar);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Navigate when metadata.url changes (e.g. opened from command palette)
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const metaUrl = metadata?.url as string | undefined;
|
|
180
|
+
if (metaUrl && metaUrl !== currentUrl) {
|
|
181
|
+
navigate(metaUrl);
|
|
182
|
+
}
|
|
183
|
+
}, [metadata?.url]);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex flex-col h-full w-full bg-background">
|
|
187
|
+
{/* Toolbar */}
|
|
188
|
+
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-border bg-surface shrink-0">
|
|
189
|
+
{/* Nav buttons */}
|
|
190
|
+
<button
|
|
191
|
+
onClick={goBack}
|
|
192
|
+
disabled={!canGoBack}
|
|
193
|
+
className="p-1.5 rounded hover:bg-surface-elevated disabled:opacity-30 transition-colors"
|
|
194
|
+
title="Back"
|
|
195
|
+
>
|
|
196
|
+
<ArrowLeft className="size-4" />
|
|
197
|
+
</button>
|
|
198
|
+
<button
|
|
199
|
+
onClick={goForward}
|
|
200
|
+
disabled={!canGoForward}
|
|
201
|
+
className="p-1.5 rounded hover:bg-surface-elevated disabled:opacity-30 transition-colors"
|
|
202
|
+
title="Forward"
|
|
203
|
+
>
|
|
204
|
+
<ArrowRight className="size-4" />
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
onClick={reload}
|
|
208
|
+
className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
|
|
209
|
+
title="Reload"
|
|
210
|
+
>
|
|
211
|
+
<RotateCcw className={`size-4 ${loading ? "animate-spin" : ""}`} />
|
|
212
|
+
</button>
|
|
213
|
+
|
|
214
|
+
{/* Address bar */}
|
|
215
|
+
<div className="flex-1 flex items-center gap-2 mx-1 px-2.5 py-1.5 rounded-md bg-background border border-border focus-within:border-accent/50 transition-colors">
|
|
216
|
+
<Globe className="size-3.5 text-text-subtle shrink-0" />
|
|
217
|
+
<input
|
|
218
|
+
type="text"
|
|
219
|
+
value={addressBar}
|
|
220
|
+
onChange={(e) => setAddressBar(e.target.value)}
|
|
221
|
+
onKeyDown={handleAddressKeyDown}
|
|
222
|
+
placeholder="Enter URL or port (e.g. 3000, localhost:8080)"
|
|
223
|
+
className="flex-1 bg-transparent text-xs text-text-primary outline-none placeholder:text-text-subtle min-w-0"
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Open external */}
|
|
228
|
+
<button
|
|
229
|
+
onClick={openExternal}
|
|
230
|
+
className="p-1.5 rounded hover:bg-surface-elevated transition-colors"
|
|
231
|
+
title="Open in browser"
|
|
232
|
+
>
|
|
233
|
+
<ExternalLink className="size-4" />
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Content */}
|
|
238
|
+
<div className="flex-1 relative min-h-0">
|
|
239
|
+
{error ? (
|
|
240
|
+
<div className="flex items-center justify-center h-full text-text-secondary text-sm">
|
|
241
|
+
<p>{error}</p>
|
|
242
|
+
</div>
|
|
243
|
+
) : (
|
|
244
|
+
<iframe
|
|
245
|
+
ref={iframeRef}
|
|
246
|
+
src={iframeSrc}
|
|
247
|
+
className="w-full h-full border-0"
|
|
248
|
+
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-modals"
|
|
249
|
+
onLoad={() => setLoading(false)}
|
|
250
|
+
onError={() => {
|
|
251
|
+
setLoading(false);
|
|
252
|
+
setError(`Failed to load ${currentUrl}`);
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
{/* Loading overlay */}
|
|
258
|
+
{loading && !error && (
|
|
259
|
+
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
|
|
260
|
+
<div className="flex items-center gap-2 text-sm text-text-secondary">
|
|
261
|
+
<RotateCcw className="size-4 animate-spin" />
|
|
262
|
+
<span>Loading...</span>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import { History, Settings2, Loader2,
|
|
2
|
+
import { History, Settings2, Loader2, RefreshCw, Search, Pencil, Check, X, BellOff } from "lucide-react";
|
|
3
3
|
import { Activity } from "lucide-react";
|
|
4
4
|
import { api, projectUrl } from "@/lib/api-client";
|
|
5
5
|
import { useTabStore } from "@/stores/tab-store";
|
|
6
6
|
import { useNotificationStore } from "@/stores/notification-store";
|
|
7
7
|
import { AISettingsSection } from "@/components/settings/ai-settings-section";
|
|
8
8
|
import { UsageDetailPanel } from "./usage-badge";
|
|
9
|
+
import { ProviderBadge } from "./provider-selector";
|
|
9
10
|
import type { SessionInfo } from "../../../types/chat";
|
|
10
11
|
import type { UsageInfo } from "../../../types/chat";
|
|
11
12
|
|
|
@@ -19,6 +20,7 @@ interface ChatHistoryBarProps {
|
|
|
19
20
|
refreshUsage?: () => void;
|
|
20
21
|
lastFetchedAt?: string | null;
|
|
21
22
|
sessionId?: string | null;
|
|
23
|
+
providerId?: string;
|
|
22
24
|
onSelectSession?: (session: SessionInfo) => void;
|
|
23
25
|
onBugReport?: () => void;
|
|
24
26
|
isConnected?: boolean;
|
|
@@ -50,7 +52,7 @@ function pctColor(pct: number): string {
|
|
|
50
52
|
|
|
51
53
|
export function ChatHistoryBar({
|
|
52
54
|
projectName, usageInfo, contextWindowPct, usageLoading, refreshUsage, lastFetchedAt,
|
|
53
|
-
sessionId, onSelectSession, onBugReport, isConnected, onReconnect,
|
|
55
|
+
sessionId, providerId, onSelectSession, onBugReport, isConnected, onReconnect,
|
|
54
56
|
}: ChatHistoryBarProps) {
|
|
55
57
|
const [activePanel, setActivePanel] = useState<PanelType>(null);
|
|
56
58
|
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
@@ -94,7 +96,7 @@ export function ChatHistoryBar({
|
|
|
94
96
|
type: "chat",
|
|
95
97
|
title: session.title || "Chat",
|
|
96
98
|
projectId: projectName ?? null,
|
|
97
|
-
metadata: { projectName, sessionId: session.id },
|
|
99
|
+
metadata: { projectName, sessionId: session.id, providerId: session.providerId },
|
|
98
100
|
closable: true,
|
|
99
101
|
});
|
|
100
102
|
}
|
|
@@ -126,7 +128,8 @@ export function ChatHistoryBar({
|
|
|
126
128
|
? sessions.filter((s) => (s.title || "").toLowerCase().includes(searchQuery.toLowerCase()))
|
|
127
129
|
: sessions;
|
|
128
130
|
|
|
129
|
-
// Usage badge display
|
|
131
|
+
// Usage badge display — only meaningful for Claude (SDK) provider
|
|
132
|
+
const isClaudeProvider = !providerId || providerId === "claude";
|
|
130
133
|
const fiveHourPct = usageInfo.fiveHour != null ? Math.round(usageInfo.fiveHour * 100) : null;
|
|
131
134
|
const sevenDayPct = usageInfo.sevenDay != null ? Math.round(usageInfo.sevenDay * 100) : null;
|
|
132
135
|
const worstPct = Math.max(fiveHourPct ?? 0, sevenDayPct ?? 0);
|
|
@@ -147,6 +150,14 @@ export function ChatHistoryBar({
|
|
|
147
150
|
<span>History</span>
|
|
148
151
|
</button>
|
|
149
152
|
|
|
153
|
+
{/* Active provider indicator */}
|
|
154
|
+
{sessionId && providerId && providerId !== "mock" && (
|
|
155
|
+
<span className="flex items-center gap-1 px-1.5 py-0.5 text-[11px] text-text-secondary">
|
|
156
|
+
<ProviderBadge providerId={providerId} />
|
|
157
|
+
<span className="capitalize">{providerId}</span>
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
|
|
150
161
|
{/* Config */}
|
|
151
162
|
<button
|
|
152
163
|
onClick={() => togglePanel("config")}
|
|
@@ -158,28 +169,37 @@ export function ChatHistoryBar({
|
|
|
158
169
|
<Settings2 className="size-3" />
|
|
159
170
|
</button>
|
|
160
171
|
|
|
161
|
-
{/* Usage & Accounts */}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
172
|
+
{/* Usage & Accounts — full display for Claude, minimal for other providers */}
|
|
173
|
+
{isClaudeProvider ? (
|
|
174
|
+
<button
|
|
175
|
+
onClick={() => togglePanel("usage")}
|
|
176
|
+
className={`flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] font-medium tabular-nums transition-colors hover:bg-surface-elevated ${
|
|
177
|
+
activePanel === "usage" ? "bg-primary/10" : ""
|
|
178
|
+
} ${usageColor}`}
|
|
179
|
+
title="Usage limits"
|
|
180
|
+
>
|
|
181
|
+
<Activity className="size-3" />
|
|
182
|
+
{usageInfo.activeAccountLabel && (
|
|
183
|
+
<span className="text-text-secondary font-normal truncate max-w-[60px]">[{usageInfo.activeAccountLabel}]</span>
|
|
184
|
+
)}
|
|
185
|
+
<span>5h:{fiveHourPct != null ? `${fiveHourPct}%` : "--%"}</span>
|
|
186
|
+
<span className="text-text-subtle">·</span>
|
|
187
|
+
<span>Wk:{sevenDayPct != null ? `${sevenDayPct}%` : "--%"}</span>
|
|
188
|
+
{contextWindowPct != null && (
|
|
189
|
+
<>
|
|
190
|
+
<span className="text-text-subtle">·</span>
|
|
191
|
+
<span className={pctColor(contextWindowPct)}>Ctx:{contextWindowPct}%</span>
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
</button>
|
|
195
|
+
) : (
|
|
196
|
+
contextWindowPct != null && (
|
|
197
|
+
<span className={`flex items-center gap-1 px-1.5 py-0.5 text-[11px] font-medium tabular-nums ${pctColor(contextWindowPct)}`}>
|
|
198
|
+
<Activity className="size-3" />
|
|
199
|
+
<span>Ctx:{contextWindowPct}%</span>
|
|
200
|
+
</span>
|
|
201
|
+
)
|
|
202
|
+
)}
|
|
183
203
|
|
|
184
204
|
{/* Spacer */}
|
|
185
205
|
<div className="flex-1" />
|
|
@@ -247,7 +267,7 @@ export function ChatHistoryBar({
|
|
|
247
267
|
key={session.id}
|
|
248
268
|
className="flex items-center gap-2 w-full px-3 py-1.5 text-left hover:bg-surface-elevated transition-colors group"
|
|
249
269
|
>
|
|
250
|
-
<
|
|
270
|
+
<ProviderBadge providerId={session.providerId} />
|
|
251
271
|
{editingId === session.id ? (
|
|
252
272
|
<form
|
|
253
273
|
className="flex items-center gap-1 flex-1 min-w-0"
|
|
@@ -303,8 +323,8 @@ export function ChatHistoryBar({
|
|
|
303
323
|
</div>
|
|
304
324
|
)}
|
|
305
325
|
|
|
306
|
-
{/* Usage panel */}
|
|
307
|
-
{activePanel === "usage" && (
|
|
326
|
+
{/* Usage panel — only for Claude provider */}
|
|
327
|
+
{activePanel === "usage" && isClaudeProvider && (
|
|
308
328
|
<UsageDetailPanel
|
|
309
329
|
usage={usageInfo}
|
|
310
330
|
visible={true}
|