devez-vibe 1.3.28 → 1.3.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/bin/dvz.exe CHANGED
Binary file
@@ -1006,117 +1006,10 @@ function emitOpeningNotice(session) {
1006
1006
  session.turn.openingNoticeEmitted = true;
1007
1007
  }
1008
1008
 
1009
- // Windows rounds larger timer delays up to the next scheduler slice. Ten
1010
- // milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
1011
- const SMOOTH_TEXT_INTERVAL_MS = 10;
1012
- const SMOOTH_TEXT_TARGET_FRAMES = 10;
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;
1022
- const graphemeSegmenter = typeof Intl.Segmenter === "function"
1023
- ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
1024
- : null;
1025
-
1026
- function splitGraphemes(text) {
1027
- return graphemeSegmenter
1028
- ? Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment)
1029
- : Array.from(text);
1030
- }
1031
-
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) {
1036
- const graphemes = splitGraphemes(text);
1037
- return {
1038
- chunk: graphemes.slice(0, size).join(""),
1039
- rest: graphemes.slice(size).join(""),
1040
- };
1041
- }
1042
-
1043
- class SmoothTextStream {
1044
- constructor(emit, intervalMs = SMOOTH_TEXT_INTERVAL_MS) {
1045
- this.emit = emit;
1046
- this.intervalMs = intervalMs;
1047
- this.pending = "";
1048
- this.timer = null;
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;
1068
- }
1069
-
1070
- push(text) {
1071
- this.pending += text;
1072
- this.schedule();
1073
- }
1074
-
1075
- schedule() {
1076
- if (this.timer != null) return;
1077
- // Wait one visual frame before the first drain. Claude often sends several
1078
- // tiny deltas back-to-back; batching them removes the uneven one-character
1079
- // jumps while keeping added latency below one frame.
1080
- this.timer = setTimeout(() => {
1081
- this.timer = null;
1082
- this.drain();
1083
- }, this.intervalMs);
1084
- }
1085
-
1086
- drain() {
1087
- if (!this.pending) {
1088
- this.timer = null;
1089
- for (const resolve of this.waiters.splice(0)) resolve();
1090
- return;
1091
- }
1092
- const size = this.nextChunkSize(splitGraphemes(this.pending).length);
1093
- const { chunk, rest } = takeSmoothTextChunk(this.pending, size);
1094
- this.pending = rest;
1095
- this.emit(chunk);
1096
- if (this.pending) {
1097
- this.schedule();
1098
- } else {
1099
- for (const resolve of this.waiters.splice(0)) resolve();
1100
- }
1101
- }
1102
-
1103
- finish() {
1104
- if (!this.pending && this.timer == null) return Promise.resolve();
1105
- return new Promise((resolve) => this.waiters.push(resolve));
1106
- }
1107
-
1108
- flush() {
1109
- if (this.timer != null) clearTimeout(this.timer);
1110
- this.timer = null;
1111
- if (this.pending) this.emit(this.pending);
1112
- this.pending = "";
1113
- for (const resolve of this.waiters.splice(0)) resolve();
1114
- }
1115
- }
1116
-
1117
- function flushSmoothStreams(session) {
1118
- for (const block of session.streamBlocks.values()) block.smooth?.flush();
1119
- }
1009
+ // Text pacing lives in the host renderer, not here. A timer in this process
1010
+ // runs on Node's scheduler, which cannot line up with the terminal's redraw
1011
+ // rhythm the mismatch was itself the visible stutter. Forward each delta as
1012
+ // it arrives and let the renderer reveal it on its own frames.
1120
1013
 
