@hienlh/ppm 0.8.59 → 0.8.61
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 +12 -0
- 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-CsZFFI1C.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-WYQKXiDW.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-DgTfBijB.js → code-editor-C6umJOvn.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-TYwvlW4u.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-C5A-ZnrC.js → diff-viewer-DApETeeX.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-mtdNxBZs.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-CYhfwlmi.js +37 -0
- package/dist/web/assets/index-n0Ww6i6b.css +2 -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-DA8at4_B.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-DK-YZN0m.js → markdown-renderer-oHkpw_nC.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-XnXGFIcT.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-t--MmXOo.js +1 -0
- package/dist/web/assets/sqlite-viewer-Zm20Z3Ys.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-goUjvGec.js +1 -0
- package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DSz5PQI0.js} +1 -1
- package/dist/web/assets/{terminal-tab-lu-7WWOT.js → terminal-tab-CxJ3m9tD.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 +106 -7
- package/docs/project-changelog.md +80 -1
- package/docs/streaming-input-guide.md +267 -0
- package/docs/system-architecture.md +177 -12
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +16 -14
- package/src/providers/mock-provider.ts +6 -1
- package/src/server/index.ts +4 -0
- package/src/server/routes/browser-preview.ts +89 -0
- package/src/server/ws/chat.ts +194 -139
- package/src/types/api.ts +9 -1
- package/src/web/components/browser/browser-tab.tsx +269 -0
- package/src/web/components/chat/chat-tab.tsx +14 -5
- package/src/web/components/chat/message-input.tsx +39 -12
- package/src/web/components/chat/message-list.tsx +15 -12
- package/src/web/components/layout/command-palette.tsx +2 -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/hooks/use-chat.ts +196 -203
- 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-CM6zFolq.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-DSlQhR7c.js +0 -1
- package/dist/web/assets/git-graph-B5QR_Cf-.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
- package/dist/web/assets/index-WKLuYsBY.css +0 -2
- package/dist/web/assets/index-frRaTxEm.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-Bjy78BoD.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-CV0kVl2C.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
- package/dist/web/assets/settings-tab-DofusrxH.js +0 -1
- package/dist/web/assets/sqlite-viewer-D5L6DIMB.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
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Streaming Input Migration Quick Reference (v0.8.55+)
|
|
2
|
+
|
|
3
|
+
## What Changed?
|
|
4
|
+
|
|
5
|
+
**Before (v0.8.54):** Each message triggered a new SDK query
|
|
6
|
+
```
|
|
7
|
+
Message 1 → SDK subprocess spawn → generate response → close
|
|
8
|
+
Message 2 → SDK subprocess spawn → generate response → close
|
|
9
|
+
(Slow, context resets between messages)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**After (v0.8.55):** Single persistent streaming session
|
|
13
|
+
```
|
|
14
|
+
Session created → AsyncGenerator streaming input opened
|
|
15
|
+
Message 1 → Push into generator → process events
|
|
16
|
+
Message 2 → Push into same generator → continue streaming
|
|
17
|
+
(Fast, continuous context, no SDK restarts)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Key Concepts
|
|
21
|
+
|
|
22
|
+
### Session State (BE-Owned)
|
|
23
|
+
The backend maintains a `SessionEntry` per chat session:
|
|
24
|
+
- Tracks connected clients (can be zero if FE disconnected)
|
|
25
|
+
- Maintains streaming phase (idle, connecting, thinking, streaming)
|
|
26
|
+
- Buffers events for reconnection sync
|
|
27
|
+
- Auto-cleans after 5 minutes of FE inactivity
|
|
28
|
+
|
|
29
|
+
### Message Priority (v0.8.55+)
|
|
30
|
+
```typescript
|
|
31
|
+
// Send message with priority
|
|
32
|
+
ws.send({
|
|
33
|
+
type: "message",
|
|
34
|
+
content: "Debug this code",
|
|
35
|
+
priority: "now" // "now" | "next" | "later"
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
- **"now"** — Abort current query, restart with this message
|
|
39
|
+
- **"next"** — Queue after current, run next
|
|
40
|
+
- **"later"** — Append to queue, run last
|
|
41
|
+
|
|
42
|
+
### Event Buffering on Reconnect
|
|
43
|
+
When FE WS reconnects after disconnect:
|
|
44
|
+
1. BE sends `session_state` with current phase + pending approval
|
|
45
|
+
2. BE sends `turn_events` with all buffered events since last connection
|
|
46
|
+
3. FE rebuilds chat UI state from buffered events
|
|
47
|
+
4. No message loss (unless session cleaned up after 5min)
|
|
48
|
+
|
|
49
|
+
## Common Patterns
|
|
50
|
+
|
|
51
|
+
### Frontend: Send Message
|
|
52
|
+
```typescript
|
|
53
|
+
// In useChat hook or message input handler
|
|
54
|
+
ws.send(JSON.stringify({
|
|
55
|
+
type: "message",
|
|
56
|
+
content: userInput,
|
|
57
|
+
priority: "now", // Optional
|
|
58
|
+
images: [{ id: "img1", data: "base64..." }] // Optional
|
|
59
|
+
}));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Frontend: Handle Reconnection
|
|
63
|
+
```typescript
|
|
64
|
+
function handleReconnect() {
|
|
65
|
+
// 1. WS open fires
|
|
66
|
+
// 2. Server sends session_state
|
|
67
|
+
const sessionState = JSON.parse(msg);
|
|
68
|
+
// 3. Server sends turn_events
|
|
69
|
+
const turnEvents = JSON.parse(msg);
|
|
70
|
+
|
|
71
|
+
// 4. FE rebuilds state from buffered events
|
|
72
|
+
turnEvents.events.forEach(event => {
|
|
73
|
+
chatStore.addEvent(event);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 5. FE is now synced with BE
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Backend: Session Lifecycle
|
|
81
|
+
```typescript
|
|
82
|
+
// 1. FE connects
|
|
83
|
+
open(ws) {
|
|
84
|
+
const entry = activeSessions.get(sessionId);
|
|
85
|
+
if (!entry) {
|
|
86
|
+
// Create new session entry
|
|
87
|
+
activeSessions.set(sessionId, {
|
|
88
|
+
phase: "idle",
|
|
89
|
+
clients: new Set([ws]),
|
|
90
|
+
turnEvents: []
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
// Reconnect: clear cleanup timer, add client
|
|
94
|
+
entry.clients.add(ws);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. FE sends message
|
|
99
|
+
message(ws, data) {
|
|
100
|
+
const parsed = JSON.parse(data);
|
|
101
|
+
if (parsed.type === "message") {
|
|
102
|
+
// Abort current if streaming, wait for cleanup
|
|
103
|
+
if (entry.phase !== "idle") {
|
|
104
|
+
entry.abort.abort();
|
|
105
|
+
await entry.streamPromise;
|
|
106
|
+
}
|
|
107
|
+
// Start new streaming loop (detached)
|
|
108
|
+
entry.streamPromise = runStreamLoop(...);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 3. Streaming loop runs independently
|
|
113
|
+
async function runStreamLoop() {
|
|
114
|
+
for await (const event of chatService.sendMessage(...)) {
|
|
115
|
+
bufferAndBroadcast(sessionId, event); // To all connected clients
|
|
116
|
+
}
|
|
117
|
+
setPhase(sessionId, "idle"); // Back to idle when done
|
|
118
|
+
if (entry.clients.size === 0) {
|
|
119
|
+
startCleanupTimer(sessionId); // 5-min cleanup
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 4. FE disconnects
|
|
124
|
+
close(ws) {
|
|
125
|
+
entry.clients.delete(ws);
|
|
126
|
+
// Stream continues! (BE owns the connection)
|
|
127
|
+
// Timer started if no more clients
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Phase State Machine
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
┌─ initializing (setup, session resume)
|
|
135
|
+
↓
|
|
136
|
+
idle ←→ connecting (waiting for first SDK event, heartbeat)
|
|
137
|
+
↑ ↓
|
|
138
|
+
│ ┌──→ thinking (extended thinking)
|
|
139
|
+
│ ↓ ↓
|
|
140
|
+
└─── streaming (text/tool_use content)
|
|
141
|
+
↑ ↓
|
|
142
|
+
└─────┘ (dynamic switch)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Transitions:**
|
|
146
|
+
- Heartbeat: `connecting` → (5s elapsed updates) → `thinking` (when content arrives)
|
|
147
|
+
- Content: `thinking` → `streaming` (first text event)
|
|
148
|
+
- Dynamic: `streaming` ↔ `thinking` (based on event types)
|
|
149
|
+
- Done: Any → `idle` (stream complete, ready for next message)
|
|
150
|
+
|
|
151
|
+
## WebSocket Messages (v0.8.55+)
|
|
152
|
+
|
|
153
|
+
### Client → Server
|
|
154
|
+
```typescript
|
|
155
|
+
// Send message
|
|
156
|
+
{ type: "message"; content: string; priority?: string; images?: {...}[] }
|
|
157
|
+
|
|
158
|
+
// Approve tool
|
|
159
|
+
{ type: "approval_response"; requestId: string; approved: boolean }
|
|
160
|
+
|
|
161
|
+
// Cancel current
|
|
162
|
+
{ type: "cancel" }
|
|
163
|
+
|
|
164
|
+
// Handshake after open
|
|
165
|
+
{ type: "ready" }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Server → Client
|
|
169
|
+
```typescript
|
|
170
|
+
// Content
|
|
171
|
+
{ type: "text"; content: string }
|
|
172
|
+
{ type: "thinking"; content: string }
|
|
173
|
+
|
|
174
|
+
// Tool execution
|
|
175
|
+
{ type: "tool_use"; tool: string; input: unknown }
|
|
176
|
+
{ type: "tool_result"; output: string; isError?: boolean }
|
|
177
|
+
|
|
178
|
+
// User approval request
|
|
179
|
+
{ type: "approval_request"; requestId: string; tool: string; input: unknown }
|
|
180
|
+
|
|
181
|
+
// Session state (sent on open/ready)
|
|
182
|
+
{ type: "session_state"; sessionId: string; phase: SessionPhase; pendingApproval: {...} | null }
|
|
183
|
+
|
|
184
|
+
// Buffered events (on reconnect)
|
|
185
|
+
{ type: "turn_events"; events: unknown[] }
|
|
186
|
+
|
|
187
|
+
// Metadata
|
|
188
|
+
{ type: "account_info"; accountId: string; accountLabel: string }
|
|
189
|
+
{ type: "phase_changed"; phase: SessionPhase; elapsed?: number }
|
|
190
|
+
{ type: "title_updated"; title: string }
|
|
191
|
+
|
|
192
|
+
// Completion
|
|
193
|
+
{ type: "done"; sessionId: string; contextWindowPct?: number }
|
|
194
|
+
|
|
195
|
+
// Error
|
|
196
|
+
{ type: "error"; message: string }
|
|
197
|
+
|
|
198
|
+
// Keepalive
|
|
199
|
+
{ type: "ping" }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Benefits
|
|
203
|
+
|
|
204
|
+
| Aspect | Before (v0.8.54) | After (v0.8.55) |
|
|
205
|
+
|--------|------------------|-----------------|
|
|
206
|
+
| **SDK Restarts** | Per message | Once per session |
|
|
207
|
+
| **Context** | Resets between messages | Persistent |
|
|
208
|
+
| **Startup Time** | 2-5s per message | Instant follow-ups |
|
|
209
|
+
| **Reconnection** | Message loss | Event buffering ensures sync |
|
|
210
|
+
| **Concurrency** | N/A | Multiple clients per session |
|
|
211
|
+
| **Tool Approvals** | Restarts query | Integrated in stream |
|
|
212
|
+
|
|
213
|
+
## Troubleshooting
|
|
214
|
+
|
|
215
|
+
### Session Cleaned Up (No Longer Exists)
|
|
216
|
+
**Cause:** FE disconnected for >5 minutes
|
|
217
|
+
**Solution:** Create new session, FE reconnects with new sessionId
|
|
218
|
+
|
|
219
|
+
### Events Missing After Reconnect
|
|
220
|
+
**Cause:** Server-side event buffer (10k event limit) overflowed
|
|
221
|
+
**Solution:** Flush buffer periodically or increase limit if needed
|
|
222
|
+
|
|
223
|
+
### Phase Stuck in "Connecting"
|
|
224
|
+
**Cause:** SDK subprocess not responding (120s timeout)
|
|
225
|
+
**Solution:** Check environment (ANTHROPIC_API_KEY, network), see error message for hints
|
|
226
|
+
|
|
227
|
+
### Multiple Clients Out of Sync
|
|
228
|
+
**Cause:** Broadcast failed for one client, others ahead
|
|
229
|
+
**Solution:** Evicted client will reconnect and re-sync from buffered events
|
|
230
|
+
|
|
231
|
+
## Debugging
|
|
232
|
+
|
|
233
|
+
### Enable Logging
|
|
234
|
+
```bash
|
|
235
|
+
# Check server logs for session lifecycle
|
|
236
|
+
[chat] session=abc123 phase → connecting
|
|
237
|
+
[chat] session=abc123 first SDK event after 1250ms: type=text
|
|
238
|
+
[chat] session=abc123 stream completed (45 events)
|
|
239
|
+
[chat] session=abc123 phase → idle
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Check Session State
|
|
243
|
+
```typescript
|
|
244
|
+
// On WS message handler
|
|
245
|
+
console.log(`Session entry:`, activeSessions.get(sessionId));
|
|
246
|
+
// Outputs: { phase, clients.size, pendingApprovalEvent, turnEvents.length }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Monitor Reconnections
|
|
250
|
+
```typescript
|
|
251
|
+
// In WS open handler
|
|
252
|
+
console.log(`FE reconnected (phase=${existing.phase}, clients=${existing.clients.size})`);
|
|
253
|
+
// Tells you: active streaming, how many clients connected
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Performance Notes
|
|
257
|
+
|
|
258
|
+
- **No SDK overhead:** Persistent streaming eliminates subprocess spawn overhead
|
|
259
|
+
- **Event buffering:** Clients see all events after reconnect (max 10k events per turn)
|
|
260
|
+
- **Memory:** Session entries cleaned after 5min (bounded memory usage)
|
|
261
|
+
- **Latency:** Follow-up messages start immediately (no SDK init)
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
**For detailed architecture:** See `docs/system-architecture.md` → "Chat Streaming Flow" section
|
|
266
|
+
**For API types:** See `src/types/api.ts` and `src/types/chat.ts`
|
|
267
|
+
**For implementation:** See `src/server/ws/chat.ts` and `src/providers/claude-agent-sdk.ts`
|
|
@@ -205,7 +205,47 @@ interface AIProvider {
|
|
|
205
205
|
**Implementations:**
|
|
206
206
|
- **claude-agent-sdk** (Primary) — @anthropic-ai/claude-agent-sdk, streaming, tool use. Reads model/effort/maxTurns/budget/thinking from config. Settings refreshed per query. Windows CLI fallback for Bun subprocess pipe issues. .env poisoning mitigation. **Multi-account support:** Injects account API token from AccountService instead of relying on ANTHROPIC_API_KEY env var when accounts configured.
|
|
207
207
|
- **mock-provider** (Testing) — Returns canned responses
|
|
208
|
-
- **
|
|
208
|
+
- **cursor-cli** (CLI-based) — Spawns `cursor-agent` CLI binary with NDJSON streaming. Extends `CliProvider` base class.
|
|
209
|
+
- **codex/gemini** (Planned) — Pluggable via `CliProvider` extension (~100-150 lines each)
|
|
210
|
+
|
|
211
|
+
#### Multi-Provider Architecture (v0.8.61+)
|
|
212
|
+
|
|
213
|
+
PPM supports multiple AI providers through a generic `AIProvider` interface and extensible base classes:
|
|
214
|
+
|
|
215
|
+
**Provider Types:**
|
|
216
|
+
1. **SDK-based** (claude-agent-sdk) — Uses Anthropic SDK for rich features (approvals, thinking blocks)
|
|
217
|
+
2. **CLI-based** (cursor-cli, codex, gemini) — Spawns external binary with NDJSON streaming
|
|
218
|
+
|
|
219
|
+
**Base Classes:**
|
|
220
|
+
- `AIProvider` interface — Defines required methods (createSession, sendMessage) + optional capabilities (abortQuery, getMessages, listSessionsByDir, ensureProjectPath)
|
|
221
|
+
- `CliProvider` abstract class — Shared spawn/parse/abort logic for all CLI-spawning providers
|
|
222
|
+
- Provider-specific subclasses implement: `buildArgs()`, `mapEvent()`, `extractSessionId()`, `isAvailable()`
|
|
223
|
+
|
|
224
|
+
**Streaming Infrastructure:**
|
|
225
|
+
- `parseNdjsonLines()` utility — Async generator that buffers partial TCP packets, yields complete JSON lines
|
|
226
|
+
- `ChatEvent` union type — Normalized event format across all providers (text, tool_use, thinking, approval_request, system, done, error)
|
|
227
|
+
- Event mappers translate provider-specific JSON → ChatEvent (e.g., Cursor's `reasoning` type → `thinking` event)
|
|
228
|
+
|
|
229
|
+
**Provider Registration & Bootstrap:**
|
|
230
|
+
- `ProviderRegistry` maintains active provider instances
|
|
231
|
+
- `bootstrapProviders()` async function checks `isAvailable()` on CLI providers before registering
|
|
232
|
+
- Graceful fallback: if Cursor binary not found, provider skips registration (no crash, logged as info)
|
|
233
|
+
- Config type `AIProviderConfig.type` union: `"agent-sdk" | "cli" | "mock"`
|
|
234
|
+
|
|
235
|
+
**CLI-Provider Features:**
|
|
236
|
+
- **Session capture** — Extract session ID from provider's init event, re-key process tracking
|
|
237
|
+
- **Workspace trust auto-retry** — Detect trust prompts in stderr, retry once with `--trust` flag
|
|
238
|
+
- **Process lifecycle** — Track active processes per session, escalate SIGTERM → SIGKILL on abort
|
|
239
|
+
- **History loading** — Override `listSessions()` to read native provider history (e.g., Cursor SQLite DAG)
|
|
240
|
+
- **Graceful degradation** — Missing binary → provider skipped, not fatal
|
|
241
|
+
|
|
242
|
+
**New Files (v0.8.61):**
|
|
243
|
+
- `src/utils/ndjson-line-parser.ts` — NDJSON streaming parser
|
|
244
|
+
- `src/providers/cli-provider-base.ts` — Abstract base class for CLI providers
|
|
245
|
+
- `src/providers/cursor-cli/cursor-provider.ts` — CursorCliProvider implementation
|
|
246
|
+
- `src/providers/cursor-cli/cursor-event-mapper.ts` — NDJSON → ChatEvent mapping
|
|
247
|
+
- `src/providers/cursor-cli/cursor-history.ts` — SQLite DAG reader for Cursor history
|
|
248
|
+
- `src/web/components/chat/provider-selector.tsx` — UI component for provider selection
|
|
209
249
|
|
|
210
250
|
---
|
|
211
251
|
|
|
@@ -453,7 +493,27 @@ Returns full updated config. Validates ranges/enums before writing.
|
|
|
453
493
|
|
|
454
494
|
---
|
|
455
495
|
|
|
456
|
-
## Chat Streaming Flow
|
|
496
|
+
## Chat Streaming Flow (Persistent AsyncGenerator Sessions)
|
|
497
|
+
|
|
498
|
+
### Architecture Overview (v0.8.55+)
|
|
499
|
+
|
|
500
|
+
PPM uses a **persistent streaming session** model instead of per-message query execution:
|
|
501
|
+
|
|
502
|
+
**Key Changes:**
|
|
503
|
+
- Provider maintains **long-lived AsyncGenerator streaming input** per chat session (not per message)
|
|
504
|
+
- Follow-up messages **push into the existing generator** instead of abort-and-replace
|
|
505
|
+
- **Single streaming loop** per session decoupled from WebSocket message handler
|
|
506
|
+
- Message priority support: `now` (interrupt current), `next` (queue first), `later` (queue at end)
|
|
507
|
+
- Supports image attachments in messages
|
|
508
|
+
|
|
509
|
+
**Design Benefits:**
|
|
510
|
+
- Continuous context preservation — multi-turn conversations flow naturally
|
|
511
|
+
- No SDK subprocess restarts between messages (faster)
|
|
512
|
+
- Clean separation: BE owns Claude connection, FE disconnect doesn't abort
|
|
513
|
+
- Message buffering on reconnect — clients that lose WS connection sync turn events
|
|
514
|
+
- Tool approvals don't restart the query — integrated into streaming loop
|
|
515
|
+
|
|
516
|
+
### Message Flow
|
|
457
517
|
|
|
458
518
|
```
|
|
459
519
|
User types: "Debug this function"
|
|
@@ -462,18 +522,26 @@ MessageInput.tsx calls useChat.sendMessage()
|
|
|
462
522
|
↓
|
|
463
523
|
useChat opens WebSocket: WS /ws/project/:name/chat/:sessionId
|
|
464
524
|
↓
|
|
465
|
-
Sends: { type: "message", content: "Debug..." }
|
|
525
|
+
Sends: { type: "message", content: "Debug...", priority?: "now"|"next"|"later" }
|
|
526
|
+
↓
|
|
527
|
+
WS handler in chat.ts receives message
|
|
466
528
|
↓
|
|
467
|
-
|
|
529
|
+
If already streaming with different content → abort previous + wait cleanup
|
|
530
|
+
If streaming, new message priority determines queue behavior:
|
|
531
|
+
• priority: "now" → abort current, restart with new content
|
|
532
|
+
• priority: "next" → push into pending queue (higher priority)
|
|
533
|
+
• priority: "later" → push to end of queue (FIFO)
|
|
534
|
+
↓
|
|
535
|
+
runStreamLoop() executes in detached async context
|
|
468
536
|
↓
|
|
469
537
|
ChatService calls provider.sendMessage() (async generator)
|
|
470
538
|
↓
|
|
471
|
-
Provider (Claude SDK)
|
|
472
|
-
1.
|
|
473
|
-
2.
|
|
474
|
-
3.
|
|
539
|
+
Provider (Claude SDK) yields events:
|
|
540
|
+
1. { type: "text", content: "Here's what..." }
|
|
541
|
+
2. { type: "text", content: " happens..." }
|
|
542
|
+
3. { type: "tool_use", tool: "read_file", input: {...} }
|
|
475
543
|
↓
|
|
476
|
-
|
|
544
|
+
Stream loop buffers + broadcasts to all connected clients:
|
|
477
545
|
{ type: "text", content: "Here's what..." }
|
|
478
546
|
{ type: "text", content: " happens..." }
|
|
479
547
|
{ type: "tool_use", tool: "read_file", input: {...} }
|
|
@@ -485,15 +553,112 @@ User sees tool approval prompt, clicks "Approve"
|
|
|
485
553
|
↓
|
|
486
554
|
Client sends: { type: "approval_response", requestId, approved: true }
|
|
487
555
|
↓
|
|
488
|
-
|
|
556
|
+
Provider continues streaming with tool result (no restart)
|
|
489
557
|
↓
|
|
490
|
-
|
|
558
|
+
If multiple messages queued, next message processes after done event
|
|
491
559
|
↓
|
|
492
560
|
Final response streamed, then: { type: "done", sessionId }
|
|
493
561
|
↓
|
|
494
|
-
|
|
562
|
+
Phase transitions to idle, clients can send new message
|
|
563
|
+
↓
|
|
564
|
+
useChat saves message to store, displays in chat history
|
|
495
565
|
```
|
|
496
566
|
|
|
567
|
+
### Session State Management
|
|
568
|
+
|
|
569
|
+
**Session Entry** (BE-owned, persists across FE disconnections):
|
|
570
|
+
```typescript
|
|
571
|
+
interface SessionEntry {
|
|
572
|
+
providerId: string; // Which AI provider (e.g., "claude")
|
|
573
|
+
clients: Set<ChatWsSocket>; // Connected FE clients (may be empty)
|
|
574
|
+
abort?: AbortController; // Current stream abort handle
|
|
575
|
+
projectPath?: string; // Project context
|
|
576
|
+
projectName?: string;
|
|
577
|
+
pingIntervals: Map<...>; // Per-client keepalive
|
|
578
|
+
phase: SessionPhase; // "initializing" | "connecting" | "thinking" | "streaming" | "idle"
|
|
579
|
+
cleanupTimer?: ReturnType<...>; // Auto-cleanup if no FE reconnects (5min)
|
|
580
|
+
pendingApprovalEvent?: {...}; // Current tool approval waiting
|
|
581
|
+
turnEvents: unknown[]; // Buffered events (for reconnect sync)
|
|
582
|
+
streamPromise?: Promise<void>; // Track ongoing runStreamLoop
|
|
583
|
+
permissionMode?: string; // Sticky permission mode for session
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**Client Connection States:**
|
|
588
|
+
- **Active streaming + FE connected** → Events broadcast to all clients in real-time
|
|
589
|
+
- **Active streaming + FE disconnected** → Events buffered in turnEvents array, BE stream continues
|
|
590
|
+
- **FE reconnects** → Receive session_state + buffered turnEvents, resync with stream
|
|
591
|
+
- **Idle (no query running)** → Phase is "idle", ready for next message
|
|
592
|
+
- **Idle + no FE for 5min** → Cleanup timer removes session from memory
|
|
593
|
+
|
|
594
|
+
### Follow-up Messages
|
|
595
|
+
|
|
596
|
+
**Abort-and-Replace Pattern:**
|
|
597
|
+
```typescript
|
|
598
|
+
if (entry.phase !== "idle" && entry.abort) {
|
|
599
|
+
console.log(`[chat] aborting current query for new message`);
|
|
600
|
+
entry.abort.abort();
|
|
601
|
+
await entry.streamPromise; // Wait for cleanup
|
|
602
|
+
// Re-fetch entry — may have been mutated during cleanup
|
|
603
|
+
entry = activeSessions.get(sessionId)!;
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Multiple Message Queueing:**
|
|
608
|
+
- First message: immediately starts runStreamLoop
|
|
609
|
+
- Second message (while streaming): abort current, wait, start new runStreamLoop
|
|
610
|
+
- Priority modes (future): could queue messages for intelligent interleaving
|
|
611
|
+
|
|
612
|
+
### WebSocket Reconnection Sync
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
FE WebSocket closes (network issue, tab closes)
|
|
616
|
+
↓
|
|
617
|
+
BE keeps session alive, streaming continues
|
|
618
|
+
↓
|
|
619
|
+
FE reconnects: WS /ws/project/:name/chat/:sessionId
|
|
620
|
+
↓
|
|
621
|
+
open() handler checks activeSessions.get(sessionId)
|
|
622
|
+
↓
|
|
623
|
+
If exists (entry found):
|
|
624
|
+
1. Clear cleanup timer (FE is back)
|
|
625
|
+
2. Send session_state with current phase + pendingApproval
|
|
626
|
+
3. If phase !== "idle", send buffered turnEvents
|
|
627
|
+
4. Add WS to clients Set
|
|
628
|
+
↓
|
|
629
|
+
FE processes session_state, renders current phase
|
|
630
|
+
↓
|
|
631
|
+
FE applies buffered events to rebuild turn state
|
|
632
|
+
↓
|
|
633
|
+
FE displays: "reconnected, current phase: streaming" etc.
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Phase Transitions
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
idle → initializing → connecting → thinking/streaming ↔ thinking/streaming → idle
|
|
640
|
+
^ ↑ ↓
|
|
641
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**Phase Descriptions:**
|
|
645
|
+
- **idle** — No query running, ready to accept new message
|
|
646
|
+
- **initializing** — Preparing (permission checks, session resume)
|
|
647
|
+
- **connecting** — Waiting for first SDK event (heartbeat: "connecting" with elapsed time every 5s)
|
|
648
|
+
- **thinking** — Receiving thinking content (extended thinking)
|
|
649
|
+
- **streaming** — Receiving text/tool_use content (dynamic switch between thinking/streaming)
|
|
650
|
+
|
|
651
|
+
### Image Attachment Support
|
|
652
|
+
|
|
653
|
+
Messages can now include images:
|
|
654
|
+
```typescript
|
|
655
|
+
type ChatWsClientMessage =
|
|
656
|
+
| { type: "message"; content: string; images?: { id: string; data: string }[]; priority?: string }
|
|
657
|
+
| ...
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Images are passed to provider's message context and included in tool input/output.
|
|
661
|
+
|
|
497
662
|
---
|
|
498
663
|
|
|
499
664
|
## Terminal Flow
|
package/package.json
CHANGED
|
@@ -502,22 +502,24 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
502
502
|
|
|
503
503
|
// Log all system events for debugging SDK lifecycle
|
|
504
504
|
if (msg.type === "system") {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (oldMeta) {
|
|
518
|
-
this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
|
|
505
|
+
const subtype = (msg as any).subtype ?? "none";
|
|
506
|
+
console.log(`[sdk] session=${sessionId} system: subtype=${subtype} ${JSON.stringify(msg).slice(0, 500)}`);
|
|
507
|
+
|
|
508
|
+
// Capture SDK session metadata from init message
|
|
509
|
+
if (subtype === "init") {
|
|
510
|
+
const initMsg = msg as any;
|
|
511
|
+
if (initMsg.session_id && initMsg.session_id !== sessionId) {
|
|
512
|
+
setSessionMapping(sessionId, initMsg.session_id);
|
|
513
|
+
const oldMeta = this.activeSessions.get(sessionId);
|
|
514
|
+
if (oldMeta) {
|
|
515
|
+
this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
|
|
516
|
+
}
|
|
519
517
|
}
|
|
520
518
|
}
|
|
519
|
+
|
|
520
|
+
// Yield system events so streaming loop can transition phases
|
|
521
|
+
// (e.g. connecting → thinking when hooks/init arrive)
|
|
522
|
+
yield { type: "system" as any, subtype } as any;
|
|
521
523
|
continue;
|
|
522
524
|
}
|
|
523
525
|
|
|
@@ -92,8 +92,13 @@ export class MockProvider implements AIProvider {
|
|
|
92
92
|
const abortController = new AbortController();
|
|
93
93
|
this.activeAborts.set(sessionId, abortController);
|
|
94
94
|
|
|
95
|
+
// Simulate SDK system events (hooks, init) — real SDK emits these before content
|
|
96
|
+
yield { type: "system" as any, subtype: "hook_started" } as any;
|
|
97
|
+
await sleep(50);
|
|
98
|
+
yield { type: "system" as any, subtype: "init" } as any;
|
|
99
|
+
|
|
95
100
|
// Simulate thinking delay
|
|
96
|
-
await sleep(
|
|
101
|
+
await sleep(250);
|
|
97
102
|
|
|
98
103
|
// Pick a response
|
|
99
104
|
const responseText =
|
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
|
|
|
@@ -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
|
+
});
|