devez-vibe 1.3.27 → 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,93 +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 graphemeSegmenter = typeof Intl.Segmenter === "function"
1015
- ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
1016
- : null;
1017
-
1018
- function splitGraphemes(text) {
1019
- return graphemeSegmenter
1020
- ? Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment)
1021
- : Array.from(text);
1022
- }
1023
-
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) {
1027
- 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
- return {
1033
- chunk: graphemes.slice(0, size).join(""),
1034
- rest: graphemes.slice(size).join(""),
1035
- };
1036
- }
1037
-
1038
- class SmoothTextStream {
1039
- constructor(emit, intervalMs = SMOOTH_TEXT_INTERVAL_MS) {
1040
- this.emit = emit;
1041
- this.intervalMs = intervalMs;
1042
- this.pending = "";
1043
- this.timer = null;
1044
- this.waiters = [];
1045
- }
1046
-
1047
- push(text) {
1048
- this.pending += text;
1049
- this.schedule();
1050
- }
1051
-
1052
- schedule() {
1053
- if (this.timer != null) return;
1054
- // Wait one visual frame before the first drain. Claude often sends several
1055
- // tiny deltas back-to-back; batching them removes the uneven one-character
1056
- // jumps while keeping added latency below one frame.
1057
- this.timer = setTimeout(() => {
1058
- this.timer = null;
1059
- this.drain();
1060
- }, this.intervalMs);
1061
- }
1062
-
1063
- drain() {
1064
- if (!this.pending) {
1065
- this.timer = null;
1066
- for (const resolve of this.waiters.splice(0)) resolve();
1067
- return;
1068
- }
1069
- const { chunk, rest } = takeSmoothTextChunk(this.pending);
1070
- this.pending = rest;
1071
- this.emit(chunk);
1072
- if (this.pending) {
1073
- this.schedule();
1074
- } else {
1075
- for (const resolve of this.waiters.splice(0)) resolve();
1076
- }
1077
- }
1078
-
1079
- finish() {
1080
- if (!this.pending && this.timer == null) return Promise.resolve();
1081
- return new Promise((resolve) => this.waiters.push(resolve));
1082
- }
1083
-
1084
- flush() {
1085
- if (this.timer != null) clearTimeout(this.timer);
1086
- this.timer = null;
1087
- if (this.pending) this.emit(this.pending);
1088
- this.pending = "";
1089
- for (const resolve of this.waiters.splice(0)) resolve();
1090
- }
1091
- }
1092
-
1093
- function flushSmoothStreams(session) {
1094
- for (const block of session.streamBlocks.values()) block.smooth?.flush();
1095
- }
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.
1096
1013
 
