devez-vibe 1.2.7 → 1.2.9

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/LICENSE CHANGED
@@ -1,33 +1,33 @@
1
- DevezCode source code license
2
-
3
- The MIT License below applies only to the DevezCode source code and to files
4
- that are not listed in the exception below.
5
-
6
- Exceptions:
7
-
8
- * `Resources/Images/FileTypes/` and `Resources/Licenses/` are Microsoft Visual
9
- Studio Image Library materials, subject to the included Microsoft terms.
10
- * `Resources/Images/ShellPresets/` contains third-party brand assets, subject
11
- to their respective owners' terms.
12
-
13
- MIT License
14
-
15
- Copyright (c) 2026 DevezCode contributors
16
-
17
- Permission is hereby granted, free of charge, to any person obtaining a copy
18
- of this software and associated documentation files (the "Software"), to deal
19
- in the Software without restriction, including without limitation the rights
20
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
- copies of the Software, and to permit persons to whom the Software is
22
- furnished to do so, subject to the following conditions:
23
-
24
- The above copyright notice and this permission notice shall be included in all
25
- copies or substantial portions of the Software.
26
-
27
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
- SOFTWARE.
1
+ DevezCode source code license
2
+
3
+ The MIT License below applies only to the DevezCode source code and to files
4
+ that are not listed in the exception below.
5
+
6
+ Exceptions:
7
+
8
+ * `Resources/Images/FileTypes/` and `Resources/Licenses/` are Microsoft Visual
9
+ Studio Image Library materials, subject to the included Microsoft terms.
10
+ * `Resources/Images/ShellPresets/` contains third-party brand assets, subject
11
+ to their respective owners' terms.
12
+
13
+ MIT License
14
+
15
+ Copyright (c) 2026 DevezCode contributors
16
+
17
+ Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ of this software and associated documentation files (the "Software"), to deal
19
+ in the Software without restriction, including without limitation the rights
20
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ copies of the Software, and to permit persons to whom the Software is
22
+ furnished to do so, subject to the following conditions:
23
+
24
+ The above copyright notice and this permission notice shall be included in all
25
+ copies or substantial portions of the Software.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ SOFTWARE.
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,
@@ -501,6 +504,45 @@ function emitDelta(session, method, itemId, delta) {
501
504
  notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
502
505
  }
503
506
 
507
+ function isKoreanPrompt(input) {
508
+ const prompt = (Array.isArray(input) ? input : [])
509
+ .filter((item) => item?.type === "text")
510
+ .map((item) => String(item.text || ""))
511
+ .join("\n");
512
+ return /[\uac00-\ud7a3]/.test(prompt);
513
+ }
514
+
515
+ function openingNotice(input) {
516
+ return isKoreanPrompt(input)
517
+ ? "요청 내용을 확인하고 필요한 작업을 진행하겠습니다."
518
+ : "I’ll review the request and proceed with the necessary work.";
519
+ }
520
+
521
+ function normalizeProgressText(turn, text) {
522
+ const value = String(text || "");
523
+ const trimmed = value.trim();
524
+ if (turn?.koreanRequest
525
+ && trimmed.length <= 160
526
+ && !/[\uac00-\ud7a3]/.test(trimmed)
527
+ && /^Now\b[^\r\n]*[.!?]?$/i.test(trimmed)) {
528
+ return "다음 부분을 이어서 확인하겠습니다.";
529
+ }
530
+ return value;
531
+ }
532
+
533
+ // Claude can answer with a tool_use as its first and only content block even
534
+ // when the prompt asks for an opening update. Keep the visible contract stable
535
+ // without duplicating a real model-written update.
536
+ function emitOpeningNotice(session) {
537
+ if (!session.turn || session.turn.openingNoticeEmitted) return;
538
+ const text = session.turn.openingNotice;
539
+ const id = nextItemId(session, "opening");
540
+ const item = { id, type: "agentMessage", text, provider: "Claude" };
541
+ emitItem(session, "started", item);
542
+ emitItem(session, "completed", item);
543
+ session.turn.openingNoticeEmitted = true;
544
+ }
545
+
504
546
  // Windows rounds larger timer delays up to the next scheduler slice. Ten
