devez-vibe 1.3.26 → 1.3.28

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/bin/dvz.exe CHANGED
Binary file
@@ -1011,6 +1011,14 @@ function emitOpeningNotice(session) {
1011
1011
  const SMOOTH_TEXT_INTERVAL_MS = 10;
1012
1012
  const SMOOTH_TEXT_TARGET_FRAMES = 10;
1013
1013
  const SMOOTH_TEXT_MAX_GRAPHEMES = 24;
1014
+ const SMOOTH_TEXT_MIN_RATE = 1;
1015
+ // Ease the per-frame rate toward the backlog's demand instead of adopting it at
1016
+ // once: a phrase-sized event would otherwise jump straight to a wide chunk and
1017
+ // read as a lurch, and the next tiny event would snap back to one grapheme.
1018
+ const SMOOTH_TEXT_RATE_EASING = 0.25;
1019
+ // Cap how far the eased rate may move in one frame so a sudden 400-grapheme
1020
+ // backlog ramps up over frames instead of widening the very first chunk.
1021
+ const SMOOTH_TEXT_RATE_STEP = 1;
1014
1022
  const graphemeSegmenter = typeof Intl.Segmenter === "function"
1015
1023
  ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
1016
1024
  : null;
@@ -1021,14 +1029,11 @@ function splitGraphemes(text) {
1021
1029
  : Array.from(text);
1022
1030
  }
1023
1031
 
1024
- // Claude can deliver a whole phrase in one SDK event. Drain roughly one visual
1025
- // frame's share at a time, catching up quickly when a large backlog arrives.
1026
- function takeSmoothTextChunk(text) {
1032
+ // Claude can deliver a whole phrase in one SDK event. Drain a steady share per
1033
+ // visual frame, where the rate eases toward the backlog instead of tracking it
1034
+ // event by event.
1035
+ function takeSmoothTextChunk(text, size) {
1027
1036
  const graphemes = splitGraphemes(text);
1028
- const size = Math.min(
1029
- SMOOTH_TEXT_MAX_GRAPHEMES,
1030
- Math.max(1, Math.ceil(graphemes.length / SMOOTH_TEXT_TARGET_FRAMES)),
1031
- );
1032
1037
  return {
1033
1038
  chunk: graphemes.slice(0, size).join(""),
1034
1039
  rest: graphemes.slice(size).join(""),
@@ -1042,6 +1047,24 @@ class SmoothTextStream {
1042
1047
  this.pending = "";
1043
1048
  this.timer = null;
1044
1049
  this.waiters = [];
1050
+ this.rate = SMOOTH_TEXT_MIN_RATE;
1051
+ // Fractional rates only stay smooth if the leftover carries to the next
1052
+ // frame; truncating every frame would quantize the pace back to integers.
1053
+ this.carry = 0;
1054
+ }
1055
+
1056
+ nextChunkSize(pendingLength) {
1057
+ const demand = pendingLength / SMOOTH_TEXT_TARGET_FRAMES;
1058
+ const target = Math.min(
1059
+ SMOOTH_TEXT_MAX_GRAPHEMES,
1060
+ Math.max(SMOOTH_TEXT_MIN_RATE, demand),
1061
+ );
1062
+ const eased = (target - this.rate) * SMOOTH_TEXT_RATE_EASING;
1063
+ this.rate += Math.max(-SMOOTH_TEXT_RATE_STEP, Math.min(SMOOTH_TEXT_RATE_STEP, eased));
1064
+ const budget = this.rate + this.carry;
1065
+ const size = Math.max(SMOOTH_TEXT_MIN_RATE, Math.floor(budget));
1066
+ this.carry = Math.max(0, budget - size);
1067
+ return size;
1045
1068
  }
1046
1069
 
1047
1070
  push(text) {
@@ -1066,7 +1089,8 @@ class SmoothTextStream {
1066
1089
  for (const resolve of this.waiters.splice(0)) resolve();
1067
1090
  return;
1068
1091
  }
1069
- const { chunk, rest } = takeSmoothTextChunk(this.pending);
1092
+ const size = this.nextChunkSize(splitGraphemes(this.pending).length);
1093
+ const { chunk, rest } = takeSmoothTextChunk(this.pending, size);
1070
1094
  this.pending = rest;
1071
1095
  this.emit(chunk);
1072
1096
  if (this.pending) {
@@ -1160,9 +1184,12 @@ async function processStreamEvent(session, message) {
1160
1184
  const item = block.type === "text"
1161
1185
  ? { id, type: "agentMessage", text: "", provider: "Claude" }
1162
1186
  : { id, type: "reasoning", summary: [] };
1163
- const smooth = block.type === "text"
1164
- ? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
1165
- : null;
1187
+ // Thinking arrives in the same phrase-sized events as text, so it needs the
1188
+ // same paced drain raw thinking deltas were the last visible stutter.
1189
+ const method = block.type === "text"
1190
+ ? "item/agentMessage/delta"
1191
+ : "item/reasoning/summaryTextDelta";
1192
+ const smooth = new SmoothTextStream((delta) => emitDelta(session, method, id, delta));
1166
1193
  // A held English line can end up dropped entirely, and an item announced
1167
1194
  // before that decision would stay on screen as an empty bubble. Hold the
1168
1195
  // start too, and emit it with the first text that survives.
@@ -1188,10 +1215,6 @@ async function processStreamEvent(session, message) {
1188
1215
  // Held text may still be dropped, and counting it as visible would suppress
1189
1216
  // the opening notice in its place — leaving the turn with nothing to show.
1190
1217
  if (current.type === "text" && current.languagePending == null) session.turn.sawVisibleText = true;
1191
- if (!current.smooth) {
1192
- emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
1193
- return;
1194
- }
1195
1218
  if (current.languagePending != null) {
1196
1219
  current.languagePending += delta;
1197
1220
  const probe = current.languagePending.trimStart();
@@ -2122,22 +2145,22 @@ function historyState(messages) {
2122
2145
  const userText = message.type === "user"
2123
2146
  ? stripInternalTags(stripHandoff(blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n")))
2124
2147
  : "";
2125
- if (userText
2126
- && !blocks.some((block) => block.type === "tool_result")
2127
- && !isInternalHistoryText(message, userText)) {
2128
- turn = {
2129
- id: `claude-turn-${message.uuid}`,
2130
- status: "completed",
2131
- items: [{ id: `claude-user-${message.uuid}`, type: "userMessage", content: [{ type: "text", text: userText }] }],
2132
- };
2133
- const startedAt = messageTime(message);
2134
- if (startedAt != null) turn.startedAt = Math.floor(startedAt / 1000);
2135
- turns.push(turn);
2136
- }
2137
- if (!turn) continue;
2138
- const completedAt = messageTime(message);
2139
- if (completedAt != null) turn.completedAt = Math.floor(completedAt / 1000);
2140
- if (message.type === "assistant") {
2148
+ if (userText
2149
+ && !blocks.some((block) => block.type === "tool_result")
2150
+ && !isInternalHistoryText(message, userText)) {
2151
+ turn = {
2152
+ id: `claude-turn-${message.uuid}`,
2153
+ status: "completed",
2154
+ items: [{ id: `claude-user-${message.uuid}`, type: "userMessage", content: [{ type: "text", text: userText }] }],
2155
+ };
2156
+ const startedAt = messageTime(message);
2157
+ if (startedAt != null) turn.startedAt = Math.floor(startedAt / 1000);
2158
+ turns.push(turn);
2159
+ }
2160
+ if (!turn) continue;
2161
+ const completedAt = messageTime(message);
2162
+ if (completedAt != null) turn.completedAt = Math.floor(completedAt / 1000);
2163
+ if (message.type === "assistant") {
2141
2164
  if (message.message?.model === "<synthetic>") {
2142
2165
  turn.synthetic = true;
2143
2166
  continue;
@@ -3030,6 +3053,14 @@ async function runSelfTest() {
3030
3053
  if (emitted.length < 2 || emitted.join("") !== smoothText) {
3031
3054
  throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
3032
3055
  }
3056
+ const ramped = [];
3057
+ const rampStream = new SmoothTextStream((chunk) => ramped.push(chunk), 0);
3058
+ rampStream.push("가".repeat(400));
3059
+ await rampStream.finish();
3060
+ const rampSizes = ramped.map((chunk) => splitGraphemes(chunk).length);
3061
+ if (rampSizes[0] > 2 || rampSizes.some((size, index) => index > 0 && size > rampSizes[index - 1] + 2)) {
3062
+ throw new Error(`Claude smooth stream rate jumped: ${JSON.stringify(rampSizes.slice(0, 8))}`);
3063
+ }
3033
3064
  const flushed = [];
3034
3065
  const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
3035
3066
  interrupted.push(smoothText);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "devez-vibe",
3
- "version": "1.3.26",
3
+ "version": "1.3.28",
4
4
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
5
  "keywords": [
6
6
  "codex",