devez-vibe 1.2.8 → 1.2.10

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
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { randomUUID } from "node:crypto";
4
- import { readFile } from "node:fs/promises";
4
+ import { createReadStream } from "node:fs";
5
+ import { readdir, readFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
5
8
  import { createInterface } from "node:readline";
6
9
  import {
7
10
  deleteSession,
@@ -435,9 +438,10 @@ async function createSession(params, resumeId) {
435
438
  turnSequence: 1,
436
439
  itemSequence: 1,
437
440
  streamBlocks: new Map(),
438
- tools: new Map(),
439
- tasks: new Map(),
440
- subagents: new Map(),
441
+ tools: new Map(),
442
+ tasks: new Map(),
443
+ planCreatePending: false,
444
+ subagents: new Map(),
441
445
  knownSubagents: new Map(),
442
446
  lastContextUsage: null,
443
447
  lastContextWindow: 0,
@@ -497,53 +501,53 @@ function emitItem(session, phase, item) {
497
501
  notify(`item/${phase}`, { threadId: session.id, turnId: session.turn?.id, item });
498
502
  }
499
503
 
500
- function emitDelta(session, method, itemId, delta) {
501
- notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
502
- }
503
-
504
- function isKoreanPrompt(input) {
505
- const prompt = (Array.isArray(input) ? input : [])
506
- .filter((item) => item?.type === "text")
507
- .map((item) => String(item.text || ""))
508
- .join("\n");
509
- return /[\uac00-\ud7a3]/.test(prompt);
510
- }
511
-
512
- function openingNotice(input) {
513
- return isKoreanPrompt(input)
514
- ? "요청 내용을 확인하고 필요한 작업을 진행하겠습니다."
515
- : "I’ll review the request and proceed with the necessary work.";
516
- }
517
-
518
- function normalizeProgressText(turn, text) {
519
- const value = String(text || "");
520
- const trimmed = value.trim();
521
- if (turn?.koreanRequest
522
- && trimmed.length <= 160
523
- && !/[\uac00-\ud7a3]/.test(trimmed)
524
- && /^Now\b[^\r\n]*[.!?]?$/i.test(trimmed)) {
525
- return "다음 부분을 이어서 확인하겠습니다.";
526
- }
527
- return value;
528
- }
529
-
530
- // Claude can answer with a tool_use as its first and only content block even
531
- // when the prompt asks for an opening update. Keep the visible contract stable
532
- // without duplicating a real model-written update.
533
- function emitOpeningNotice(session) {
534
- if (!session.turn || session.turn.openingNoticeEmitted) return;
535
- const text = session.turn.openingNotice;
536
- const id = nextItemId(session, "opening");
537
- const item = { id, type: "agentMessage", text, provider: "Claude" };
538
- emitItem(session, "started", item);
539
- emitItem(session, "completed", item);
540
- session.turn.openingNoticeEmitted = true;
541
- }
504
+ function emitDelta(session, method, itemId, delta) {
505
+ notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
506
+ }
507
+
508
+ function isKoreanPrompt(input) {
509
+ const prompt = (Array.isArray(input) ? input : [])
510
+ .filter((item) => item?.type === "text")
511
+ .map((item) => String(item.text || ""))
512
+ .join("\n");
513
+ return /[\uac00-\ud7a3]/.test(prompt);
514
+ }
515
+
516
+ function openingNotice(input) {
517
+ return isKoreanPrompt(input)
518
+ ? "요청 내용을 확인하고 필요한 작업을 진행하겠습니다."
519
+ : "I’ll review the request and proceed with the necessary work.";
520
+ }
521
+
522
+ function normalizeProgressText(turn, text) {
523
+ const value = String(text || "");
524
+ const trimmed = value.trim();
525
+ if (turn?.koreanRequest
526
+ && trimmed.length <= 160
527
+ && !/[\uac00-\ud7a3]/.test(trimmed)
528
+ && /^Now\b[^\r\n]*[.!?]?$/i.test(trimmed)) {
529
+ return "다음 부분을 이어서 확인하겠습니다.";
530
+ }
531
+ return value;
532
+ }
533
+
534
+ // Claude can answer with a tool_use as its first and only content block even
535
+ // when the prompt asks for an opening update. Keep the visible contract stable
536
+ // without duplicating a real model-written update.
537
+ function emitOpeningNotice(session) {
538
+ if (!session.turn || session.turn.openingNoticeEmitted) return;
539
+ const text = session.turn.openingNotice;
540
+ const id = nextItemId(session, "opening");
541
+ const item = { id, type: "agentMessage", text, provider: "Claude" };
542
+ emitItem(session, "started", item);
543
+ emitItem(session, "completed", item);
544
+ session.turn.openingNoticeEmitted = true;
545
+ }
542
546
 
543
547
  // Windows rounds larger timer delays up to the next scheduler slice. Ten
544
548
  // milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
545
549
  const SMOOTH_TEXT_INTERVAL_MS = 10;
546
- const SMOOTH_TEXT_TARGET_FRAMES = 8;
550
+ const SMOOTH_TEXT_TARGET_FRAMES = 10;
547
551
  const SMOOTH_TEXT_MAX_GRAPHEMES = 24;
548
552
  const graphemeSegmenter = typeof Intl.Segmenter === "function"
549
553
  ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
@@ -578,10 +582,21 @@ class SmoothTextStream {
578
582
  this.waiters = [];
579
583
  }
580
584
 
581
- push(text) {
582
- this.pending += text;
583
- if (this.timer == null) this.drain();
584
- }
585
+ push(text) {
586
+ this.pending += text;
587
+ this.schedule();
588
+ }
589
+
590
+ schedule() {
591
+ if (this.timer != null) return;
592
+ // Wait one visual frame before the first drain. Claude often sends several
593
+ // tiny deltas back-to-back; batching them removes the uneven one-character
594
+ // jumps while keeping added latency below one frame.
595
+ this.timer = setTimeout(() => {
596
+ this.timer = null;
597
+ this.drain();
598
+ }, this.intervalMs);
599
+ }
585
600
 
586
601
  drain() {
587
602
  if (!this.pending) {
@@ -590,14 +605,13 @@ class SmoothTextStream {
590
605
  return;
591
606
  }
592
607
  const { chunk, rest } = takeSmoothTextChunk(this.pending);
593
- this.pending = rest;
594
- this.emit(chunk);
595
- if (this.pending) {
596
- this.timer = setTimeout(() => this.drain(), this.intervalMs);
597
- } else {
598
- this.timer = null;
599
- for (const resolve of this.waiters.splice(0)) resolve();
600
- }
608
+ this.pending = rest;
609
+ this.emit(chunk);
610
+ if (this.pending) {
611
+ this.schedule();
612
+ } else {
613
+ for (const resolve of this.waiters.splice(0)) resolve();
614
+ }
601
615
  }
602
616
 
603
617
  finish() {
@@ -671,7 +685,7 @@ async function processStreamEvent(session, message) {
671
685
  flushSmoothStreams(session);
672
686
  session.streamBlocks.clear();
673
687
  }
674
- if (event.type === "content_block_start") {
688
+ if (event.type === "content_block_start") {
675
689
  const block = event.content_block || {};
676
690
  if (block.type !== "text" && block.type !== "thinking") return;
677
691
  const id = nextItemId(session, block.type);
@@ -681,54 +695,54 @@ async function processStreamEvent(session, message) {
681
695
  const smooth = block.type === "text"
682
696
  ? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
683
697
  : null;
684
- session.streamBlocks.set(event.index, {
685
- id,
686
- type: block.type,
687
- text: "",
688
- smooth,
689
- languagePending: block.type === "text" && session.turn.koreanRequest ? "" : null,
690
- holdEnglishProgress: false,
691
- });
698
+ session.streamBlocks.set(event.index, {
699
+ id,
700
+ type: block.type,
701
+ text: "",
702
+ smooth,
703
+ languagePending: block.type === "text" && session.turn.koreanRequest ? "" : null,
704
+ holdEnglishProgress: false,
705
+ });
692
706
  emitItem(session, "started", item);
693
707
  return;
694
708
  }
695
- if (event.type === "content_block_delta") {
709
+ if (event.type === "content_block_delta") {
696
710
  const current = session.streamBlocks.get(event.index);
697
711
  if (!current) return;
698
- const delta = event.delta?.text || event.delta?.thinking || "";
699
- if (!delta) return;
700
- current.text += delta;
701
- if (current.type === "text") session.turn.sawVisibleText = true;
702
- if (!current.smooth) {
703
- emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
704
- return;
705
- }
706
- if (current.languagePending != null) {
707
- current.languagePending += delta;
708
- const probe = current.languagePending.trimStart();
709
- const lower = probe.toLowerCase();
710
- if (!current.holdEnglishProgress && "now".startsWith(lower)) return;
711
- if (/^now(?:\s|$)/i.test(probe)) {
712
- current.holdEnglishProgress = true;
713
- return;
714
- }
715
- current.smooth.push(current.languagePending);
716
- current.languagePending = null;
717
- return;
718
- }
719
- current.smooth.push(delta);
720
- return;
721
- }
712
+ const delta = event.delta?.text || event.delta?.thinking || "";
713
+ if (!delta) return;
714
+ current.text += delta;
715
+ if (current.type === "text") session.turn.sawVisibleText = true;
716
+ if (!current.smooth) {
717
+ emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
718
+ return;
719
+ }
720
+ if (current.languagePending != null) {
721
+ current.languagePending += delta;
722
+ const probe = current.languagePending.trimStart();
723
+ const lower = probe.toLowerCase();
724
+ if (!current.holdEnglishProgress && "now".startsWith(lower)) return;
725
+ if (/^now(?:\s|$)/i.test(probe)) {
726
+ current.holdEnglishProgress = true;
727
+ return;
728
+ }
729
+ current.smooth.push(current.languagePending);
730
+ current.languagePending = null;
731
+ return;
732
+ }
733
+ current.smooth.push(delta);
734
+ return;
735
+ }
722
736
  if (event.type === "content_block_stop") {
723
- const current = session.streamBlocks.get(event.index);
724
- if (!current) return;
725
- if (current.languagePending != null) {
726
- const visible = normalizeProgressText(session.turn, current.languagePending);
727
- current.text = visible;
728
- current.smooth?.push(visible);
729
- current.languagePending = null;
730
- }
731
- await current.smooth?.finish();
737
+ const current = session.streamBlocks.get(event.index);
738
+ if (!current) return;
739
+ if (current.languagePending != null) {
740
+ const visible = normalizeProgressText(session.turn, current.languagePending);
741
+ current.text = visible;
742
+ current.smooth?.push(visible);
743
+ current.languagePending = null;
744
+ }
745
+ await current.smooth?.finish();
732
746
  const item = current.type === "text"
733
747
  ? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
734
748
  : { id: current.id, type: "reasoning", summary: [current.text] };
@@ -753,31 +767,31 @@ function processAssistant(session, message) {
753
767
  message.message?.model,
754
768
  session.model,
755
769
  );
756
- const content = Array.isArray(message.message?.content) ? message.message.content : [];
757
- const hasToolUse = content.some((block) => block.type === "tool_use");
758
- const hasVisibleText = session.turn.sawVisibleText || content.some(
759
- (block) => block.type === "text" && String(block.text || "").trim(),
760
- );
761
- // Without partial SDK events, replay completed text before tool items so the
762
- // visible order still matches the assistant content order.
763
- if (!session.streamBlocks.size && !session.turn.sawStreamText) {
764
- for (const block of content) {
765
- if (block.type !== "text" && block.type !== "thinking") continue;
766
- const id = nextItemId(session, block.type);
767
- const item = block.type === "text"
768
- ? { id, type: "agentMessage", text: normalizeProgressText(session.turn, block.text), provider: "Claude" }
769
- : { id, type: "reasoning", summary: [block.thinking || ""] };
770
- emitItem(session, "started", item);
771
- emitItem(session, "completed", item);
772
- }
773
- }
774
- if (hasToolUse && !hasVisibleText) emitOpeningNotice(session);
775
- for (const block of content) {
776
- if (block.type === "tool_use") processToolUse(session, block);
777
- }
778
- }
770
+ const content = Array.isArray(message.message?.content) ? message.message.content : [];
771
+ const hasToolUse = content.some((block) => block.type === "tool_use");
772
+ const hasVisibleText = session.turn.sawVisibleText || content.some(
773
+ (block) => block.type === "text" && String(block.text || "").trim(),
774
+ );
775
+ // Without partial SDK events, replay completed text before tool items so the
776
+ // visible order still matches the assistant content order.
777
+ if (!session.streamBlocks.size && !session.turn.sawStreamText) {
778
+ for (const block of content) {
779
+ if (block.type !== "text" && block.type !== "thinking") continue;
780
+ const id = nextItemId(session, block.type);
781
+ const item = block.type === "text"
782
+ ? { id, type: "agentMessage", text: normalizeProgressText(session.turn, block.text), provider: "Claude" }
783
+ : { id, type: "reasoning", summary: [block.thinking || ""] };
784
+ emitItem(session, "started", item);
785
+ emitItem(session, "completed", item);
786
+ }
787
+ }
788
+ if (hasToolUse && !hasVisibleText) emitOpeningNotice(session);
789
+ for (const block of content) {
790
+ if (block.type === "tool_use") processToolUse(session, block);
791
+ }
792
+ }
779
793
 
780
- function processToolUse(session, block) {
794
+ function processToolUse(session, block) {
781
795
  const name = block.name || "Tool";
782
796
  const input = block.input || {};
783
797
  if (name === "TaskCreate" || name === "TaskUpdate" || name === "TaskList") {
@@ -785,11 +799,13 @@ function processToolUse(session, block) {
785
799
  session.tools.set(block.id, { name, input, suppressed: true });
786
800
  return;
787
801
  }
788
- if (name === "AskUserQuestion") {
789
- session.tools.set(block.id, { name, input, suppressed: true });
790
- return;
791
- }
792
- const item = toolItem(session, block.id, name, input);
802
+ if (name === "AskUserQuestion") {
803
+ flushPendingPlan(session);
804
+ session.tools.set(block.id, { name, input, suppressed: true });
805
+ return;
806
+ }
807
+ flushPendingPlan(session);
808
+ const item = toolItem(session, block.id, name, input);
793
809
  session.tools.set(block.id, { name, input, item });
794
810
  emitItem(session, "started", item);
795
811
  if (SUBAGENT_TOOLS.includes(name)) startSubagent(session, block);
@@ -895,21 +911,30 @@ function latestTaskPlan(tasks) {
895
911
  return new Map(entries.slice(start));
896
912
  }
897
913
 
898
- function updatePlanFromToolUse(session, name, toolUseId, input) {
914
+ function updatePlanFromToolUse(session, name, toolUseId, input) {
899
915
  const turnId = session.turn?.id;
900
916
  if (name === "TaskCreate") {
901
917
  prepareTaskPlanForCreate(session.tasks, input.subject);
902
- session.tasks.set(`pending:${toolUseId}`, {
918
+ session.tasks.set(`pending:${toolUseId}`, {
903
919
  id: `pending:${toolUseId}`,
904
920
  subject: input.subject || input.description || "작업",
905
921
  status: "pending",
906
- turnId,
907
- });
908
- } else if (name === "TaskUpdate") {
909
- applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
910
- }
911
- emitPlan(session);
912
- }
922
+ turnId,
923
+ });
924
+ session.planCreatePending = true;
925
+ return;
926
+ } else if (name === "TaskUpdate") {
927
+ session.planCreatePending = false;
928
+ applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
929
+ }
930
+ if (name === "TaskUpdate") emitPlan(session);
931
+ }
932
+
933
+ function flushPendingPlan(session) {
934
+ if (!session.planCreatePending) return;
935
+ session.planCreatePending = false;
936
+ emitPlan(session);
937
+ }
913
938
 
914
939
  function updatePlanFromToolResult(session, pending, message) {
915
940
  const value = message.tool_use_result;
@@ -1322,8 +1347,9 @@ async function runPendingPrompt(session) {
1322
1347
  }
1323
1348
  }
1324
1349
 
1325
- function finishTurn(session, error, durationMs) {
1326
- if (!session.turn) return;
1350
+ function finishTurn(session, error, durationMs) {
1351
+ if (!session.turn) return;
1352
+ flushPendingPlan(session);
1327
1353
  flushSmoothStreams(session);
1328
1354
  clearForegroundSubagents(session);
1329
1355
  const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
@@ -1411,7 +1437,7 @@ async function startPrompt(params) {
1411
1437
  return runPrompt(session, params);
1412
1438
  }
1413
1439
 
1414
- async function runPrompt(session, params) {
1440
+ async function runPrompt(session, params) {
1415
1441
  const id = session.id;
1416
1442
  if (params.model) {
1417
1443
  const model = stripClaudeModel(params.model);
@@ -1422,13 +1448,13 @@ async function runPrompt(session, params) {
1422
1448
  if (effort) {
1423
1449
  await session.query.applyFlagSettings({ effortLevel: effort });
1424
1450
  }
1425
- session.effort = effort;
1426
- await applyPermissionMode(session, params.permissionMode);
1427
- const content = await inputContent(params.input, params.handoffContext);
1428
- const turnId = beginTurn(session, params.input);
1429
- session.queue.push({
1430
- type: "user",
1431
- message: { role: "user", content },
1451
+ session.effort = effort;
1452
+ await applyPermissionMode(session, params.permissionMode);
1453
+ const content = await inputContent(params.input, params.handoffContext);
1454
+ const turnId = beginTurn(session, params.input);
1455
+ session.queue.push({
1456
+ type: "user",
1457
+ message: { role: "user", content },
1432
1458
  parent_tool_use_id: null,
1433
1459
  session_id: id,
1434
1460
  origin: { kind: "human" },
@@ -1438,16 +1464,16 @@ async function runPrompt(session, params) {
1438
1464
 
1439
1465
  // A background task notification is an internal user message that starts its
1440
1466
  // own Claude response even though the host did not submit a new prompt.
1441
- function beginTurn(session, input = []) {
1442
- const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
1443
- session.turn = {
1444
- id: turnId,
1445
- sawStreamText: false,
1446
- sawVisibleText: false,
1447
- koreanRequest: isKoreanPrompt(input),
1448
- openingNotice: openingNotice(input),
1449
- openingNoticeEmitted: false,
1450
- };
1467
+ function beginTurn(session, input = []) {
1468
+ const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
1469
+ session.turn = {
1470
+ id: turnId,
1471
+ sawStreamText: false,
1472
+ sawVisibleText: false,
1473
+ koreanRequest: isKoreanPrompt(input),
1474
+ openingNotice: openingNotice(input),
1475
+ openingNoticeEmitted: false,
1476
+ };
1451
1477
  session.lastContextUsage = null;
1452
1478
  notify("turn/started", { threadId: session.id, turn: { id: turnId } });
1453
1479
  return turnId;
@@ -1569,6 +1595,75 @@ function historyTurns(messages) {
1569
1595
  return historyState(messages).turns;
1570
1596
  }
1571
1597
 
1598
+ // A transcript lives in a folder encoded from the cwd string, so two spellings of
1599
+ // the same path (Windows differs only in case) resolve to different folders and a
1600
+ // session recorded under one spelling is invisible to the other. Remember the
1601
+ // spelling the transcript was written with, keyed by session id.
1602
+ const transcriptCwds = new Map();
1603
+
1604
+ function claudeProjectsDir() {
1605
+ return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "projects");
1606
+ }
1607
+
1608
+ /** Read the cwd a transcript records. Only the head is scanned: the opening
1609
+ * records carry no cwd, but a real turn shows up long before the limit. */
1610
+ async function readTranscriptCwd(path, limit = 200) {
1611
+ const stream = createReadStream(path, { encoding: "utf8" });
1612
+ try {
1613
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
1614
+ try {
1615
+ let seen = 0;
1616
+ for await (const line of reader) {
1617
+ if (++seen > limit) break;
1618
+ let cwd;
1619
+ try {
1620
+ cwd = JSON.parse(line)?.cwd;
1621
+ } catch {
1622
+ continue;
1623
+ }
1624
+ if (typeof cwd === "string" && cwd) return cwd;
1625
+ }
1626
+ return null;
1627
+ } finally {
1628
+ reader.close();
1629
+ }
1630
+ } finally {
1631
+ stream.destroy();
1632
+ }
1633
+ }
1634
+
1635
+ /** Locate the transcript of `id` under any project folder and report the cwd it
1636
+ * records, which is the spelling the SDK needs to find it again. */
1637
+ async function transcriptCwd(id) {
1638
+ if (transcriptCwds.has(id)) return transcriptCwds.get(id);
1639
+ let entries;
1640
+ try {
1641
+ entries = await readdir(claudeProjectsDir(), { withFileTypes: true });
1642
+ } catch {
1643
+ return null;
1644
+ }
1645
+ for (const entry of entries) {
1646
+ if (!entry.isDirectory()) continue;
1647
+ let cwd;
1648
+ try {
1649
+ cwd = await readTranscriptCwd(join(claudeProjectsDir(), entry.name, `${id}.jsonl`));
1650
+ } catch {
1651
+ continue;
1652
+ }
1653
+ if (cwd) {
1654
+ transcriptCwds.set(id, cwd);
1655
+ return cwd;
1656
+ }
1657
+ }
1658
+ return null;
1659
+ }
1660
+
1661
+ /** The cwd to read `id`'s transcript with: the one the transcript itself records,
1662
+ * falling back to the host's when no transcript is on disk. */
1663
+ async function readableCwd(id, cwd) {
1664
+ return await transcriptCwd(id) || cwd;
1665
+ }
1666
+
1572
1667
  async function dispatch(method, params = {}) {
1573
1668
  if (method === "model/list") return loadModelCatalog(params);
1574
1669
  if (method === "session/permissionMode") {
@@ -1608,15 +1703,19 @@ async function dispatch(method, params = {}) {
1608
1703
  tokenUsage: historyTokenUsage(messages, existing.models, existing.model),
1609
1704
  };
1610
1705
  }
1611
- const info = await getSessionInfo(id, { dir: params.cwd });
1612
- if (!info) throw new Error(`Claude 세션을 찾을 없습니다: ${id}`);
1613
- const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
1706
+ const dir = await readableCwd(id, params.cwd);
1707
+ // The transcript itself decides whether there is anything to resume:
1708
+ // getSessionInfo only sees sessions the CLI indexed, and a bridge-run session
1709
+ // whose transcript is intact can be missing from that index.
1710
+ const messages = await getSessionMessages(id, { dir, includeSystemMessages: true });
1711
+ if (!messages.length) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
1712
+ const info = await getSessionInfo(id, { dir });
1614
1713
  const lastModel = [...messages].reverse().find((message) => message.type === "assistant")?.message?.model;
1615
1714
  // The transcript's own model outranks the host's fallback, which is only what
1616
1715
  // a new session would have opened on.
1617
1716
  const { session, account, usage } = await createSession({
1618
1717
  ...params,
1619
- cwd: info.cwd || params.cwd,
1718
+ cwd: info?.cwd || dir,
1620
1719
  model: params.model || lastModel || params.fallbackModel,
1621
1720
  effort: params.effort || params.fallbackEffort,
1622
1721
  }, id);
@@ -1657,7 +1756,7 @@ async function dispatch(method, params = {}) {
1657
1756
  }
1658
1757
  if (method === "session/history") {
1659
1758
  const id = liveSessionId(params.sessionId);
1660
- const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
1759
+ const messages = await getSessionMessages(id, { dir: await readableCwd(id, params.cwd), includeSystemMessages: true });
1661
1760
  return { data: historyTurns(messages), nextCursor: null };
1662
1761
  }
1663
1762
  if (method === "session/prompt") return startPrompt(params);
@@ -1683,7 +1782,7 @@ async function dispatch(method, params = {}) {
1683
1782
  }
1684
1783
  if (method === "session/fork") {
1685
1784
  const source = liveSessionId(params.sessionId);
1686
- const forked = await forkSession(source, { dir: params.cwd });
1785
+ const forked = await forkSession(source, { dir: await readableCwd(source, params.cwd) });
1687
1786
  const id = forked.sessionId || forked;
1688
1787
  const { session, account, usage } = await createSession(params, id);
1689
1788
  return {
@@ -1837,9 +1936,53 @@ async function runSelfTest() {
1837
1936
  }
1838
1937
  prepareTaskPlanForCreate(restoredTasks, "7. 추가 작업");
1839
1938
  if (restoredTasks.size !== 6) throw new Error("Claude appended task unexpectedly reset the plan");
1840
- prepareTaskPlanForCreate(restoredTasks, "1. 새 작업");
1841
- if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
1842
- const usage = tokenBreakdown({
1939
+ prepareTaskPlanForCreate(restoredTasks, "1. 새 작업");
1940
+ if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
1941
+ const batchedPlanSession = {
1942
+ id: "batched-plan-self-test",
1943
+ turn: { id: "batched-plan-turn" },
1944
+ tasks: new Map(),
1945
+ planCreatePending: false,
1946
+ };
1947
+ const batchedPlanCaptured = [];
1948
+ const batchedPlanWrite = process.stdout.write;
1949
+ process.stdout.write = (chunk) => {
1950
+ batchedPlanCaptured.push(String(chunk));
1951
+ return true;
1952
+ };
1953
+ try {
1954
+ for (let index = 1; index <= 3; index++) {
1955
+ const toolUseId = `batched-create-${index}`;
1956
+ const subject = `${index}. 작업 ${index}`;
1957
+ updatePlanFromToolUse(batchedPlanSession, "TaskCreate", toolUseId, { subject });
1958
+ updatePlanFromToolResult(
1959
+ batchedPlanSession,
1960
+ { name: "TaskCreate", toolUseId },
1961
+ { tool_use_result: { task: { id: String(index), subject } } },
1962
+ );
1963
+ }
1964
+ updatePlanFromToolUse(
1965
+ batchedPlanSession,
1966
+ "TaskUpdate",
1967
+ "batched-update-1",
1968
+ { taskId: "1", status: "in_progress" },
1969
+ );
1970
+ } finally {
1971
+ process.stdout.write = batchedPlanWrite;
1972
+ }
1973
+ const batchedPlanEvents = batchedPlanCaptured
1974
+ .join("")
1975
+ .trim()
1976
+ .split("\n")
1977
+ .filter(Boolean)
1978
+ .map((line) => JSON.parse(line))
1979
+ .filter((event) => event.method === "turn/plan/updated");
1980
+ if (batchedPlanEvents.length !== 1
1981
+ || batchedPlanEvents[0].params?.plan?.length !== 3
1982
+ || batchedPlanEvents[0].params.plan[0]?.status !== "inProgress") {
1983
+ throw new Error(`Claude batched plan self-test failed: ${JSON.stringify(batchedPlanEvents)}`);
1984
+ }
1985
+ const usage = tokenBreakdown({
1843
1986
  input_tokens: 2,
1844
1987
  cache_read_input_tokens: 68_000,
1845
1988
  cache_creation_input_tokens: 500,
@@ -1966,95 +2109,98 @@ async function runSelfTest() {
1966
2109
  .filter(Boolean)
1967
2110
  .map((line) => JSON.parse(line));
1968
2111
  const lifecycleMethods = lifecycleEvents.map((event) => event.method);
1969
- if (!lifecycleMethods.includes("turn/subagents/updated")
1970
- || lifecycleMethods.filter((method) => method === "turn/started").length < 3
1971
- || !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
1972
- && event.params?.line?.kind === "error")) {
1973
- throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
1974
- }
1975
- const openingSession = {
1976
- id: "opening-self-test",
1977
- model: "claude:default",
1978
- models: [],
1979
- turn: null,
1980
- turnSequence: 1,
1981
- itemSequence: 1,
1982
- streamBlocks: new Map(),
1983
- tools: new Map(),
1984
- tasks: new Map(),
1985
- subagents: new Map(),
1986
- knownSubagents: new Map(),
1987
- lastContextUsage: null,
1988
- lastContextWindow: 0,
1989
- };
1990
- const openingCaptured = [];
1991
- process.stdout.write = (chunk) => {
1992
- openingCaptured.push(String(chunk));
1993
- return true;
1994
- };
1995
- try {
1996
- beginTurn(openingSession, [{ type: "text", text: "provider 메뉴를 수정해" }]);
1997
- processAssistant(openingSession, {
1998
- message: {
1999
- content: [{ type: "tool_use", id: "read-1", name: "Read", input: { file_path: "src/main.rs" } }],
2000
- },
2001
- });
2002
- } finally {
2003
- process.stdout.write = stdoutWrite;
2004
- }
2005
- const openingEvents = openingCaptured
2006
- .join("")
2007
- .trim()
2008
- .split("\n")
2009
- .filter(Boolean)
2010
- .map((line) => JSON.parse(line));
2011
- const openingMessageIndex = openingEvents.findIndex((event) =>
2012
- event.method === "item/completed"
2013
- && event.params?.item?.type === "agentMessage"
2014
- && event.params.item.text === "요청 내용을 확인하고 필요한 작업을 진행하겠습니다.");
2015
- const openingToolIndex = openingEvents.findIndex((event) =>
2016
- event.method === "item/started" && event.params?.item?.type === "dynamicToolCall");
2017
- if (openingMessageIndex < 0 || openingToolIndex < 0 || openingMessageIndex > openingToolIndex) {
2018
- throw new Error(`Claude opening notice order self-test failed: ${JSON.stringify(openingEvents)}`);
2019
- }
2020
- const languageCaptured = [];
2021
- process.stdout.write = (chunk) => {
2022
- languageCaptured.push(String(chunk));
2023
- return true;
2024
- };
2025
- try {
2026
- openingSession.streamBlocks.clear();
2027
- await processStreamEvent(openingSession, {
2028
- event: { type: "content_block_start", index: 0, content_block: { type: "text" } },
2029
- });
2030
- await processStreamEvent(openingSession, {
2031
- event: { type: "content_block_delta", index: 0, delta: { text: "Now the tile view logic." } },
2032
- });
2033
- await processStreamEvent(openingSession, {
2034
- event: { type: "content_block_stop", index: 0 },
2035
- });
2036
- } finally {
2037
- process.stdout.write = stdoutWrite;
2038
- }
2039
- const languageEvents = languageCaptured
2040
- .join("")
2041
- .trim()
2042
- .split("\n")
2043
- .filter(Boolean)
2044
- .map((line) => JSON.parse(line));
2045
- const visibleLanguage = languageEvents
2046
- .filter((event) => event.method === "item/agentMessage/delta")
2047
- .map((event) => event.params?.delta || "")
2048
- .join("");
2049
- if (visibleLanguage !== "다음 부분을 이어서 확인하겠습니다."
2050
- || languageEvents.some((event) => JSON.stringify(event).includes("Now the tile view logic."))) {
2051
- throw new Error(`Claude Korean progress normalization self-test failed: ${JSON.stringify(languageEvents)}`);
2112
+ if (!lifecycleMethods.includes("turn/subagents/updated")
2113
+ || lifecycleMethods.filter((method) => method === "turn/started").length < 3
2114
+ || !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
2115
+ && event.params?.line?.kind === "error")) {
2116
+ throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
2117
+ }
2118
+ const openingSession = {
2119
+ id: "opening-self-test",
2120
+ model: "claude:default",
2121
+ models: [],
2122
+ turn: null,
2123
+ turnSequence: 1,
2124
+ itemSequence: 1,
2125
+ streamBlocks: new Map(),
2126
+ tools: new Map(),
2127
+ tasks: new Map(),
2128
+ subagents: new Map(),
2129
+ knownSubagents: new Map(),
2130
+ lastContextUsage: null,
2131
+ lastContextWindow: 0,
2132
+ };
2133
+ const openingCaptured = [];
2134
+ process.stdout.write = (chunk) => {
2135
+ openingCaptured.push(String(chunk));
2136
+ return true;
2137
+ };
2138
+ try {
2139
+ beginTurn(openingSession, [{ type: "text", text: "provider 메뉴를 수정해" }]);
2140
+ processAssistant(openingSession, {
2141
+ message: {
2142
+ content: [{ type: "tool_use", id: "read-1", name: "Read", input: { file_path: "src/main.rs" } }],
2143
+ },
2144
+ });
2145
+ } finally {
2146
+ process.stdout.write = stdoutWrite;
2147
+ }
2148
+ const openingEvents = openingCaptured
2149
+ .join("")
2150
+ .trim()
2151
+ .split("\n")
2152
+ .filter(Boolean)
2153
+ .map((line) => JSON.parse(line));
2154
+ const openingMessageIndex = openingEvents.findIndex((event) =>
2155
+ event.method === "item/completed"
2156
+ && event.params?.item?.type === "agentMessage"
2157
+ && event.params.item.text === "요청 내용을 확인하고 필요한 작업을 진행하겠습니다.");
2158
+ const openingToolIndex = openingEvents.findIndex((event) =>
2159
+ event.method === "item/started" && event.params?.item?.type === "dynamicToolCall");
2160
+ if (openingMessageIndex < 0 || openingToolIndex < 0 || openingMessageIndex > openingToolIndex) {
2161
+ throw new Error(`Claude opening notice order self-test failed: ${JSON.stringify(openingEvents)}`);
2162
+ }
2163
+ const languageCaptured = [];
2164
+ process.stdout.write = (chunk) => {
2165
+ languageCaptured.push(String(chunk));
2166
+ return true;
2167
+ };
2168
+ try {
2169
+ openingSession.streamBlocks.clear();
2170
+ await processStreamEvent(openingSession, {
2171
+ event: { type: "content_block_start", index: 0, content_block: { type: "text" } },
2172
+ });
2173
+ await processStreamEvent(openingSession, {
2174
+ event: { type: "content_block_delta", index: 0, delta: { text: "Now the tile view logic." } },
2175
+ });
2176
+ await processStreamEvent(openingSession, {
2177
+ event: { type: "content_block_stop", index: 0 },
2178
+ });
2179
+ } finally {
2180
+ process.stdout.write = stdoutWrite;
2181
+ }
2182
+ const languageEvents = languageCaptured
2183
+ .join("")
2184
+ .trim()
2185
+ .split("\n")
2186
+ .filter(Boolean)
2187
+ .map((line) => JSON.parse(line));
2188
+ const visibleLanguage = languageEvents
2189
+ .filter((event) => event.method === "item/agentMessage/delta")
2190
+ .map((event) => event.params?.delta || "")
2191
+ .join("");
2192
+ if (visibleLanguage !== "다음 부분을 이어서 확인하겠습니다."
2193
+ || languageEvents.some((event) => JSON.stringify(event).includes("Now the tile view logic."))) {
2194
+ throw new Error(`Claude Korean progress normalization self-test failed: ${JSON.stringify(languageEvents)}`);
2195
+ }
2196
+ const smoothText = "Claude가 👨‍👩‍👧‍👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
2197
+ const emitted = [];
2198
+ const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
2199
+ smooth.push(smoothText);
2200
+ if (emitted.length !== 0) {
2201
+ throw new Error(`Claude smooth stream did not batch its first frame: ${JSON.stringify(emitted)}`);
2052
2202
  }
2053
- const smoothText = "Claude가 👨‍👩‍👧‍👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
2054
- const emitted = [];
2055
- const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
2056
- smooth.push(smoothText);
2057
- await smooth.finish();
2203
+ await smooth.finish();
2058
2204
  if (emitted.length < 2 || emitted.join("") !== smoothText) {
2059
2205
  throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
2060
2206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
4
4
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
5
  "keywords": [
6
6
  "codex",