505
547
  // milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
506
548
  const SMOOTH_TEXT_INTERVAL_MS = 10;
@@ -642,7 +684,14 @@ async function processStreamEvent(session, message) {
642
684
  const smooth = block.type === "text"
643
685
  ? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
644
686
  : null;
645
- session.streamBlocks.set(event.index, { id, type: block.type, text: "", smooth });
687
+ session.streamBlocks.set(event.index, {
688
+ id,
689
+ type: block.type,
690
+ text: "",
691
+ smooth,
692
+ languagePending: block.type === "text" && session.turn.koreanRequest ? "" : null,
693
+ holdEnglishProgress: false,
694
+ });
646
695
  emitItem(session, "started", item);
647
696
  return;
648
697
  }
@@ -652,13 +701,36 @@ async function processStreamEvent(session, message) {
652
701
  const delta = event.delta?.text || event.delta?.thinking || "";
653
702
  if (!delta) return;
654
703
  current.text += delta;
655
- if (current.smooth) current.smooth.push(delta);
656
- else emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
704
+ if (current.type === "text") session.turn.sawVisibleText = true;
705
+ if (!current.smooth) {
706
+ emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
707
+ return;
708
+ }
709
+ if (current.languagePending != null) {
710
+ current.languagePending += delta;
711
+ const probe = current.languagePending.trimStart();
712
+ const lower = probe.toLowerCase();
713
+ if (!current.holdEnglishProgress && "now".startsWith(lower)) return;
714
+ if (/^now(?:\s|$)/i.test(probe)) {
715
+ current.holdEnglishProgress = true;
716
+ return;
717
+ }
718
+ current.smooth.push(current.languagePending);
719
+ current.languagePending = null;
720
+ return;
721
+ }
722
+ current.smooth.push(delta);
657
723
  return;
658
724
  }
659
725
  if (event.type === "content_block_stop") {
660
726
  const current = session.streamBlocks.get(event.index);
661
727
  if (!current) return;
728
+ if (current.languagePending != null) {
729
+ const visible = normalizeProgressText(session.turn, current.languagePending);
730
+ current.text = visible;
731
+ current.smooth?.push(visible);
732
+ current.languagePending = null;
733
+ }
662
734
  await current.smooth?.finish();
663
735
  const item = current.type === "text"
664
736
  ? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
@@ -685,20 +757,27 @@ function processAssistant(session, message) {
685
757
  session.model,
686
758
  );
687
759
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
688
- for (const block of content) {
689
- if (block.type === "tool_use") processToolUse(session, block);
690
- }
760
+ const hasToolUse = content.some((block) => block.type === "tool_use");
761
+ const hasVisibleText = session.turn.sawVisibleText || content.some(
762
+ (block) => block.type === "text" && String(block.text || "").trim(),
763
+ );
764
+ // Without partial SDK events, replay completed text before tool items so the
765
+ // visible order still matches the assistant content order.
691
766
  if (!session.streamBlocks.size && !session.turn.sawStreamText) {
692
767
  for (const block of content) {
693
768
  if (block.type !== "text" && block.type !== "thinking") continue;
694
769
  const id = nextItemId(session, block.type);
695
770
  const item = block.type === "text"
696
- ? { id, type: "agentMessage", text: block.text || "", provider: "Claude" }
771
+ ? { id, type: "agentMessage", text: normalizeProgressText(session.turn, block.text), provider: "Claude" }
697
772
  : { id, type: "reasoning", summary: [block.thinking || ""] };
698
773
  emitItem(session, "started", item);
699
774
  emitItem(session, "completed", item);
700
775
  }
701
776
  }
777
+ if (hasToolUse && !hasVisibleText) emitOpeningNotice(session);
778
+ for (const block of content) {
779
+ if (block.type === "tool_use") processToolUse(session, block);
780
+ }
702
781
  }