1121
1014
  function tokenBreakdown(usage) {
1122
1015
  if (!usage) return null;
@@ -1174,7 +1067,6 @@ async function processStreamEvent(session, message) {
1174
1067
  if (!session.turn || message.parent_tool_use_id) return;
1175
1068
  const event = message.event || {};
1176
1069
  if (event.type === "message_start") {
1177
- flushSmoothStreams(session);
1178
1070
  session.streamBlocks.clear();
1179
1071
  }
1180
1072
  if (event.type === "content_block_start") {
@@ -1184,12 +1076,10 @@ async function processStreamEvent(session, message) {
1184
1076
  const item = block.type === "text"
1185
1077
  ? { id, type: "agentMessage", text: "", provider: "Claude" }
1186
1078
  : { id, type: "reasoning", summary: [] };
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
1079
  const method = block.type === "text"
1190
1080
  ? "item/agentMessage/delta"
1191
1081
  : "item/reasoning/summaryTextDelta";
1192
- const smooth = new SmoothTextStream((delta) => emitDelta(session, method, id, delta));
1082
+ const emit = (delta) => emitDelta(session, method, id, delta);
1193
1083
  // A held English line can end up dropped entirely, and an item announced
1194
1084
  // before that decision would stay on screen as an empty bubble. Hold the
1195
1085
  // start too, and emit it with the first text that survives.
@@ -1198,7 +1088,7 @@ async function processStreamEvent(session, message) {
1198
1088
  id,
1199
1089
  type: block.type,
1200
1090
  text: "",
1201
- smooth,
1091
+ emit,
1202
1092
  languagePending: held ? "" : null,
1203
1093
  holdEnglishProgress: false,
1204
1094
  pendingStart: held ? item : null,
@@ -1226,11 +1116,11 @@ async function processStreamEvent(session, message) {
1226
1116
  }
1227
1117
  emitHeldStart(session, current);
1228
1118
  session.turn.sawVisibleText = true;
1229
- current.smooth.push(current.languagePending);
1119
+ current.emit(current.languagePending);
1230
1120
  current.languagePending = null;
1231
1121
  return;
1232
1122
  }
1233
- current.smooth.push(delta);
1123
+ current.emit(delta);
1234
1124
  return;
1235
1125
  }
1236
1126
  if (event.type === "content_block_stop") {
@@ -1246,10 +1136,9 @@ async function processStreamEvent(session, message) {
1246
1136
  }
1247
1137
  emitHeldStart(session, current);
1248
1138
  if (visible.trim()) session.turn.sawVisibleText = true;
1249
- current.smooth?.push(visible);
1139
+ current.emit(visible);
1250
1140
  }
1251
1141
  emitHeldStart(session, current);
1252
- await current.smooth?.finish();
1253
1142
  const item = current.type === "text"
1254
1143
  ? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
1255
1144
  : { id: current.id, type: "reasoning", summary: [current.text] };
@@ -1935,7 +1824,6 @@ async function runPendingPrompt(session) {
1935
1824
  function finishTurn(session, error, durationMs) {
1936
1825
  if (!session.turn) return;
1937
1826
  flushPendingPlan(session);
1938
- flushSmoothStreams(session);
1939
1827
  clearForegroundSubagents(session);
1940
1828
  const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
1941
1829
  if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
@@ -2397,6 +2285,14 @@ async function dispatch(method, params = {}) {
2397
2285
  ...(rejection ? { rejection } : {}),
2398
2286
  };
2399
2287
  }
2288
+ // The host clears its `Working` row on `turn/completed` alone, so a single
2289
+ // dropped notification would strand the spinner forever. This answers "is the
2290
+ // turn you are still waiting on actually running?" from the bridge's own state.
2291
+ if (method === "session/turnStatus") {
2292
+ const session = lookupSession(params.sessionId);
2293
+ if (!session) return { known: false, running: false, turnId: null };
2294
+ return { known: true, running: Boolean(session.turn), turnId: session.turn?.id ?? null };
2295
+ }
2400
2296
  if (method === "session/start") {
2401
2297
  const { session, account, usage } = await createSession(params);
2402
2298
  return {
@@ -3042,33 +2938,6 @@ async function runSelfTest() {
3042
2938
  if (keptStarted !== 0 || keptCompleted?.params?.item?.text !== "타일 보기 로직을 고쳤습니다.") {
3043
2939
  throw new Error(`Claude held Korean text self-test failed: ${JSON.stringify(keptEvents)}`);
3044
2940
  }
3045
- const smoothText = "Claude가 👨‍👩‍👧‍👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
3046
- const emitted = [];
3047
- const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
3048
- smooth.push(smoothText);
3049
- if (emitted.length !== 0) {
3050
- throw new Error(`Claude smooth stream did not batch its first frame: ${JSON.stringify(emitted)}`);
3051
- }
3052
- await smooth.finish();
3053
- if (emitted.length < 2 || emitted.join("") !== smoothText) {
3054
- throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
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
- }
3064
- const flushed = [];
3065
- const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
3066
- interrupted.push(smoothText);
3067
- interrupted.flush();
3068
- await interrupted.finish();
3069
- if (flushed.join("") !== smoothText) {
3070
- throw new Error(`Claude smooth stream flush self-test failed: ${JSON.stringify(flushed)}`);
3071
- }
3072
2941
  process.stdout.write("Claude bridge self-test passed\n");
3073
2942
  }
3074
2943
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.3.28",
3
+ "version": "1.3.30",
4
4
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
5
  "keywords": [
6
6
  "codex",