@hienlh/ppm 0.9.29 → 0.9.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.29",
3
+ "version": "0.9.30",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -162,33 +162,61 @@ async function handleStreaming(
162
162
  send(chunk({ role: "assistant", content: "" }, null));
163
163
 
164
164
  const skipBlockIndices = new Set<number>();
165
+ let streamed = false;
166
+ let lastContentLen = 0;
165
167
 
166
168
  for await (const message of response) {
167
- if (message.type !== "stream_event") continue;
168
-
169
- const event = (message as any).event;
170
- const eventType = event.type as string;
171
- const eventIndex = event.index as number | undefined;
172
-
173
- // Track and skip tool_use blocks
174
- if (eventType === "content_block_start" && event.content_block?.type === "tool_use") {
175
- if (eventIndex !== undefined) skipBlockIndices.add(eventIndex);
169
+ const msgType = (message as any).type;
170
+
171
+ // ── stream_event: raw Anthropic SSE events (best quality) ──
172
+ if (msgType === "stream_event") {
173
+ const event = (message as any).event;
174
+ const eventType = event.type as string;
175
+ const eventIndex = event.index as number | undefined;
176
+
177
+ if (eventType === "content_block_start" && event.content_block?.type === "tool_use") {
178
+ if (eventIndex !== undefined) skipBlockIndices.add(eventIndex);
179
+ continue;
180
+ }
181
+ if (eventIndex !== undefined && skipBlockIndices.has(eventIndex)) continue;
182
+
183
+ if (eventType === "content_block_delta" && event.delta?.type === "text_delta") {
184
+ const text = event.delta.text ?? "";
185
+ if (text) send(chunk({ content: text }, null));
186
+ }
187
+ if (eventType === "message_stop") send(chunk({}, "stop"));
188
+ streamed = true;
176
189
  continue;
177
190
  }
178
- if (eventIndex !== undefined && skipBlockIndices.has(eventIndex)) continue;
179
191
 
180
- // Text deltas OpenAI content chunks
181
- if (eventType === "content_block_delta" && event.delta?.type === "text_delta") {
182
- const text = event.delta.text ?? "";
183
- if (text) send(chunk({ content: text }, null));
192
+ // ── partial: incremental content (fallback if no stream_event) ──
193
+ if (msgType === "partial" && !streamed) {
194
+ const content = (message as any).message?.content ?? [];
195
+ let fullText = "";
196
+ for (const block of content) {
197
+ if (block.type === "text") fullText += block.text ?? "";
198
+ }
199
+ const delta = fullText.slice(lastContentLen);
200
+ if (delta) {
201
+ send(chunk({ content: delta }, null));
202
+ lastContentLen = fullText.length;
203
+ }
204
+ continue;
184
205
  }
185
206
 
186
- // Message complete finish chunk
187
- if (eventType === "message_stop") {
188
- send(chunk({}, "stop"));
207
+ // ── assistant: final message (fallback if nothing streamed) ──
208
+ if (msgType === "assistant" && !streamed && lastContentLen === 0) {
209
+ const content = (message as any).message?.content ?? [];
210
+ let fullText = "";
211
+ for (const block of content) {
212
+ if (block.type === "text") fullText += block.text ?? "";
213
+ }
214
+ if (fullText) send(chunk({ content: fullText }, null));
189
215
  }
190
216
  }
191
217
 
218
+ // Always send finish + DONE
219
+ if (!streamed) send(chunk({}, "stop"));
192
220
  accountSelector.onSuccess(account.id);
193
221
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
194
222
  controller.close();
@@ -172,40 +172,82 @@ async function handleStreaming(
172
172
 
173
173
  // Track tool_use block indices to filter them out
174
174
  const skipBlockIndices = new Set<number>();
175
+ let streamed = false; // track if we sent any SSE events
176
+ let lastContentLen = 0; // for partial message diff
175
177
 
176
178
  try {
177
179
  for await (const message of response) {
178
- if (message.type !== "stream_event") continue;
180
+ const msgType = (message as any).type;
179
181
 
180
- const event = (message as any).event;
181
- const eventType = event.type as string;
182
- const eventIndex = event.index as number | undefined;
182
+ // ── stream_event: raw Anthropic SSE events (best quality) ──
183
+ if (msgType === "stream_event") {
184
+ const event = (message as any).event;
185
+ const eventType = event.type as string;
186
+ const eventIndex = event.index as number | undefined;
183
187
 
184
- // Filter tool_use content blocks external tools expect text only
185
- if (eventType === "content_block_start") {
186
- const block = event.content_block;
187
- if (block?.type === "tool_use") {
188
+ if (eventType === "content_block_start" && event.content_block?.type === "tool_use") {
188
189
  if (eventIndex !== undefined) skipBlockIndices.add(eventIndex);
189
190
  continue;
190
191
  }
192
+ if (eventIndex !== undefined && skipBlockIndices.has(eventIndex)) continue;
193
+
194
+ if (eventType === "message_delta") {
195
+ const patched = {
196
+ ...event,
197
+ delta: { ...(event.delta || {}), stop_reason: "end_turn" },
198
+ usage: event.usage || { output_tokens: 0 },
199
+ };
200
+ controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(patched)}\n\n`));
201
+ } else {
202
+ controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`));
203
+ }
204
+ streamed = true;
205
+ continue;
191
206
  }
192
207
 
193
- // Skip deltas and stops for tool_use blocks
194
- if (eventIndex !== undefined && skipBlockIndices.has(eventIndex)) continue;
195
-
196
- // Override message_delta to always show end_turn
197
- if (eventType === "message_delta") {
198
- const patched = {
199
- ...event,
200
- delta: { ...(event.delta || {}), stop_reason: "end_turn" },
201
- usage: event.usage || { output_tokens: 0 },
202
- };
203
- controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(patched)}\n\n`));
208
+ // ── partial: incremental content (fallback if no stream_event) ──
209
+ if (msgType === "partial" && !streamed) {
210
+ const content = (message as any).message?.content ?? [];
211
+ let fullText = "";
212
+ for (const block of content) {
213
+ if (block.type === "text") fullText += block.text ?? "";
214
+ }
215
+ const delta = fullText.slice(lastContentLen);
216
+ if (delta) {
217
+ // Emit Anthropic SSE envelope on first partial
218
+ if (lastContentLen === 0) {
219
+ const msgStart = { type: "message_start", message: { id: `msg_${Date.now()}`, type: "message", role: "assistant", model: body.model, content: [], stop_reason: null, usage: { input_tokens: 0, output_tokens: 0 } } };
220
+ controller.enqueue(encoder.encode(`event: message_start\ndata: ${JSON.stringify(msgStart)}\n\n`));
221
+ controller.enqueue(encoder.encode(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n`));
222
+ }
223
+ controller.enqueue(encoder.encode(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: delta } })}\n\n`));
224
+ lastContentLen = fullText.length;
225
+ }
204
226
  continue;
205
227
  }
206
228
 
207
- // Forward all other events (message_start, text deltas, content_block_start/stop, message_stop)
208
- controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`));
229
+ // ── assistant: final complete message (fallback if nothing streamed) ──
230
+ if (msgType === "assistant" && !streamed && lastContentLen === 0) {
231
+ const content = (message as any).message?.content ?? [];
232
+ let fullText = "";
233
+ for (const block of content) {
234
+ if (block.type === "text") fullText += block.text ?? "";
235
+ }
236
+ if (fullText) {
237
+ const msgStart = { type: "message_start", message: { id: `msg_${Date.now()}`, type: "message", role: "assistant", model: body.model, content: [], stop_reason: null, usage: { input_tokens: 0, output_tokens: 0 } } };
238
+ controller.enqueue(encoder.encode(`event: message_start\ndata: ${JSON.stringify(msgStart)}\n\n`));
239
+ controller.enqueue(encoder.encode(`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n`));
240
+ controller.enqueue(encoder.encode(`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: fullText } })}\n\n`));
241
+ lastContentLen = fullText.length;
242
+ }
243
+ }
244
+ }
245
+
246
+ // Close SSE envelope if we used partial/assistant fallback
247
+ if (!streamed && lastContentLen > 0) {
248
+ controller.enqueue(encoder.encode(`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: 0 })}\n\n`));
249
+ controller.enqueue(encoder.encode(`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 0 } })}\n\n`));
250
+ controller.enqueue(encoder.encode(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`));
209
251
  }
210
252
 
211
253
  accountSelector.onSuccess(account.id);