703
782
 
704
783
  function processToolUse(session, block) {
@@ -762,51 +841,51 @@ function prepareTaskPlanForCreate(tasks, subject) {
762
841
  if (numberedTaskIndex(subject) === 1) tasks.clear();
763
842
  }
764
843
 
765
- function applyTaskUpdate(tasks, input, turnId, onIntermediate) {
766
- const task = tasks.get(String(input.taskId));
767
- if (!task) return false;
768
- if (input.subject) task.subject = input.subject;
769
- const status = input.status;
770
- if (status === "in_progress" || status === "completed") {
771
- const entries = [...tasks.values()];
772
- const targetIndex = entries.indexOf(task);
773
-
774
- // Claude occasionally closes a later pending task at the end of a turn
775
- // without ever starting it. Keep the visible plan truthful and sequential:
776
- // every skipped predecessor and the target itself pass through in_progress.
777
- for (let index = 0; index < targetIndex; index++) {
778
- const previous = entries[index];
779
- if (previous.status === "completed") continue;
780
- if (previous.status !== "in_progress") {
781
- previous.status = "in_progress";
782
- previous.turnId = turnId;
783
- onIntermediate?.();
784
- }
785
- previous.status = "completed";
786
- previous.turnId = turnId;
787
- onIntermediate?.();
788
- }
789
-
790
- for (let index = 0; index < entries.length; index++) {
791
- const other = entries[index];
792
- if (other === task || other.status !== "in_progress") continue;
793
- other.status = index < targetIndex ? "completed" : "pending";
794
- other.turnId = turnId;
795
- onIntermediate?.();
796
- }
797
-
798
- if (status === "completed" && task.status !== "in_progress" && task.status !== "completed") {
799
- task.status = "in_progress";
800
- task.turnId = turnId;
801
- onIntermediate?.();
802
- }
803
- task.status = status;
804
- } else if (status) {
805
- task.status = status;
806
- }
807
- task.turnId = turnId;
808
- return true;
809
- }
844
+ function applyTaskUpdate(tasks, input, turnId, onIntermediate) {
845
+ const task = tasks.get(String(input.taskId));
846
+ if (!task) return false;
847
+ if (input.subject) task.subject = input.subject;
848
+ const status = input.status;
849
+ if (status === "in_progress" || status === "completed") {
850
+ const entries = [...tasks.values()];
851
+ const targetIndex = entries.indexOf(task);
852
+
853
+ // Claude occasionally closes a later pending task at the end of a turn
854
+ // without ever starting it. Keep the visible plan truthful and sequential:
855
+ // every skipped predecessor and the target itself pass through in_progress.
856
+ for (let index = 0; index < targetIndex; index++) {
857
+ const previous = entries[index];
858
+ if (previous.status === "completed") continue;
859
+ if (previous.status !== "in_progress") {
860
+ previous.status = "in_progress";
861
+ previous.turnId = turnId;
862
+ onIntermediate?.();
863
+ }
864
+ previous.status = "completed";
865
+ previous.turnId = turnId;
866
+ onIntermediate?.();
867
+ }
868
+
869
+ for (let index = 0; index < entries.length; index++) {
870
+ const other = entries[index];
871
+ if (other === task || other.status !== "in_progress") continue;
872
+ other.status = index < targetIndex ? "completed" : "pending";
873
+ other.turnId = turnId;
874
+ onIntermediate?.();
875
+ }
876
+
877
+ if (status === "completed" && task.status !== "in_progress" && task.status !== "completed") {
878
+ task.status = "in_progress";
879
+ task.turnId = turnId;
880
+ onIntermediate?.();
881
+ }
882
+ task.status = status;
883
+ } else if (status) {
884
+ task.status = status;
885
+ }
886
+ task.turnId = turnId;
887
+ return true;
888
+ }
810
889
 
811
890
  // TaskList에는 예전 계획까지 함께 들어올 수 있다. 마지막으로 번호가 1부터
812
891
  // 시작한 묶음만 현재 계획으로 삼되, 번호가 없는 목록은 손실 없이 그대로 둔다.
@@ -829,9 +908,9 @@ function updatePlanFromToolUse(session, name, toolUseId, input) {
829
908
  status: "pending",
830
909
  turnId,
831
910
  });
