@hienlh/ppm 0.9.0-beta.4 → 0.9.0-beta.6
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 +27 -8
- package/dist/web/assets/{_basePickBy-COwDPZl_.js → _basePickBy-CZovQgWd.js} +1 -1
- package/dist/web/assets/{_baseUniq-DCb0mkTp.js → _baseUniq-ClnvscgW.js} +1 -1
- package/dist/web/assets/{api-settings-CuUkz5gb.js → api-settings--eVrUeZM.js} +1 -1
- package/dist/web/assets/{arc-D0bJaFyD.js → arc-C2Qaz-ch.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-BVEUkQYB.js → architectureDiagram-2XIMDMQ5-Jq91S_rs.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-CU2t4NHJ.js → blockDiagram-WCTKOSBZ-CKGufRTy.js} +1 -1
- package/dist/web/assets/browser-tab-DAvH4mv0.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-DzjR91sM.js → c4Diagram-IC4MRINW-BNP2L9r_.js} +1 -1
- package/dist/web/assets/channel-w7yboq56.js +1 -0
- package/dist/web/assets/chat-tab-WEBXxGgN.js +7 -0
- package/dist/web/assets/{chunk-4BX2VUAB-0YMkpW2S.js → chunk-4BX2VUAB-BptTlTyl.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-Dp0pTM5r.js → chunk-55IACEB6-C4mUdyio.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CuYKSUgJ.js → chunk-7E7YKBS2-6xAQfBwa.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DvbvLUIN.js → chunk-7R4GIKGN-DXaGAn_K.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-CcEW1AMZ.js → chunk-C72U2L5F-DOtEiN5f.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Cgt-qg75.js → chunk-EGIJ26TM-D0KJTa_T.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-JCLgVcaC.js → chunk-FMBD7UC4-C_1aG0eb.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-B82RP9ow.js → chunk-GEFDOKGD-DwVPiYfW.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-Pb-JMOgO.js → chunk-JSJVCQXG-BSrqCL_3.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-BRj-ZEvL.js → chunk-KX2RTZJC-BCxGmbzy.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-CBRPKraG.js → chunk-KYZI473N-BKO5gMeU.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-DNFj84V6.js → chunk-L3YUKLVL-3wBgkSvL.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BnPzQK-O.js → chunk-MX3YWQON-BgjSEzus.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-BRj25yO7.js → chunk-NQ4KR5QH-DLrZwBEm.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BdXwVXjJ.js → chunk-O4XLMI2P-BurQy8tt.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-LfXT4p8B.js → chunk-OZEHJAEY-YTn24bGg.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-EdgQyTqa.js → chunk-PQ6SQG4A-BxtUGYhW.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-D3thuSok.js → chunk-PU5JKC2W-B66ELkQm.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-LdG7RqsM.js → chunk-R5LLSJPH-euR2RxLN.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-BHFnnXOt.js → chunk-WL4C6EOR-_2CBOJdI.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-DUmQrLsF.js → chunk-XIRO2GV7-kqQ0g6wW.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-CsGTseUr.js → chunk-XPW4576I-CtcaMb09.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-5W2emiq4.js → chunk-XZSTWKYB-BYxFzZwS.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-COdZIaX4.js → chunk-YBOYWFTD-Dx_fX35n.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +1 -0
- package/dist/web/assets/clone-BSi6cgDh.js +1 -0
- package/dist/web/assets/{code-editor-BRMOypkX.js → code-editor-B5sg_uJQ.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-C1QJ6GPW.js → cose-bilkent-S5V4N54A-CHHjH2dV.js} +1 -1
- package/dist/web/assets/{dagre-CWo8w9wK.js → dagre-CNtSxiE_.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-Br4t5TRV.js → dagre-KLK3FWXG-ChenfPp1.js} +1 -1
- package/dist/web/assets/database-viewer-CwtyWCkE.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-CkDC2uAj.js → diagram-E7M64L7V-CzKYZM0Y.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-NvhckwcA.js → diagram-IFDJBPK2-ChB_paPo.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO--nUaNiyB.js → diagram-P4PSJMXO-D1eW1dkL.js} +1 -1
- package/dist/web/assets/{diff-viewer-jDU2bcGj.js → diff-viewer-CzE5M-Wd.js} +1 -1
- package/dist/web/assets/{erDiagram-INFDFZHY-DK4QEZYh.js → erDiagram-INFDFZHY-mCvUFSn6.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-B9h_Ba-v.js → flowDiagram-PKNHOUZH-14ohZ1M1.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-BVlftqyZ.js → ganttDiagram-A5KZAMGK-DIX0pLbk.js} +1 -1
- package/dist/web/assets/git-graph-6yxCeeN9.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js → gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js} +1 -1
- package/dist/web/assets/{graphlib-BbbiUImY.js → graphlib-DhOZxqsh.js} +1 -1
- package/dist/web/assets/index-DE8b9u8F.css +2 -0
- package/dist/web/assets/index-wuWZBO9y.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +2 -0
- package/dist/web/assets/input-Brjz2Vv-.js +41 -0
- package/dist/web/assets/{isEmpty-DXomfd7J.js → isEmpty-C0YYdhYj.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-cW7SMLa_.js → ishikawaDiagram-PHBUUO56-olazD6dZ.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-DFQXUZsc.js → journeyDiagram-4ABVD52K-CttDH9bb.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-BMUhjxqj.js → kanban-definition-K7BYSVSG-BBXbI37U.js} +1 -1
- package/dist/web/assets/keybindings-store-mkBHnWN1.js +1 -0
- package/dist/web/assets/{line--xyfYP3x.js → line-DBLLF7lH.js} +1 -1
- package/dist/web/assets/{linear-BdqW7iQu.js → linear-BLFWatDe.js} +1 -1
- package/dist/web/assets/{markdown-renderer-BCjJbGP8.js → markdown-renderer-CxWxvrzT.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-BY8JfkE_.js → mermaid-parser.core-BKiGOTjR.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-DIv-LMXG.js → mindmap-definition-YRQLILUH-DoT7m4Sz.js} +1 -1
- package/dist/web/assets/{ordinal-CIoJK3nc.js → ordinal-CCj7PWgZ.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-seSK40d1.js → pieDiagram-SKSYHLDU-Bkh2E4zE.js} +1 -1
- package/dist/web/assets/postgres-viewer-UP3yv9Yh.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-BaRFqlsA.js → quadrantDiagram-337W2JSQ-B7zgALOL.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-1WWjMQB_.js → requirementDiagram-Z7DCOOCP-D_5GXNRo.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DEGGYsk7.js → sankeyDiagram-WA2Y5GQK-BA9EFAAe.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-BtRvoUTC.js → sequenceDiagram-2WXFIKYE-fyWIrHiG.js} +1 -1
- package/dist/web/assets/{settings-store-D3dJqGhB.js → settings-store-Bbhg_ptG.js} +2 -2
- package/dist/web/assets/settings-tab-BoBXlVHe.js +1 -0
- package/dist/web/assets/sqlite-viewer-lzRVvM5j.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-C16aO8tn.js → stateDiagram-RAJIS63D-DfRBcaBu.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +1 -0
- package/dist/web/assets/{tab-store-DSz5PQI0.js → tab-store-DcIBZTD4.js} +1 -1
- package/dist/web/assets/{terminal-tab-MRg8y1xF.js → terminal-tab-CAZtLK6i.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-DrjxCpEM.js → timeline-definition-YZTLITO2-DYfwJ1jM.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +1 -0
- package/dist/web/assets/{use-monaco-theme-BQzvItNE.js → use-monaco-theme-vwto-Vlf.js} +1 -1
- package/dist/web/assets/{vennDiagram-LZ73GAT5-DfYFnniI.js → vennDiagram-LZ73GAT5-DqbKNRD9.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-BRvXOVlG.js → xychartDiagram-JWTSCODW-DhUL86qT.js} +1 -1
- package/dist/web/index.html +10 -12
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +212 -76
- package/src/server/index.ts +4 -1
- package/src/server/routes/browser-preview.ts +135 -65
- package/src/server/ws/chat.ts +103 -73
- package/src/types/api.ts +1 -1
- package/src/types/chat.ts +2 -0
- package/src/web/components/browser/browser-tab.tsx +105 -224
- package/src/web/components/chat/chat-tab.tsx +3 -3
- package/src/web/components/chat/message-input.tsx +42 -4
- package/src/web/hooks/use-chat.ts +21 -9
- package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +0 -1
- package/dist/web/assets/arrow-left-C_j9Ki73.js +0 -1
- package/dist/web/assets/browser-tab-BhTdeeZd.js +0 -1
- package/dist/web/assets/channel-CKNZAqoN.js +0 -1
- package/dist/web/assets/chat-tab-ZiiUVOxM.js +0 -7
- package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +0 -1
- package/dist/web/assets/clone-DNDy9Sms.js +0 -1
- package/dist/web/assets/database-viewer-CEoDpzPz.js +0 -1
- package/dist/web/assets/git-graph-DMQzw4Sp.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +0 -1
- package/dist/web/assets/index-B4Iz1Wbi.css +0 -2
- package/dist/web/assets/index-QiSWS6f-.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +0 -2
- package/dist/web/assets/input-DGlv6gt_.js +0 -41
- package/dist/web/assets/keybindings-store-BplH-yiN.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +0 -1
- package/dist/web/assets/postgres-viewer-s0snZ9CL.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +0 -1
- package/dist/web/assets/settings-tab-2YkgmrY0.js +0 -1
- package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +0 -1
- package/dist/web/assets/switch-mjGtIVDJ.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +0 -1
- /package/dist/web/assets/{api-client-icCZ-07C.js → api-client-DpGMOZNf.js} +0 -0
- /package/dist/web/assets/{array-CLwNaqU1.js → array-BGFCBI0e.js} +0 -0
- /package/dist/web/assets/{columns-2-Bcg3QJBg.js → columns-2-ChOTgl3e.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-B-QQuWwK.js → cytoscape.esm-Ccan6xou.js} +0 -0
- /package/dist/web/assets/{defaultLocale-D_VMtRaY.js → defaultLocale-CRZydyG6.js} +0 -0
- /package/dist/web/assets/{dist-Ckxnw5rl.js → dist-Cce3efmT.js} +0 -0
- /package/dist/web/assets/{dist-CMmNEgEP.js → dist-T0Vhi0Mh.js} +0 -0
- /package/dist/web/assets/{init-vVpfz1D6.js → init-B8gtcn7T.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-DvHDmeBe.js → isArrayLikeObject-B4pdpV8V.js} +0 -0
- /package/dist/web/assets/{katex-C3cZrCvP.js → katex-Bbu770d9.js} +0 -0
- /package/dist/web/assets/{math-a44lmFDa.js → math-DwgHI-Cu.js} +0 -0
- /package/dist/web/assets/{path-CuyvWNAH.js → path-DZF-JdEe.js} +0 -0
- /package/dist/web/assets/{preload-helper-CsoeaaUJ.js → preload-helper-qlgyTAkD.js} +0 -0
- /package/dist/web/assets/{react-BPIfZRKM.js → react-BGf7KNLk.js} +0 -0
- /package/dist/web/assets/{rough.esm-c4PR5shF.js → rough.esm-VLpapkIG.js} +0 -0
- /package/dist/web/assets/{src-CLWraeNW.js → src-BoSBNdA_.js} +0 -0
- /package/dist/web/assets/{table-C9jDaRl2.js → table-Yo02WRH-.js} +0 -0
- /package/dist/web/assets/{tag-CENGyt_L.js → tag-CaC1ng2E.js} +0 -0
- /package/dist/web/assets/{utils-Bslrbb-G.js → utils-btZ8C8-R.js} +0 -0
package/src/server/ws/chat.ts
CHANGED
|
@@ -22,7 +22,6 @@ type ChatWsSocket = {
|
|
|
22
22
|
interface SessionEntry {
|
|
23
23
|
providerId: string;
|
|
24
24
|
clients: Set<ChatWsSocket>;
|
|
25
|
-
abort?: AbortController;
|
|
26
25
|
projectPath?: string;
|
|
27
26
|
projectName?: string;
|
|
28
27
|
pingIntervals: Map<ChatWsSocket, ReturnType<typeof setInterval>>;
|
|
@@ -32,6 +31,8 @@ interface SessionEntry {
|
|
|
32
31
|
turnEvents: unknown[];
|
|
33
32
|
streamPromise?: Promise<void>;
|
|
34
33
|
permissionMode?: string;
|
|
34
|
+
/** Whether the persistent event consumer loop is running */
|
|
35
|
+
isStreamingActive: boolean;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/** Tracks active sessions — persists even when FE disconnects */
|
|
@@ -125,6 +126,11 @@ function startCleanupTimer(sessionId: string): void {
|
|
|
125
126
|
entry.cleanupTimer = setTimeout(() => {
|
|
126
127
|
console.log(`[chat] session=${sessionId} cleanup: no FE reconnected within timeout`);
|
|
127
128
|
logSessionEvent(sessionId, "INFO", "Session cleaned up (no FE reconnected)");
|
|
129
|
+
// Close streaming session in provider
|
|
130
|
+
const provider = providerRegistry.get(entry.providerId);
|
|
131
|
+
if (provider && "closeStreamingSession" in provider) {
|
|
132
|
+
(provider as any).closeStreamingSession(sessionId);
|
|
133
|
+
}
|
|
128
134
|
for (const interval of entry.pingIntervals.values()) clearInterval(interval);
|
|
129
135
|
entry.pingIntervals.clear();
|
|
130
136
|
activeSessions.delete(sessionId);
|
|
@@ -132,28 +138,25 @@ function startCleanupTimer(sessionId: string): void {
|
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
141
|
+
* Persistent event consumer — runs for the entire session lifetime.
|
|
142
|
+
* First message creates the query; follow-ups push into the provider's
|
|
143
|
+
* message channel. Events from ALL turns flow through this single loop.
|
|
137
144
|
*/
|
|
138
|
-
async function
|
|
139
|
-
let sessionId = initialSessionId;
|
|
145
|
+
async function startSessionConsumer(sessionId: string, providerId: string, content: string, permissionMode?: string, images?: Array<{ data: string; mediaType: string }>): Promise<void> {
|
|
140
146
|
const entry = activeSessions.get(sessionId);
|
|
141
147
|
if (!entry) {
|
|
142
|
-
console.error(`[chat] session=${sessionId}
|
|
148
|
+
console.error(`[chat] session=${sessionId} startSessionConsumer: no entry — aborting`);
|
|
143
149
|
return;
|
|
144
150
|
}
|
|
145
|
-
|
|
146
|
-
console.log(`[chat] session=${sessionId} runStreamLoop started (clients=${entry.clients.size})`);
|
|
151
|
+
console.log(`[chat] session=${sessionId} startSessionConsumer started (clients=${entry.clients.size})`);
|
|
147
152
|
|
|
148
|
-
|
|
149
|
-
entry.abort = abortController;
|
|
153
|
+
entry.isStreamingActive = true;
|
|
150
154
|
entry.pendingApprovalEvent = undefined;
|
|
151
155
|
entry.turnEvents = [];
|
|
152
156
|
setPhase(sessionId, "connecting");
|
|
153
157
|
|
|
154
158
|
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
155
159
|
let lastContextWindowPct: number | undefined;
|
|
156
|
-
let doneEmitted = false;
|
|
157
160
|
|
|
158
161
|
try {
|
|
159
162
|
const userPreview = content.slice(0, 200);
|
|
@@ -162,12 +165,12 @@ async function runStreamLoop(initialSessionId: string, providerId: string, conte
|
|
|
162
165
|
|
|
163
166
|
let eventCount = 0;
|
|
164
167
|
let firstEventReceived = false;
|
|
165
|
-
|
|
168
|
+
let startTime = Date.now();
|
|
166
169
|
|
|
167
170
|
// Heartbeat: while waiting for first response, send elapsed time every 5s
|
|
168
171
|
const CONNECTION_TIMEOUT_S = 120;
|
|
169
172
|
heartbeat = setInterval(() => {
|
|
170
|
-
if (firstEventReceived
|
|
173
|
+
if (firstEventReceived) {
|
|
171
174
|
clearInterval(heartbeat);
|
|
172
175
|
return;
|
|
173
176
|
}
|
|
@@ -186,15 +189,12 @@ async function runStreamLoop(initialSessionId: string, providerId: string, conte
|
|
|
186
189
|
type: "error",
|
|
187
190
|
message: `Claude SDK timed out after ${elapsed}s for project "${projectPath || "(no project)"}".${wslHint}\n\nDebug steps:\n1. Run: \`${debugCmd}\` — if it also hangs, the issue is your Claude CLI environment\n2. Check env vars: \`echo $ANTHROPIC_API_KEY $ANTHROPIC_BASE_URL\` — stale/invalid keys cause silent hang\n3. Try with env cleared: \`ANTHROPIC_API_KEY="" ANTHROPIC_BASE_URL="" ${debugCmd}\`\n4. Check hooks/MCP: \`cat ${projectPath}/.claude/settings.local.json\`\n5. Refresh auth: \`claude login\``,
|
|
188
191
|
});
|
|
189
|
-
abortController.abort();
|
|
190
192
|
return;
|
|
191
193
|
}
|
|
192
|
-
// Heartbeat uses broadcast() directly — NOT setPhase() (same-phase guard would skip elapsed updates)
|
|
193
194
|
broadcast(sessionId, { type: "phase_changed", phase: "connecting", elapsed });
|
|
194
195
|
}, 5_000);
|
|
195
196
|
|
|
196
|
-
for await (const event of chatService.sendMessage(providerId, sessionId, content, { permissionMode })) {
|
|
197
|
-
if (abortController.signal.aborted) break;
|
|
197
|
+
for await (const event of chatService.sendMessage(providerId, sessionId, content, { permissionMode, images })) {
|
|
198
198
|
eventCount++;
|
|
199
199
|
const ev = event as any;
|
|
200
200
|
const evType = ev.type ?? "unknown";
|
|
@@ -216,14 +216,13 @@ async function runStreamLoop(initialSessionId: string, providerId: string, conte
|
|
|
216
216
|
continue;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
// System events
|
|
220
|
-
// These indicate SDK has connected and is processing, but no content yet.
|
|
219
|
+
// System events → transition connecting → thinking
|
|
221
220
|
if (evType === "system") {
|
|
222
221
|
if (!firstEventReceived) {
|
|
223
222
|
if (heartbeat) clearInterval(heartbeat);
|
|
224
223
|
setPhase(sessionId, "thinking");
|
|
225
224
|
}
|
|
226
|
-
continue;
|
|
225
|
+
continue;
|
|
227
226
|
}
|
|
228
227
|
|
|
229
228
|
// First content event — stop heartbeat, transition phase
|
|
@@ -256,10 +255,11 @@ async function runStreamLoop(initialSessionId: string, providerId: string, conte
|
|
|
256
255
|
console.error(`[chat] session=${sessionId} error: ${errorDetail}`);
|
|
257
256
|
logSessionEvent(sessionId, "ERROR", errorDetail);
|
|
258
257
|
} else if (evType === "done") {
|
|
259
|
-
|
|
258
|
+
// Turn complete — transition to idle, clear buffer for next turn
|
|
260
259
|
logSessionEvent(sessionId, "DONE", `subtype=${ev.resultSubtype ?? "none"} turns=${ev.numTurns ?? "?"} ctx=${ev.contextWindowPct ?? "?"}%`);
|
|
261
260
|
if (ev.contextWindowPct != null) lastContextWindowPct = ev.contextWindowPct;
|
|
262
|
-
|
|
261
|
+
|
|
262
|
+
// Fire-and-forget: title + notification
|
|
263
263
|
sdkListSessions({ dir: entry.projectPath, limit: 50 }).then((sessions) => {
|
|
264
264
|
const found = sessions.find((s) => s.sessionId === sessionId || s.sessionId === ev.sessionId);
|
|
265
265
|
const title = found?.customTitle ?? found?.summary;
|
|
@@ -269,7 +269,6 @@ async function runStreamLoop(initialSessionId: string, providerId: string, conte
|
|
|
269
269
|
if (session) session.title = title;
|
|
270
270
|
}
|
|
271
271
|
}).catch(() => {});
|
|
272
|
-
// Fire-and-forget notification broadcast (push + telegram)
|
|
273
272
|
import("../../services/notification.service.ts").then(({ notificationService }) => {
|
|
274
273
|
const project = entry.projectName || "Project";
|
|
275
274
|
const session = chatService.getSession(sessionId);
|
|
@@ -284,7 +283,6 @@ async function runStreamLoop(initialSessionId: string, providerId: string, conte
|
|
|
284
283
|
}).catch(() => {});
|
|
285
284
|
} else if (evType === "approval_request") {
|
|
286
285
|
entry.pendingApprovalEvent = ev;
|
|
287
|
-
// Fire-and-forget notification for approval/question
|
|
288
286
|
import("../../services/notification.service.ts").then(({ notificationService }) => {
|
|
289
287
|
const project = entry.projectName || "Project";
|
|
290
288
|
const session = chatService.getSession(sessionId);
|
|
@@ -303,32 +301,40 @@ async function runStreamLoop(initialSessionId: string, providerId: string, conte
|
|
|
303
301
|
|
|
304
302
|
// Buffer + broadcast content events
|
|
305
303
|
bufferAndBroadcast(sessionId, event);
|
|
304
|
+
|
|
305
|
+
// After "done", transition to idle + clear turn buffer for next turn
|
|
306
|
+
// Consumer loop continues — query waits for next message in generator
|
|
307
|
+
if (evType === "done") {
|
|
308
|
+
entry.turnEvents = [];
|
|
309
|
+
entry.pendingApprovalEvent = undefined;
|
|
310
|
+
setPhase(sessionId, "idle");
|
|
311
|
+
// Reset heartbeat tracking for next turn
|
|
312
|
+
firstEventReceived = false;
|
|
313
|
+
startTime = Date.now();
|
|
314
|
+
}
|
|
306
315
|
}
|
|
307
316
|
|
|
308
|
-
logSessionEvent(sessionId, "INFO", `
|
|
309
|
-
console.log(`[chat] session=${sessionId}
|
|
317
|
+
logSessionEvent(sessionId, "INFO", `Session consumer completed (${eventCount} events total)`);
|
|
318
|
+
console.log(`[chat] session=${sessionId} session consumer completed (${eventCount} events)`);
|
|
310
319
|
} catch (e) {
|
|
311
320
|
const errMsg = (e as Error).message;
|
|
312
321
|
logSessionEvent(sessionId, "ERROR", `Exception: ${errMsg}`);
|
|
313
|
-
|
|
314
|
-
bufferAndBroadcast(sessionId, { type: "error", message: errMsg });
|
|
315
|
-
}
|
|
322
|
+
bufferAndBroadcast(sessionId, { type: "error", message: errMsg });
|
|
316
323
|
} finally {
|
|
317
324
|
if (heartbeat) clearInterval(heartbeat);
|
|
318
|
-
|
|
319
|
-
if (!doneEmitted) {
|
|
320
|
-
bufferAndBroadcast(sessionId, { type: "done", sessionId, contextWindowPct: lastContextWindowPct });
|
|
321
|
-
}
|
|
322
|
-
// 2. Clear buffer BEFORE setting phase to idle
|
|
325
|
+
entry.isStreamingActive = false;
|
|
323
326
|
entry.turnEvents = [];
|
|
324
|
-
// 3. Transition to idle
|
|
325
327
|
setPhase(sessionId, "idle");
|
|
326
|
-
// 4. Cleanup
|
|
327
|
-
entry.abort = undefined;
|
|
328
328
|
entry.pendingApprovalEvent = undefined;
|
|
329
|
+
// Close streaming session in provider
|
|
330
|
+
const provider = providerRegistry.get(entry.providerId);
|
|
331
|
+
if (provider && "closeStreamingSession" in provider) {
|
|
332
|
+
(provider as any).closeStreamingSession(sessionId);
|
|
333
|
+
}
|
|
329
334
|
if (entry.clients.size === 0) {
|
|
330
335
|
startCleanupTimer(sessionId);
|
|
331
336
|
}
|
|
337
|
+
console.log(`[chat] session=${sessionId} consumer loop ended`);
|
|
332
338
|
}
|
|
333
339
|
}
|
|
334
340
|
|
|
@@ -404,6 +410,7 @@ export const chatWebSocket = {
|
|
|
404
410
|
pingIntervals: new Map(),
|
|
405
411
|
phase: "idle",
|
|
406
412
|
turnEvents: [],
|
|
413
|
+
isStreamingActive: false,
|
|
407
414
|
};
|
|
408
415
|
activeSessions.set(sessionId, newEntry);
|
|
409
416
|
setupClientPing(newEntry, ws);
|
|
@@ -453,7 +460,7 @@ export const chatWebSocket = {
|
|
|
453
460
|
if (pn) { try { pp = resolveProjectPath(pn); } catch { /* ignore */ } }
|
|
454
461
|
const newEntry: SessionEntry = {
|
|
455
462
|
providerId: pid, clients: new Set([ws]), projectPath: pp, projectName: pn,
|
|
456
|
-
pingIntervals: new Map(), phase: "idle", turnEvents: [],
|
|
463
|
+
pingIntervals: new Map(), phase: "idle", turnEvents: [], isStreamingActive: false,
|
|
457
464
|
};
|
|
458
465
|
activeSessions.set(sessionId, newEntry);
|
|
459
466
|
setupClientPing(newEntry, ws);
|
|
@@ -490,51 +497,74 @@ export const chatWebSocket = {
|
|
|
490
497
|
ws.send(JSON.stringify({ type: "error", message: "Message content is required" }));
|
|
491
498
|
return;
|
|
492
499
|
}
|
|
500
|
+
// Validate image payload
|
|
501
|
+
if (parsed.images?.length) {
|
|
502
|
+
if (parsed.images.length > 5) {
|
|
503
|
+
ws.send(JSON.stringify({ type: "error", message: "Max 5 images per message" }));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const MAX_BASE64_SIZE = 7_000_000; // ~5MB decoded
|
|
507
|
+
const SUPPORTED_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
508
|
+
for (const img of parsed.images) {
|
|
509
|
+
if (img.data.length > MAX_BASE64_SIZE) {
|
|
510
|
+
ws.send(JSON.stringify({ type: "error", message: "Image too large (max 5MB)" }));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (!SUPPORTED_TYPES.has(img.mediaType)) {
|
|
514
|
+
ws.send(JSON.stringify({ type: "error", message: `Unsupported image type: ${img.mediaType}` }));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
493
519
|
// Store permission mode — sticky for this session
|
|
494
520
|
if (parsed.permissionMode) {
|
|
495
521
|
entry.permissionMode = parsed.permissionMode;
|
|
496
522
|
}
|
|
497
523
|
|
|
498
|
-
// Resume session in provider (can be slow on first call — sdkListSessions)
|
|
499
524
|
const provider = providerRegistry.get(providerId);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (
|
|
505
|
-
|
|
506
|
-
|
|
525
|
+
|
|
526
|
+
if (!entry.isStreamingActive) {
|
|
527
|
+
// First message or post-crash recovery: start persistent consumer
|
|
528
|
+
// Resume session in provider (can be slow on first call — sdkListSessions)
|
|
529
|
+
if (provider && "resumeSession" in provider) {
|
|
530
|
+
const t0 = Date.now();
|
|
531
|
+
await (provider as any).resumeSession(sessionId);
|
|
532
|
+
const elapsed = Date.now() - t0;
|
|
533
|
+
if (elapsed > 500) {
|
|
534
|
+
console.warn(`[chat] session=${sessionId} resumeSession took ${elapsed}ms`);
|
|
535
|
+
logSessionEvent(sessionId, "PERF", `resumeSession took ${elapsed}ms`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (entry.projectPath && provider && "ensureProjectPath" in provider) {
|
|
539
|
+
(provider as any).ensureProjectPath(sessionId, entry.projectPath);
|
|
507
540
|
}
|
|
508
|
-
}
|
|
509
|
-
if (entry.projectPath && provider?.ensureProjectPath) {
|
|
510
|
-
provider.ensureProjectPath(sessionId, entry.projectPath);
|
|
511
|
-
}
|
|
512
541
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
entry.
|
|
517
|
-
|
|
518
|
-
|
|
542
|
+
entry.turnEvents = [];
|
|
543
|
+
setPhase(sessionId, "initializing");
|
|
544
|
+
|
|
545
|
+
const permMode = entry.permissionMode;
|
|
546
|
+
const msgImages = parsed.type === "message" ? parsed.images : undefined;
|
|
547
|
+
entry.streamPromise = new Promise<void>((resolve) => {
|
|
548
|
+
setTimeout(() => {
|
|
549
|
+
startSessionConsumer(sessionId, providerId, parsed.content, permMode, msgImages).then(resolve, resolve);
|
|
550
|
+
}, 0);
|
|
551
|
+
});
|
|
552
|
+
} else {
|
|
553
|
+
// Follow-up: push into existing generator via provider
|
|
554
|
+
if (provider && "pushMessage" in provider && parsed.type === "message") {
|
|
555
|
+
(provider as any).pushMessage(sessionId, parsed.content, {
|
|
556
|
+
priority: parsed.priority ?? 'next',
|
|
557
|
+
images: parsed.images,
|
|
558
|
+
});
|
|
519
559
|
}
|
|
520
|
-
//
|
|
521
|
-
entry =
|
|
522
|
-
|
|
560
|
+
// Clear turn events for new turn display + transition phase
|
|
561
|
+
entry.turnEvents = [];
|
|
562
|
+
entry.pendingApprovalEvent = undefined;
|
|
563
|
+
setPhase(sessionId, "thinking");
|
|
564
|
+
console.log(`[chat] session=${sessionId} follow-up pushed to generator`);
|
|
523
565
|
}
|
|
524
|
-
|
|
525
|
-
// Reset for new query
|
|
526
|
-
entry.turnEvents = [];
|
|
527
|
-
setPhase(sessionId, "initializing");
|
|
528
|
-
|
|
529
|
-
// Store promise reference on entry to prevent GC from collecting the async operation.
|
|
530
|
-
// Use setTimeout(0) to detach from WS handler's async scope.
|
|
531
|
-
const permMode = entry.permissionMode;
|
|
532
|
-
entry.streamPromise = new Promise<void>((resolve) => {
|
|
533
|
-
setTimeout(() => {
|
|
534
|
-
runStreamLoop(sessionId, providerId, parsed.content, permMode).then(resolve, resolve);
|
|
535
|
-
}, 0);
|
|
536
|
-
});
|
|
537
566
|
} else if (parsed.type === "cancel") {
|
|
567
|
+
// Interrupt current turn — session stays alive for next message
|
|
538
568
|
const provider = providerRegistry.get(providerId);
|
|
539
569
|
provider?.abortQuery?.(sessionId);
|
|
540
570
|
} else if (parsed.type === "approval_response") {
|
|
@@ -559,7 +589,7 @@ export const chatWebSocket = {
|
|
|
559
589
|
evictClient(entry, ws);
|
|
560
590
|
console.log(`[chat] session=${sessionId} FE disconnected (phase=${entry.phase}, clients=${entry.clients.size})`);
|
|
561
591
|
|
|
562
|
-
if (entry.clients.size === 0 && entry.
|
|
592
|
+
if (entry.clients.size === 0 && !entry.isStreamingActive) {
|
|
563
593
|
startCleanupTimer(sessionId);
|
|
564
594
|
}
|
|
565
595
|
},
|
package/src/types/api.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type TerminalWsMessage =
|
|
|
23
23
|
|
|
24
24
|
/** WebSocket message types (chat) */
|
|
25
25
|
export type ChatWsClientMessage =
|
|
26
|
-
| { type: "message"; content: string; permissionMode?: string }
|
|
26
|
+
| { type: "message"; content: string; permissionMode?: string; priority?: 'now' | 'next' | 'later'; images?: Array<{ data: string; mediaType: string }> }
|
|
27
27
|
| { type: "cancel" }
|
|
28
28
|
| { type: "approval_response"; requestId: string; approved: boolean; reason?: string; data?: unknown }
|
|
29
29
|
| { type: "ready" };
|