@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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
5
|
+
import type { ChatMessage, SessionInfo } from "../provider.interface.ts";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CHATS_DIR = join(homedir(), ".cursor", "chats");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List all Cursor sessions found in ~/.cursor/chats/.
|
|
11
|
+
* Scans directory structure: {cwdHash}/{sessionId}/store.db
|
|
12
|
+
* Reads meta table for session name and createdAt.
|
|
13
|
+
* @param chatsDir — override for testing (defaults to ~/.cursor/chats)
|
|
14
|
+
*/
|
|
15
|
+
export async function listCursorSessions(providerId: string, chatsDir?: string): Promise<SessionInfo[]> {
|
|
16
|
+
const dir = chatsDir ?? DEFAULT_CHATS_DIR;
|
|
17
|
+
if (!existsSync(dir)) return [];
|
|
18
|
+
const { Database } = await import("bun:sqlite");
|
|
19
|
+
const sessions: SessionInfo[] = [];
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
for (const cwdHash of readdirSync(dir)) {
|
|
23
|
+
const cwdDir = join(dir, cwdHash);
|
|
24
|
+
try {
|
|
25
|
+
for (const sessionId of readdirSync(cwdDir)) {
|
|
26
|
+
const dbPath = join(cwdDir, sessionId, "store.db");
|
|
27
|
+
if (!existsSync(dbPath)) continue;
|
|
28
|
+
|
|
29
|
+
let title = `Cursor ${sessionId.slice(0, 8)}`;
|
|
30
|
+
let createdAt = new Date().toISOString();
|
|
31
|
+
|
|
32
|
+
// Read meta table for name + createdAt (value is hex-encoded JSON)
|
|
33
|
+
try {
|
|
34
|
+
const db = new Database(dbPath, { readonly: true });
|
|
35
|
+
const row = db.query("SELECT value FROM meta LIMIT 1").get() as { value: string | Buffer } | null;
|
|
36
|
+
db.close();
|
|
37
|
+
if (row?.value) {
|
|
38
|
+
const hex = typeof row.value === "string" ? row.value : Buffer.from(row.value).toString("utf-8");
|
|
39
|
+
const json = Buffer.from(hex, "hex").toString("utf-8");
|
|
40
|
+
const meta = JSON.parse(json);
|
|
41
|
+
if (meta.name) title = meta.name.split("\n")[0].slice(0, 80);
|
|
42
|
+
if (meta.createdAt) createdAt = new Date(meta.createdAt).toISOString();
|
|
43
|
+
}
|
|
44
|
+
} catch { /* use defaults */ }
|
|
45
|
+
|
|
46
|
+
sessions.push({ id: sessionId, providerId, title, createdAt });
|
|
47
|
+
}
|
|
48
|
+
} catch { /* skip unreadable dir */ }
|
|
49
|
+
}
|
|
50
|
+
} catch { /* skip if chats dir unreadable */ }
|
|
51
|
+
|
|
52
|
+
return sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load chat history from Cursor's SQLite DAG storage.
|
|
57
|
+
* Path: ~/.cursor/chats/{MD5(cwd)}/{sessionId}/store.db
|
|
58
|
+
* Falls back to scanning all cwdHash dirs if projectPath doesn't match.
|
|
59
|
+
*/
|
|
60
|
+
export async function loadCursorHistory(
|
|
61
|
+
sessionId: string,
|
|
62
|
+
projectPath?: string,
|
|
63
|
+
chatsDir?: string,
|
|
64
|
+
): Promise<ChatMessage[]> {
|
|
65
|
+
const baseDir = chatsDir ?? DEFAULT_CHATS_DIR;
|
|
66
|
+
let dbPath: string | null = null;
|
|
67
|
+
|
|
68
|
+
// Try direct path first (fast path when projectPath is known)
|
|
69
|
+
if (projectPath) {
|
|
70
|
+
const cwdHash = createHash("md5").update(projectPath).digest("hex");
|
|
71
|
+
const candidate = join(baseDir, cwdHash, sessionId, "store.db");
|
|
72
|
+
if (existsSync(candidate)) dbPath = candidate;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fallback: scan all cwdHash dirs for this sessionId
|
|
76
|
+
if (!dbPath && existsSync(baseDir)) {
|
|
77
|
+
try {
|
|
78
|
+
for (const cwdHash of readdirSync(baseDir)) {
|
|
79
|
+
const candidate = join(baseDir, cwdHash, sessionId, "store.db");
|
|
80
|
+
if (existsSync(candidate)) { dbPath = candidate; break; }
|
|
81
|
+
}
|
|
82
|
+
} catch { /* skip */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!dbPath) return [];
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Use Bun's native SQLite
|
|
89
|
+
const { Database } = await import("bun:sqlite");
|
|
90
|
+
const db = new Database(dbPath, { readonly: true });
|
|
91
|
+
|
|
92
|
+
const blobs = db
|
|
93
|
+
.query("SELECT rowid, id, data FROM blobs ORDER BY rowid")
|
|
94
|
+
.all() as Array<{ rowid: number; id: string; data: Buffer }>;
|
|
95
|
+
db.close();
|
|
96
|
+
|
|
97
|
+
return parseDagBlobs(blobs, sessionId);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.warn(`[cursor-history] Failed to load session ${sessionId}:`, err);
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Parse DAG blobs into ordered ChatMessages */
|
|
105
|
+
function parseDagBlobs(
|
|
106
|
+
blobs: Array<{ rowid: number; id: string; data: Buffer }>,
|
|
107
|
+
_sessionId: string,
|
|
108
|
+
): ChatMessage[] {
|
|
109
|
+
const messages: ChatMessage[] = [];
|
|
110
|
+
|
|
111
|
+
for (const blob of blobs) {
|
|
112
|
+
try {
|
|
113
|
+
const text = extractTextContent(blob.data);
|
|
114
|
+
if (!text) continue;
|
|
115
|
+
|
|
116
|
+
// Try to parse as JSON
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(text);
|
|
119
|
+
|
|
120
|
+
// Format: { role: "...", content: "..." | [...] } — structured message
|
|
121
|
+
// Skip system prompts — they're huge and not useful for history
|
|
122
|
+
if (parsed.role === "system") continue;
|
|
123
|
+
if (parsed.role && parsed.content) {
|
|
124
|
+
let content: string;
|
|
125
|
+
if (typeof parsed.content === "string") {
|
|
126
|
+
content = parsed.content;
|
|
127
|
+
} else if (Array.isArray(parsed.content)) {
|
|
128
|
+
// Content parts: [{ type: "text", text: "..." }, ...]
|
|
129
|
+
content = parsed.content
|
|
130
|
+
.filter((p: any) => p.type === "text" && p.text)
|
|
131
|
+
.map((p: any) => p.text)
|
|
132
|
+
.join("\n") || JSON.stringify(parsed.content);
|
|
133
|
+
} else {
|
|
134
|
+
content = JSON.stringify(parsed.content);
|
|
135
|
+
}
|
|
136
|
+
// Filter out Cursor's injected <user_info> system context messages
|
|
137
|
+
if (parsed.role === "user" && /^<user_info>\s/i.test(content.trimStart())) continue;
|
|
138
|
+
// Strip Cursor's <user_query> wrapper from user messages
|
|
139
|
+
if (parsed.role === "user") {
|
|
140
|
+
const match = content.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/);
|
|
141
|
+
if (match?.[1]) content = match[1];
|
|
142
|
+
}
|
|
143
|
+
messages.push({
|
|
144
|
+
id: blob.id,
|
|
145
|
+
role: parsed.role,
|
|
146
|
+
content,
|
|
147
|
+
timestamp: new Date().toISOString(),
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Format: [{ type: "text", text: "..." }] — content parts array
|
|
153
|
+
if (Array.isArray(parsed)) {
|
|
154
|
+
const textParts = parsed
|
|
155
|
+
.filter((p: any) => p.type === "text" && p.text)
|
|
156
|
+
.map((p: any) => p.text)
|
|
157
|
+
.join("\n");
|
|
158
|
+
if (textParts) {
|
|
159
|
+
messages.push({
|
|
160
|
+
id: blob.id,
|
|
161
|
+
role: messages.length % 2 === 0 ? "user" : "assistant",
|
|
162
|
+
content: textParts,
|
|
163
|
+
timestamp: new Date().toISOString(),
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch { /* not JSON */ }
|
|
169
|
+
} catch { /* skip corrupt blob */ }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return messages;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Extract readable text from a DAG blob.
|
|
177
|
+
* Handles 2 known formats:
|
|
178
|
+
* 1. UTF-8 JSON string starting with { or [ (role/content messages)
|
|
179
|
+
* 2. JSON array (content parts like [{type:"text",text:"..."}])
|
|
180
|
+
* Skips binary DAG metadata blobs (parent refs, headers).
|
|
181
|
+
*/
|
|
182
|
+
function extractTextContent(data: Buffer | Uint8Array): string | null {
|
|
183
|
+
if (!data || data.length === 0) return null;
|
|
184
|
+
|
|
185
|
+
const buf = Buffer.from(data);
|
|
186
|
+
|
|
187
|
+
// Quick binary check: if first byte is not printable ASCII, it's a DAG metadata blob
|
|
188
|
+
const firstByte = buf[0];
|
|
189
|
+
if (firstByte !== undefined && firstByte < 0x20 && firstByte !== 0x0a && firstByte !== 0x0d && firstByte !== 0x09) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const text = buf.toString("utf-8");
|
|
194
|
+
|
|
195
|
+
// Only accept clean JSON starting with { or [
|
|
196
|
+
if (text.startsWith("{") || text.startsWith("[")) {
|
|
197
|
+
// Validate it's actually parseable JSON
|
|
198
|
+
try {
|
|
199
|
+
JSON.parse(text);
|
|
200
|
+
return text;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { CliProvider } from "../cli-provider-base.ts";
|
|
2
|
+
import { mapCursorEvent } from "./cursor-event-mapper.ts";
|
|
3
|
+
import { listCursorSessions, loadCursorHistory } from "./cursor-history.ts";
|
|
4
|
+
import type { ChatEvent, ChatMessage, SessionInfo, ModelOption } from "../provider.interface.ts";
|
|
5
|
+
import type { ChildProcess } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const TRUST_PATTERNS = [
|
|
8
|
+
/workspace trust required/i,
|
|
9
|
+
/do you trust the contents/i,
|
|
10
|
+
/pass --trust/i,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cursor CLI provider — spawns `cursor-agent` with NDJSON streaming.
|
|
15
|
+
* Extends CliProvider with Cursor-specific event mapping, arg building,
|
|
16
|
+
* workspace trust auto-retry, and SQLite DAG history.
|
|
17
|
+
*/
|
|
18
|
+
export class CursorCliProvider extends CliProvider {
|
|
19
|
+
readonly id = "cursor";
|
|
20
|
+
readonly name = "Cursor";
|
|
21
|
+
readonly cliCommand = "cursor-agent";
|
|
22
|
+
|
|
23
|
+
async isAvailable(): Promise<boolean> {
|
|
24
|
+
try {
|
|
25
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
26
|
+
const proc = Bun.spawn([cmd, "cursor-agent"], { stdout: "pipe", stderr: "pipe" });
|
|
27
|
+
await proc.exited;
|
|
28
|
+
return proc.exitCode === 0;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
buildArgs(params: {
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
message: string;
|
|
37
|
+
model?: string;
|
|
38
|
+
permissionMode?: string;
|
|
39
|
+
isResume: boolean;
|
|
40
|
+
}): string[] {
|
|
41
|
+
const args: string[] = [];
|
|
42
|
+
|
|
43
|
+
if (params.sessionId && params.isResume) {
|
|
44
|
+
args.push(`--resume=${params.sessionId}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
args.push("-p", params.message);
|
|
48
|
+
|
|
49
|
+
if (!params.isResume && params.model) {
|
|
50
|
+
args.push("--model", params.model);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
args.push("--output-format", "stream-json");
|
|
54
|
+
|
|
55
|
+
// Permission mode → CLI flags
|
|
56
|
+
const mode = params.permissionMode || "default";
|
|
57
|
+
if (mode === "bypassPermissions") {
|
|
58
|
+
args.push("-f");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return args;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
mapEvent(raw: unknown, sessionId: string): ChatEvent[] {
|
|
65
|
+
return mapCursorEvent(raw, sessionId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
extractSessionId(raw: unknown): string | null {
|
|
69
|
+
const obj = raw as Record<string, unknown>;
|
|
70
|
+
if (obj?.type === "system" && obj?.subtype === "init") {
|
|
71
|
+
return (obj.session_id as string) || null;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Override listSessions to include Cursor's native history
|
|
77
|
+
override async listSessions(): Promise<SessionInfo[]> {
|
|
78
|
+
const inMemory = await super.listSessions();
|
|
79
|
+
try {
|
|
80
|
+
const native = await listCursorSessions(this.id);
|
|
81
|
+
// Merge: in-memory first, then native (deduplicated)
|
|
82
|
+
const seen = new Set(inMemory.map((s) => s.id));
|
|
83
|
+
const merged = [...inMemory];
|
|
84
|
+
for (const s of native) {
|
|
85
|
+
if (!seen.has(s.id)) merged.push(s);
|
|
86
|
+
}
|
|
87
|
+
return merged;
|
|
88
|
+
} catch {
|
|
89
|
+
return inMemory;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Optional: load history from SQLite
|
|
94
|
+
async getMessages(sessionId: string): Promise<ChatMessage[]> {
|
|
95
|
+
const meta = this.sessions.get(sessionId);
|
|
96
|
+
return loadCursorHistory(sessionId, meta?.projectPath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Cached models list with TTL from `cursor-agent --list-models` */
|
|
100
|
+
private modelsCache: { models: ModelOption[]; expiry: number } | null = null;
|
|
101
|
+
private static CACHE_TTL = 5 * 60 * 1000; // 5 min
|
|
102
|
+
|
|
103
|
+
async listModels(): Promise<ModelOption[]> {
|
|
104
|
+
if (this.modelsCache && Date.now() < this.modelsCache.expiry) {
|
|
105
|
+
return this.modelsCache.models;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const proc = Bun.spawn(["cursor-agent", "--list-models"], {
|
|
109
|
+
stdout: "pipe",
|
|
110
|
+
stderr: "pipe",
|
|
111
|
+
});
|
|
112
|
+
const timeout = setTimeout(() => proc.kill(), 10_000);
|
|
113
|
+
const text = await new Response(proc.stdout).text();
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
await proc.exited;
|
|
116
|
+
const models: ModelOption[] = [];
|
|
117
|
+
for (const line of text.split("\n")) {
|
|
118
|
+
// Format: "model-id - Model Label" or "model-id - Model Label (current, default)"
|
|
119
|
+
const match = line.match(/^(\S+)\s+-\s+(.+?)(?:\s+\(.*\))?$/);
|
|
120
|
+
if (match?.[1] && match[2]) {
|
|
121
|
+
models.push({ value: match[1], label: match[2].trim() });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (models.length > 0) {
|
|
125
|
+
this.modelsCache = { models, expiry: Date.now() + CursorCliProvider.CACHE_TTL };
|
|
126
|
+
}
|
|
127
|
+
return models;
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Workspace trust detection: log warning so user knows to re-run with --trust
|
|
134
|
+
protected override spawnProcess(args: string[], cwd: string): ChildProcess {
|
|
135
|
+
const proc = super.spawnProcess(args, cwd);
|
|
136
|
+
|
|
137
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
138
|
+
const text = data.toString();
|
|
139
|
+
if (TRUST_PATTERNS.some((p) => p.test(text))) {
|
|
140
|
+
console.warn("[cursor] Workspace trust prompt detected. Re-run with bypassPermissions mode or add --trust flag.");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return proc;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -19,11 +19,17 @@ class ProviderRegistry {
|
|
|
19
19
|
return this.providers.get(id);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** List providers visible to users (excludes internal-only providers like mock) */
|
|
22
23
|
list(): ProviderInfo[] {
|
|
23
|
-
return Array.from(this.providers.values())
|
|
24
|
-
|
|
25
|
-
name: p.name
|
|
26
|
-
|
|
24
|
+
return Array.from(this.providers.values())
|
|
25
|
+
.filter((p) => p.id !== "mock")
|
|
26
|
+
.map((p) => ({ id: p.id, name: p.name }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** List all registered providers including internal ones (for ChatService aggregation) */
|
|
30
|
+
listAll(): ProviderInfo[] {
|
|
31
|
+
return Array.from(this.providers.values())
|
|
32
|
+
.map((p) => ({ id: p.id, name: p.name }));
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/** Get the default provider based on config's default_provider */
|
|
@@ -40,5 +46,38 @@ class ProviderRegistry {
|
|
|
40
46
|
|
|
41
47
|
/** Singleton registry */
|
|
42
48
|
export const providerRegistry = new ProviderRegistry();
|
|
49
|
+
|
|
50
|
+
// SDK providers registered synchronously (no binary check needed)
|
|
43
51
|
providerRegistry.register(new ClaudeAgentSdkProvider());
|
|
44
52
|
providerRegistry.register(new MockProvider()); // testing only
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Bootstrap CLI providers asynchronously.
|
|
56
|
+
* Checks isAvailable() before registering — call at server startup.
|
|
57
|
+
*/
|
|
58
|
+
export async function bootstrapProviders(): Promise<void> {
|
|
59
|
+
try {
|
|
60
|
+
const { CursorCliProvider } = await import("./cursor-cli/cursor-provider.ts");
|
|
61
|
+
const cursor = new CursorCliProvider();
|
|
62
|
+
if (await cursor.isAvailable()) {
|
|
63
|
+
providerRegistry.register(cursor);
|
|
64
|
+
// Ensure config has an entry for cursor so settings UI shows it
|
|
65
|
+
const ai = configService.get("ai");
|
|
66
|
+
if (!ai.providers["cursor"]) {
|
|
67
|
+
configService.set("ai", {
|
|
68
|
+
...ai,
|
|
69
|
+
providers: {
|
|
70
|
+
...ai.providers,
|
|
71
|
+
cursor: { type: "cli", cli_command: "cursor-agent", permission_mode: "bypassPermissions" },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
configService.save();
|
|
75
|
+
}
|
|
76
|
+
console.log("[registry] Cursor provider registered (cursor-agent found)");
|
|
77
|
+
} else {
|
|
78
|
+
console.log("[registry] Cursor provider skipped (cursor-agent not found)");
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.warn("[registry] Failed to load Cursor provider:", (e as Error).message);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { databaseRoutes } from "./routes/database.ts";
|
|
|
14
14
|
import { fsBrowseRoutes } from "./routes/fs-browse.ts";
|
|
15
15
|
import { accountsRoutes } from "./routes/accounts.ts";
|
|
16
16
|
import { proxyRoutes } from "./routes/proxy.ts";
|
|
17
|
+
import { browserPreviewRoutes } from "./routes/browser-preview.ts";
|
|
17
18
|
import { initAdapters } from "../services/database/init-adapters.ts";
|
|
18
19
|
import { terminalWebSocket } from "./ws/terminal.ts";
|
|
19
20
|
import { chatWebSocket } from "./ws/chat.ts";
|
|
@@ -126,6 +127,9 @@ app.route("/proxy", proxyRoutes);
|
|
|
126
127
|
app.use("/api/*", authMiddleware);
|
|
127
128
|
app.get("/api/auth/check", (c) => c.json(ok(true)));
|
|
128
129
|
|
|
130
|
+
// Browser preview reverse proxy — proxies to localhost:<port> for iframe embedding
|
|
131
|
+
app.route("/api/preview", browserPreviewRoutes);
|
|
132
|
+
|
|
129
133
|
// Filesystem operations (browse, list, read, write) — consolidated in fs-browse route
|
|
130
134
|
app.route("/api/fs", fsBrowseRoutes);
|
|
131
135
|
|
|
@@ -166,6 +170,10 @@ export async function startServer(options: {
|
|
|
166
170
|
// Setup log file (both foreground and daemon modes)
|
|
167
171
|
await setupLogFile();
|
|
168
172
|
|
|
173
|
+
// Bootstrap CLI providers (checks binary availability)
|
|
174
|
+
const { bootstrapProviders } = await import("../providers/registry.ts");
|
|
175
|
+
await bootstrapProviders();
|
|
176
|
+
|
|
169
177
|
// Check if port is already in use before starting.
|
|
170
178
|
// Skip in hot-reload mode — Bun.serve() replaces the previous server on the same port,
|
|
171
179
|
// but a net.createServer() probe would see it as "in use" and exit prematurely.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser preview reverse proxy — forwards requests to localhost:<port>.
|
|
5
|
+
* Mounted at /api/preview/:port/* so the frontend iframe can load
|
|
6
|
+
* any localhost dev server through PPM's own origin (avoiding CORS/framing issues).
|
|
7
|
+
*/
|
|
8
|
+
export const browserPreviewRoutes = new Hono();
|
|
9
|
+
|
|
10
|
+
/** Only allow proxying to localhost ports (security: prevent SSRF) */
|
|
11
|
+
function isValidPort(port: string): boolean {
|
|
12
|
+
const n = parseInt(port, 10);
|
|
13
|
+
return !isNaN(n) && n >= 1 && n <= 65535;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
browserPreviewRoutes.all("/:port{[0-9]+}/*", async (c) => {
|
|
17
|
+
const port = c.req.param("port");
|
|
18
|
+
if (!isValidPort(port)) {
|
|
19
|
+
return c.text("Invalid port", 400);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Build target URL — strip the /api/preview/:port prefix
|
|
23
|
+
const url = new URL(c.req.url);
|
|
24
|
+
const prefix = `/api/preview/${port}`;
|
|
25
|
+
const targetPath = url.pathname.slice(prefix.length) || "/";
|
|
26
|
+
const targetUrl = `http://localhost:${port}${targetPath}${url.search}`;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Forward the request with original method, headers, and body
|
|
30
|
+
const headers = new Headers(c.req.raw.headers);
|
|
31
|
+
// Remove host header so target server sees localhost
|
|
32
|
+
headers.delete("host");
|
|
33
|
+
|
|
34
|
+
const resp = await fetch(targetUrl, {
|
|
35
|
+
method: c.req.method,
|
|
36
|
+
headers,
|
|
37
|
+
body: ["GET", "HEAD"].includes(c.req.method) ? undefined : c.req.raw.body,
|
|
38
|
+
redirect: "manual",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Clone response headers, remove framing restrictions so iframe works
|
|
42
|
+
const respHeaders = new Headers(resp.headers);
|
|
43
|
+
respHeaders.delete("x-frame-options");
|
|
44
|
+
respHeaders.delete("content-security-policy");
|
|
45
|
+
|
|
46
|
+
return new Response(resp.body, {
|
|
47
|
+
status: resp.status,
|
|
48
|
+
statusText: resp.statusText,
|
|
49
|
+
headers: respHeaders,
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
return c.text(`Cannot connect to localhost:${port}`, 502);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Handle root path (no trailing slash)
|
|
57
|
+
browserPreviewRoutes.all("/:port{[0-9]+}", async (c) => {
|
|
58
|
+
const port = c.req.param("port");
|
|
59
|
+
if (!isValidPort(port)) {
|
|
60
|
+
return c.text("Invalid port", 400);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const url = new URL(c.req.url);
|
|
64
|
+
const targetUrl = `http://localhost:${port}/${url.search}`;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const headers = new Headers(c.req.raw.headers);
|
|
68
|
+
headers.delete("host");
|
|
69
|
+
|
|
70
|
+
const resp = await fetch(targetUrl, {
|
|
71
|
+
method: c.req.method,
|
|
72
|
+
headers,
|
|
73
|
+
body: ["GET", "HEAD"].includes(c.req.method) ? undefined : c.req.raw.body,
|
|
74
|
+
redirect: "manual",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const respHeaders = new Headers(resp.headers);
|
|
78
|
+
respHeaders.delete("x-frame-options");
|
|
79
|
+
respHeaders.delete("content-security-policy");
|
|
80
|
+
|
|
81
|
+
return new Response(resp.body, {
|
|
82
|
+
status: resp.status,
|
|
83
|
+
statusText: resp.statusText,
|
|
84
|
+
headers: respHeaders,
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
return c.text(`Cannot connect to localhost:${port}`, 502);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -57,6 +57,19 @@ chatRoutes.get("/providers", (c) => {
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
/** GET /chat/providers/:providerId/models — list available models for a provider */
|
|
61
|
+
chatRoutes.get("/providers/:providerId/models", async (c) => {
|
|
62
|
+
try {
|
|
63
|
+
const providerId = c.req.param("providerId");
|
|
64
|
+
const provider = providerRegistry.get(providerId);
|
|
65
|
+
if (!provider) return c.json(err(`Provider "${providerId}" not found`), 404);
|
|
66
|
+
const models = await provider.listModels?.() ?? [];
|
|
67
|
+
return c.json(ok(models));
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return c.json(err((e as Error).message), 500);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
60
73
|
/** GET /chat/sessions — list chat sessions filtered by project from context */
|
|
61
74
|
chatRoutes.get("/sessions", async (c) => {
|
|
62
75
|
try {
|
|
@@ -146,9 +159,7 @@ chatRoutes.post("/sessions/:id/fork", async (c) => {
|
|
|
146
159
|
});
|
|
147
160
|
// Store fork source so WS handler knows to use forkSession on first message
|
|
148
161
|
const provider = providerRegistry.get(providerId);
|
|
149
|
-
|
|
150
|
-
(provider as any).setForkSource(session.id, sourceId);
|
|
151
|
-
}
|
|
162
|
+
provider?.setForkSource?.(session.id, sourceId);
|
|
152
163
|
return c.json(ok({ ...session, forkedFrom: sourceId }), 201);
|
|
153
164
|
} catch (e) {
|
|
154
165
|
return c.json(err((e as Error).message), 500);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../../types/config.ts";
|
|
12
12
|
import { ok, err } from "../../types/api.ts";
|
|
13
13
|
import { proxyService } from "../../services/proxy.service.ts";
|
|
14
|
+
import { providerRegistry } from "../../providers/registry.ts";
|
|
14
15
|
|
|
15
16
|
export const settingsRoutes = new Hono();
|
|
16
17
|
|
|
@@ -155,6 +156,19 @@ settingsRoutes.put("/ai", async (c) => {
|
|
|
155
156
|
}
|
|
156
157
|
});
|
|
157
158
|
|
|
159
|
+
/** GET /settings/ai/providers/:id/models — list models for a provider (global, no project context needed) */
|
|
160
|
+
settingsRoutes.get("/ai/providers/:id/models", async (c) => {
|
|
161
|
+
try {
|
|
162
|
+
const id = c.req.param("id");
|
|
163
|
+
const provider = providerRegistry.get(id);
|
|
164
|
+
if (!provider) return c.json(err(`Provider "${id}" not found`), 404);
|
|
165
|
+
const models = await provider.listModels?.() ?? [];
|
|
166
|
+
return c.json(ok(models));
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return c.json(err((e as Error).message), 500);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
158
172
|
// ── Keybindings ──────────────────────────────────────────────────────
|
|
159
173
|
|
|
160
174
|
const KEYBINDINGS_KEY = "keybindings";
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -135,7 +135,8 @@ function startCleanupTimer(sessionId: string): void {
|
|
|
135
135
|
* Standalone streaming loop — decoupled from WS message handler.
|
|
136
136
|
* Runs independently so WS close does NOT kill the Claude query.
|
|
137
137
|
*/
|
|
138
|
-
async function runStreamLoop(
|
|
138
|
+
async function runStreamLoop(initialSessionId: string, providerId: string, content: string, permissionMode?: string): Promise<void> {
|
|
139
|
+
let sessionId = initialSessionId;
|
|
139
140
|
const entry = activeSessions.get(sessionId);
|
|
140
141
|
if (!entry) {
|
|
141
142
|
console.error(`[chat] session=${sessionId} runStreamLoop: no entry — aborting`);
|
|
@@ -198,6 +199,23 @@ async function runStreamLoop(sessionId: string, providerId: string, content: str
|
|
|
198
199
|
const ev = event as any;
|
|
199
200
|
const evType = ev.type ?? "unknown";
|
|
200
201
|
|
|
202
|
+
// Session ID migrated: CLI provider assigned a different ID than PPM generated.
|
|
203
|
+
// Migrate activeSessions key so all subsequent events use the real ID.
|
|
204
|
+
if (evType === "session_migrated") {
|
|
205
|
+
const { oldSessionId, newSessionId } = ev;
|
|
206
|
+
const migrated = activeSessions.get(oldSessionId);
|
|
207
|
+
if (migrated) {
|
|
208
|
+
activeSessions.delete(oldSessionId);
|
|
209
|
+
activeSessions.set(newSessionId, migrated);
|
|
210
|
+
sessionId = newSessionId; // update local ref for subsequent setPhase/broadcast calls
|
|
211
|
+
// Notify frontend to update its sessionId state
|
|
212
|
+
broadcast(newSessionId, { type: "session_migrated", oldSessionId, newSessionId });
|
|
213
|
+
console.log(`[chat] session migrated: ${oldSessionId} → ${newSessionId}`);
|
|
214
|
+
logSessionEvent(newSessionId, "INFO", `Session ID migrated from ${oldSessionId}`);
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
201
219
|
// System events (hook_started, init, etc.) → transition connecting → thinking
|
|
202
220
|
// These indicate SDK has connected and is processing, but no content yet.
|
|
203
221
|
if (evType === "system") {
|
|
@@ -479,17 +497,17 @@ export const chatWebSocket = {
|
|
|
479
497
|
|
|
480
498
|
// Resume session in provider (can be slow on first call — sdkListSessions)
|
|
481
499
|
const provider = providerRegistry.get(providerId);
|
|
482
|
-
if (provider
|
|
500
|
+
if (provider) {
|
|
483
501
|
const t0 = Date.now();
|
|
484
|
-
await
|
|
502
|
+
await provider.resumeSession(sessionId);
|
|
485
503
|
const elapsed = Date.now() - t0;
|
|
486
504
|
if (elapsed > 500) {
|
|
487
505
|
console.warn(`[chat] session=${sessionId} resumeSession took ${elapsed}ms`);
|
|
488
506
|
logSessionEvent(sessionId, "PERF", `resumeSession took ${elapsed}ms`);
|
|
489
507
|
}
|
|
490
508
|
}
|
|
491
|
-
if (entry.projectPath && provider
|
|
492
|
-
|
|
509
|
+
if (entry.projectPath && provider?.ensureProjectPath) {
|
|
510
|
+
provider.ensureProjectPath(sessionId, entry.projectPath);
|
|
493
511
|
}
|
|
494
512
|
|
|
495
513
|
// Abort-and-replace: if already streaming, abort current query and wait for cleanup
|
|
@@ -518,9 +536,7 @@ export const chatWebSocket = {
|
|
|
518
536
|
});
|
|
519
537
|
} else if (parsed.type === "cancel") {
|
|
520
538
|
const provider = providerRegistry.get(providerId);
|
|
521
|
-
|
|
522
|
-
(provider as any).abortQuery(sessionId);
|
|
523
|
-
}
|
|
539
|
+
provider?.abortQuery?.(sessionId);
|
|
524
540
|
} else if (parsed.type === "approval_response") {
|
|
525
541
|
const provider = providerRegistry.get(providerId);
|
|
526
542
|
if (provider && typeof provider.resolveApproval === "function") {
|