1097
1014
  function tokenBreakdown(usage) {
1098
1015
  if (!usage) return null;
@@ -1150,7 +1067,6 @@ async function processStreamEvent(session, message) {
1150
1067
  if (!session.turn || message.parent_tool_use_id) return;
1151
1068
  const event = message.event || {};
1152
1069
  if (event.type === "message_start") {
1153
- flushSmoothStreams(session);
1154
1070
  session.streamBlocks.clear();
1155
1071
  }
1156
1072
  if (event.type === "content_block_start") {
@@ -1160,9 +1076,10 @@ async function processStreamEvent(session, message) {
1160
1076
  const item = block.type === "text"
1161
1077
  ? { id, type: "agentMessage", text: "", provider: "Claude" }
1162
1078
  : { id, type: "reasoning", summary: [] };
1163
- const smooth = block.type === "text"
1164
- ? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
1165
- : null;
1079
+ const method = block.type === "text"
1080
+ ? "item/agentMessage/delta"
1081
+ : "item/reasoning/summaryTextDelta";
1082
+ const emit = (delta) => emitDelta(session, method, id, delta);
1166
1083
  // A held English line can end up dropped entirely, and an item announced
1167
1084
  // before that decision would stay on screen as an empty bubble. Hold the
1168
1085
  // start too, and emit it with the first text that survives.
@@ -1171,7 +1088,7 @@ async function processStreamEvent(session, message) {
1171
1088
  id,
1172
1089
  type: block.type,
1173
1090
  text: "",
1174
- smooth,
1091
+ emit,
1175
1092
  languagePending: held ? "" : null,
1176
1093
  holdEnglishProgress: false,
1177
1094
  pendingStart: held ? item : null,
@@ -1188,10 +1105,6 @@ async function processStreamEvent(session, message) {
1188
1105
  // Held text may still be dropped, and counting it as visible would suppress
1189
1106
  // the opening notice in its place — leaving the turn with nothing to show.
1190
1107
  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
1108
  if (current.languagePending != null) {
1196
1109
  current.languagePending += delta;
1197
1110
  const probe = current.languagePending.trimStart();
@@ -1203,11 +1116,11 @@ async function processStreamEvent(session, message) {
1203
1116
  }
1204
1117
  emitHeldStart(session, current);
1205
1118
  session.turn.sawVisibleText = true;
1206
- current.smooth.push(current.languagePending);
1119
+ current.emit(current.languagePending);
1207
1120
  current.languagePending = null;
1208
1121
  return;
1209
1122
  }
1210
- current.smooth.push(delta);
1123
+ current.emit(delta);
1211
1124
  return;
1212
1125
  }
1213
1126
  if (event.type === "content_block_stop") {
@@ -1223,10 +1136,9 @@ async function processStreamEvent(session, message) {
1223
1136
  }
1224
1137
  emitHeldStart(session, current);
1225
1138
  if (visible.trim()) session.turn.sawVisibleText = true;
1226
- current.smooth?.push(visible);
1139
+ current.emit(visible);
1227
1140
  }
1228
1141
  emitHeldStart(session, current);
1229
- await current.smooth?.finish();
1230
1142
  const item = current.type === "text"
1231
1143
  ? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
1232
1144
  : { id: current.id, type: "reasoning", summary: [current.text] };
@@ -1912,7 +1824,6 @@ async function runPendingPrompt(session) {
1912
1824
  function finishTurn(session, error, durationMs) {
1913
1825
  if (!session.turn) return;
1914
1826
  flushPendingPlan(session);
1915
- flushSmoothStreams(session);
1916
1827
  clearForegroundSubagents(session);
1917
1828
  const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
1918
1829
  if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
@@ -2122,22 +2033,22 @@ function historyState(messages) {
2122
2033
  const userText = message.type === "user"
2123
2034
  ? stripInternalTags(stripHandoff(blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n")))
2124
2035
  : "";
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") {
2036
+ if (userText
2037
+ && !blocks.some((block) => block.type === "tool_result")
2038
+ && !isInternalHistoryText(message, userText)) {
2039
+ turn = {
2040
+ id: `claude-turn-${message.uuid}`,
2041
+ status: "completed",
2042
+ items: [{ id: `claude-user-${message.uuid}`, type: "userMessage", content: [{ type: "text", text: userText }] }],
2043
+ };
2044
+ const startedAt = messageTime(message);
2045
+ if (startedAt != null) turn.startedAt = Math.floor(startedAt / 1000);
2046
+ turns.push(turn);
2047
+ }
2048
+ if (!turn) continue;
2049
+ const completedAt = messageTime(message);
2050
+ if (completedAt != null) turn.completedAt = Math.floor(completedAt / 1000);
2051
+ if (message.type === "assistant") {
2141
2052
  if (message.message?.model === "<synthetic>") {
2142
2053
  turn.synthetic = true;
2143
2054
  continue;
@@ -2374,6 +2285,14 @@ async function dispatch(method, params = {}) {
2374
2285
  ...(rejection ? { rejection } : {}),
2375
2286
  };
2376
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
+ }
2377
2296
  if (method === "session/start") {
2378
2297
  const { session, account, usage } = await createSession(params);
2379
2298
  return {
@@ -3019,25 +2938,6 @@ async function runSelfTest() {
3019
2938
  if (keptStarted !== 0 || keptCompleted?.params?.item?.text !== "타일 보기 로직을 고쳤습니다.") {
3020
2939
  throw new Error(`Claude held Korean text self-test failed: ${JSON.stringify(keptEvents)}`);
3021
2940
  }
3022
- const smoothText = "Claude가 👨‍👩‍👧‍👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
3023
- const emitted = [];
3024
- const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
3025
- smooth.push(smoothText);
3026
- if (emitted.length !== 0) {
3027
- throw new Error(`Claude smooth stream did not batch its first frame: ${JSON.stringify(emitted)}`);
3028
- }
3029
- await smooth.finish();
3030
- if (emitted.length < 2 || emitted.join("") !== smoothText) {
3031
- throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
3032
- }
3033
- const flushed = [];
3034
- const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
3035
- interrupted.push(smoothText);
3036
- interrupted.flush();
3037
- await interrupted.finish();
3038
- if (flushed.join("") !== smoothText) {
3039
- throw new Error(`Claude smooth stream flush self-test failed: ${JSON.stringify(flushed)}`);
3040
- }
3041
2941
  process.stdout.write("Claude bridge self-test passed\n");
3042
2942
  }
3043
2943
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.3.27",
3
+ "version": "1.3.30",
4
4
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
5
  "keywords": [
6
6
  "codex",