832
- } else if (name === "TaskUpdate") {
833
- applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
834
- }
911
+ } else if (name === "TaskUpdate") {
912
+ applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
913
+ }
835
914
  emitPlan(session);
836
915
  }
837
916
 
@@ -1348,10 +1427,11 @@ async function runPrompt(session, params) {
1348
1427
  }
1349
1428
  session.effort = effort;
1350
1429
  await applyPermissionMode(session, params.permissionMode);
1351
- const turnId = beginTurn(session);
1430
+ const content = await inputContent(params.input, params.handoffContext);
1431
+ const turnId = beginTurn(session, params.input);
1352
1432
  session.queue.push({
1353
1433
  type: "user",
1354
- message: { role: "user", content: await inputContent(params.input, params.handoffContext) },
1434
+ message: { role: "user", content },
1355
1435
  parent_tool_use_id: null,
1356
1436
  session_id: id,
1357
1437
  origin: { kind: "human" },
@@ -1361,9 +1441,16 @@ async function runPrompt(session, params) {
1361
1441
 
1362
1442
  // A background task notification is an internal user message that starts its
1363
1443
  // own Claude response even though the host did not submit a new prompt.
1364
- function beginTurn(session) {
1444
+ function beginTurn(session, input = []) {
1365
1445
  const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
1366
- session.turn = { id: turnId, sawStreamText: false };
1446
+ session.turn = {
1447
+ id: turnId,
1448
+ sawStreamText: false,
1449
+ sawVisibleText: false,
1450
+ koreanRequest: isKoreanPrompt(input),
1451
+ openingNotice: openingNotice(input),
1452
+ openingNoticeEmitted: false,
1453
+ };
1367
1454
  session.lastContextUsage = null;
1368
1455
  notify("turn/started", { threadId: session.id, turn: { id: turnId } });
1369
1456
  return turnId;
@@ -1485,6 +1572,75 @@ function historyTurns(messages) {
1485
1572
  return historyState(messages).turns;
1486
1573
  }
1487
1574
 
1575
+ // A transcript lives in a folder encoded from the cwd string, so two spellings of
1576
+ // the same path (Windows differs only in case) resolve to different folders and a
1577
+ // session recorded under one spelling is invisible to the other. Remember the
1578
+ // spelling the transcript was written with, keyed by session id.
1579
+ const transcriptCwds = new Map();
1580
+
1581
+ function claudeProjectsDir() {
1582
+ return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "projects");
1583
+ }
1584
+
1585
+ /** Read the cwd a transcript records. Only the head is scanned: the opening
1586
+ * records carry no cwd, but a real turn shows up long before the limit. */
1587
+ async function readTranscriptCwd(path, limit = 200) {
1588
+ const stream = createReadStream(path, { encoding: "utf8" });
1589
+ try {
1590
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
1591
+ try {
1592
+ let seen = 0;
1593
+ for await (const line of reader) {
1594
+ if (++seen > limit) break;
1595
+ let cwd;
1596
+ try {
1597
+ cwd = JSON.parse(line)?.cwd;
1598
+ } catch {
1599
+ continue;
1600
+ }
1601
+ if (typeof cwd === "string" && cwd) return cwd;
1602
+ }
1603
+ return null;
1604
+ } finally {
1605
+ reader.close();
1606
+ }
1607
+ } finally {
1608
+ stream.destroy();
1609
+ }
1610
+ }
1611
+
1612
+ /** Locate the transcript of `id` under any project folder and report the cwd it
1613
+ * records, which is the spelling the SDK needs to find it again. */
1614
+ async function transcriptCwd(id) {
1615
+ if (transcriptCwds.has(id)) return transcriptCwds.get(id);
1616
+ let entries;
1617
+ try {
1618
+ entries = await readdir(claudeProjectsDir(), { withFileTypes: true });
1619
+ } catch {
1620
+ return null;
1621
+ }
1622
+ for (const entry of entries) {
1623
+ if (!entry.isDirectory()) continue;
1624
+ let cwd;
1625
+ try {
1626
+ cwd = await readTranscriptCwd(join(claudeProjectsDir(), entry.name, `${id}.jsonl`));
1627
+ } catch {
1628
+ continue;
1629
+ }
1630
+ if (cwd) {
1631
+ transcriptCwds.set(id, cwd);
1632
+ return cwd;
1633
+ }
1634
+ }
1635
+ return null;
1636
+ }
1637
+
1638
+ /** The cwd to read `id`'s transcript with: the one the transcript itself records,
1639
+ * falling back to the host's when no transcript is on disk. */
1640
+ async function readableCwd(id, cwd) {
1641
+ return await transcriptCwd(id) || cwd;
1642
+ }
1643
+
1488
1644
  async function dispatch(method, params = {}) {
1489
1645
  if (method === "model/list") return loadModelCatalog(params);
1490
1646
  if (method === "session/permissionMode") {
@@ -1524,15 +1680,19 @@ async function dispatch(method, params = {}) {
1524
1680
  tokenUsage: historyTokenUsage(messages, existing.models, existing.model),
1525
1681
  };
1526
1682
  }
1527
- const info = await getSessionInfo(id, { dir: params.cwd });
1528
- if (!info) throw new Error(`Claude 세션을 찾을 없습니다: ${id}`);
1529
- const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
1683
+ const dir = await readableCwd(id, params.cwd);
1684
+ // The transcript itself decides whether there is anything to resume:
1685
+ // getSessionInfo only sees sessions the CLI indexed, and a bridge-run session
1686
+ // whose transcript is intact can be missing from that index.
1687
+ const messages = await getSessionMessages(id, { dir, includeSystemMessages: true });
1688
+ if (!messages.length) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
1689
+ const info = await getSessionInfo(id, { dir });
1530
1690
  const lastModel = [...messages].reverse().find((message) => message.type === "assistant")?.message?.model;
1531
1691
  // The transcript's own model outranks the host's fallback, which is only what
1532
1692
  // a new session would have opened on.
1533
1693
  const { session, account, usage } = await createSession({
1534
1694
  ...params,
1535
- cwd: info.cwd || params.cwd,
1695
+ cwd: info?.cwd || dir,
1536
1696
  model: params.model || lastModel || params.fallbackModel,
1537
1697
  effort: params.effort || params.fallbackEffort,
1538
1698
  }, id);
@@ -1573,7 +1733,7 @@ async function dispatch(method, params = {}) {
1573
1733
  }
1574
1734
  if (method === "session/history") {
1575
1735
  const id = liveSessionId(params.sessionId);
1576
- const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
1736
+ const messages = await getSessionMessages(id, { dir: await readableCwd(id, params.cwd), includeSystemMessages: true });
1577
1737
  return { data: historyTurns(messages), nextCursor: null };
1578
1738
  }
1579
1739
  if (method === "session/prompt") return startPrompt(params);
@@ -1599,7 +1759,7 @@ async function dispatch(method, params = {}) {
1599
1759
  }
1600
1760
  if (method === "session/fork") {
1601
1761
  const source = liveSessionId(params.sessionId);
1602
- const forked = await forkSession(source, { dir: params.cwd });
1762
+ const forked = await forkSession(source, { dir: await readableCwd(source, params.cwd) });
1603
1763
  const id = forked.sessionId || forked;
1604
1764
  const { session, account, usage } = await createSession(params, id);
1605
1765
  return {
@@ -1717,33 +1877,33 @@ async function runSelfTest() {
1717
1877
  }
1718
1878
  applyTaskUpdate(restoredTasks, { taskId: "26", status: "completed" }, "resumed-turn");
1719
1879
  applyTaskUpdate(restoredTasks, { taskId: "27", status: "in_progress" }, "resumed-turn");
1720
- if (restoredTasks.size !== 6
1721
- || restoredTasks.get("25")?.status !== "completed"
1722
- || restoredTasks.get("26")?.status !== "completed"
1723
- || restoredTasks.get("27")?.status !== "in_progress"
1724
- || [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
1725
- throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
1726
- }
1727
- const skippedTasks = new Map([
1728
- ["1", { id: "1", subject: "1. 조사", status: "pending" }],
1729
- ["2", { id: "2", subject: "2. 분석", status: "pending" }],
1730
- ["3", { id: "3", subject: "3. 검증", status: "pending" }],
1731
- ]);
1732
- const transitions = [];
1733
- const snapshot = () => transitions.push([...skippedTasks.values()].map((task) => task.status).join(","));
1734
- applyTaskUpdate(skippedTasks, { taskId: "3", status: "completed" }, "turn", snapshot);
1735
- snapshot();
1736
- if (transitions.join("|") !== [
1737
- "in_progress,pending,pending",
1738
- "completed,pending,pending",
1739
- "completed,in_progress,pending",
1740
- "completed,completed,pending",
1741
- "completed,completed,in_progress",
1742
- "completed,completed,completed",
1743
- ].join("|")) {
1744
- throw new Error(`Claude skipped task transition self-test failed: ${transitions.join("|")}`);
1745
- }
1746
- const mixedPlans = new Map([
1880
+ if (restoredTasks.size !== 6
1881
+ || restoredTasks.get("25")?.status !== "completed"
1882
+ || restoredTasks.get("26")?.status !== "completed"
1883
+ || restoredTasks.get("27")?.status !== "in_progress"
1884
+ || [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
1885
+ throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
1886
+ }
1887
+ const skippedTasks = new Map([
1888
+ ["1", { id: "1", subject: "1. 조사", status: "pending" }],
1889
+ ["2", { id: "2", subject: "2. 분석", status: "pending" }],
1890
+ ["3", { id: "3", subject: "3. 검증", status: "pending" }],
1891
+ ]);
1892
+ const transitions = [];
1893
+ const snapshot = () => transitions.push([...skippedTasks.values()].map((task) => task.status).join(","));
1894
+ applyTaskUpdate(skippedTasks, { taskId: "3", status: "completed" }, "turn", snapshot);
1895
+ snapshot();
1896
+ if (transitions.join("|") !== [
1897
+ "in_progress,pending,pending",
1898
+ "completed,pending,pending",
1899
+ "completed,in_progress,pending",
1900
+ "completed,completed,pending",
1901
+ "completed,completed,in_progress",
1902
+ "completed,completed,completed",
1903
+ ].join("|")) {
1904
+ throw new Error(`Claude skipped task transition self-test failed: ${transitions.join("|")}`);
1905
+ }
1906
+ const mixedPlans = new Map([
1747
1907
  ["old-1", { subject: "1. 이전 작업", status: "completed" }],
1748
1908
  ["old-2", { subject: "2. 이전 검증", status: "completed" }],
1749
1909
  ...[...restoredTasks],
@@ -1888,6 +2048,84 @@ async function runSelfTest() {
1888
2048
  && event.params?.line?.kind === "error")) {
1889
2049
  throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
1890
2050
  }
2051
+ const openingSession = {
2052
+ id: "opening-self-test",
2053
+ model: "claude:default",
2054
+ models: [],
2055
+ turn: null,
2056
+ turnSequence: 1,
2057
+ itemSequence: 1,
2058
+ streamBlocks: new Map(),
2059
+ tools: new Map(),
2060
+ tasks: new Map(),
2061
+ subagents: new Map(),
2062
+ knownSubagents: new Map(),
2063
+ lastContextUsage: null,
2064
+ lastContextWindow: 0,
2065
+ };
2066
+ const openingCaptured = [];
2067
+ process.stdout.write = (chunk) => {
2068
+ openingCaptured.push(String(chunk));
2069
+ return true;
2070
+ };
2071
+ try {
2072
+ beginTurn(openingSession, [{ type: "text", text: "provider 메뉴를 수정해" }]);
2073
+ processAssistant(openingSession, {
2074
+ message: {
2075
+ content: [{ type: "tool_use", id: "read-1", name: "Read", input: { file_path: "src/main.rs" } }],
2076
+ },
2077
+ });
2078
+ } finally {
2079
+ process.stdout.write = stdoutWrite;
2080
+ }
2081
+ const openingEvents = openingCaptured
2082
+ .join("")
2083
+ .trim()
2084
+ .split("\n")
2085
+ .filter(Boolean)
2086
+ .map((line) => JSON.parse(line));
2087
+ const openingMessageIndex = openingEvents.findIndex((event) =>
2088
+ event.method === "item/completed"
2089
+ && event.params?.item?.type === "agentMessage"
2090
+ && event.params.item.text === "요청 내용을 확인하고 필요한 작업을 진행하겠습니다.");
2091
+ const openingToolIndex = openingEvents.findIndex((event) =>
2092
+ event.method === "item/started" && event.params?.item?.type === "dynamicToolCall");
2093
+ if (openingMessageIndex < 0 || openingToolIndex < 0 || openingMessageIndex > openingToolIndex) {
2094
+ throw new Error(`Claude opening notice order self-test failed: ${JSON.stringify(openingEvents)}`);
2095
+ }
2096
+ const languageCaptured = [];
2097
+ process.stdout.write = (chunk) => {
2098
+ languageCaptured.push(String(chunk));
2099
+ return true;
2100
+ };
2101
+ try {
2102
+ openingSession.streamBlocks.clear();
2103
+ await processStreamEvent(openingSession, {
2104
+ event: { type: "content_block_start", index: 0, content_block: { type: "text" } },
2105
+ });
2106
+ await processStreamEvent(openingSession, {
2107
+ event: { type: "content_block_delta", index: 0, delta: { text: "Now the tile view logic." } },
2108
+ });
2109
+ await processStreamEvent(openingSession, {
2110
+ event: { type: "content_block_stop", index: 0 },
2111
+ });
2112
+ } finally {
2113
+ process.stdout.write = stdoutWrite;
2114
+ }
2115
+ const languageEvents = languageCaptured
2116
+ .join("")
2117
+ .trim()
2118
+ .split("\n")
2119
+ .filter(Boolean)
2120
+ .map((line) => JSON.parse(line));
2121
+ const visibleLanguage = languageEvents
2122
+ .filter((event) => event.method === "item/agentMessage/delta")
2123
+ .map((event) => event.params?.delta || "")
2124
+ .join("");
2125
+ if (visibleLanguage !== "다음 부분을 이어서 확인하겠습니다."
2126
+ || languageEvents.some((event) => JSON.stringify(event).includes("Now the tile view logic."))) {
2127
+ throw new Error(`Claude Korean progress normalization self-test failed: ${JSON.stringify(languageEvents)}`);
2128
+ }
1891
2129
  const smoothText = "Claude가 👨‍👩‍👧‍👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
1892
2130
  const emitted = [];
1893
2131
  const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
5
  "keywords": [
6
6
  "codex",