devez-vibe 1.6.36 → 1.6.38

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
@@ -3,7 +3,7 @@
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { spawn } from "node:child_process";
5
5
  import { createReadStream, existsSync, readdirSync } from "node:fs";
6
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
6
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
7
7
  import { homedir } from "node:os";
8
8
  import { dirname, join } from "node:path";
9
9
  import { createInterface } from "node:readline";
@@ -29,6 +29,7 @@ const modelCatalogs = new Map();
29
29
  const CLAUDE_MODEL_ORDER = ["fable", "opus", "sonnet", "haiku"];
30
30
  const OPUS_48_MODEL = "claude-opus-4-8";
31
31
  const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
32
+ const CLAUDE_TASK_TOOLS = ["TaskCreate", "TaskGet", "TaskUpdate", "TaskList"];
32
33
  let nextHostRequest = 1;
33
34
 
34
35
  class AsyncQueue {
@@ -575,6 +576,12 @@ function makeOptions(params, sessionId, resume) {
575
576
  },
576
577
  skills: "all",
577
578
  tools: { type: "preset", preset: "claude_code" },
579
+ // Task tools left the default surface in SDK 0.3.233. Listing them here
580
+ // keeps the plan panel available without replacing the Claude Code preset.
581
+ allowedTools: [...CLAUDE_TASK_TOOLS],
582
+ // DevezVibe keeps per-task controls available through Claude's TaskStop
583
+ // tool, so a turn interrupt must not tear down independent background work.
584
+ perTaskStopAffordance: true,
578
585
  systemPrompt: {
579
586
  type: "preset",
580
587
  preset: "claude_code",
@@ -662,6 +669,13 @@ async function requestToolPermission(toolName, input, permission) {
662
669
  return { behavior: "deny", message: questionFallbackMessage(questions) };
663
670
  }
664
671
  }
672
+ // Esc on the host cancels the question outright. Claude Code stops the turn
673
+ // there, so the tool must not be handed an empty answer set: that reads as a
674
+ // successful call and the model keeps talking about a question nobody
675
+ // answered. The host sends its own interrupt right behind this reply.
676
+ if (response?.cancelled) {
677
+ return { behavior: "deny", message: "사용자가 질문을 취소하고 작업을 중단했습니다." };
678
+ }
665
679
  const answers = {};
666
680
  for (let index = 0; index < questions.length; index += 1) {
667
681
  const selected = response?.answers?.[`q${index}`]?.answers;
@@ -1062,6 +1076,7 @@ async function createSession(params, resumeId) {
1062
1076
  subagents: new Map(),
1063
1077
  knownSubagents: new Map(),
1064
1078
  hiddenSubagentTasks: new Set(),
1079
+ ambientSubagentTasks: new Set(),
1065
1080
  subagentPulse: null,
1066
1081
  lastContextUsage: null,
1067
1082
  lastContextWindow: 0,
@@ -1359,7 +1374,7 @@ function processAssistant(session, message) {
1359
1374
  function processToolUse(session, block) {
1360
1375
  const name = block.name || "Tool";
1361
1376
  const input = block.input || {};
1362
- if (name === "TaskCreate" || name === "TaskUpdate" || name === "TaskList") {
1377
+ if (CLAUDE_TASK_TOOLS.includes(name)) {
1363
1378
  updatePlanFromToolUse(session, name, block.id, input);
1364
1379
  session.tools.set(block.id, { name, input, suppressed: true });
1365
1380
  return;
@@ -1689,7 +1704,8 @@ function upsertStructuredSubagent(session, message) {
1689
1704
  const taskId = firstLine(message.task_id || "", 80);
1690
1705
  const toolUseId = firstLine(message.tool_use_id || "", 80);
1691
1706
  const subagentType = firstLine(message.subagent_type || "", 40);
1692
- if (taskId && session.hiddenSubagentTasks?.has(taskId)) return null;
1707
+ if (taskId && (session.hiddenSubagentTasks?.has(taskId)
1708
+ || session.ambientSubagentTasks?.has(taskId))) return null;
1693
1709
  const byTool = toolUseId && findSubagent(session, toolUseId);
1694
1710
  const byTask = taskId && findSubagent(session, taskId);
1695
1711
  let running = byTool || byTask;
@@ -1743,7 +1759,13 @@ function upsertStructuredSubagent(session, message) {
1743
1759
  function finishStructuredSubagent(session, message, kind, text) {
1744
1760
  const taskId = firstLine(message.task_id || "", 80);
1745
1761
  const toolUseId = firstLine(message.tool_use_id || "", 80);
1746
- const wasHidden = taskId ? session.hiddenSubagentTasks?.delete(taskId) === true : false;
1762
+ const wasTranscriptHidden = taskId
1763
+ ? session.hiddenSubagentTasks?.delete(taskId) === true
1764
+ : false;
1765
+ const wasAmbient = taskId
1766
+ ? session.ambientSubagentTasks?.delete(taskId) === true
1767
+ : false;
1768
+ const wasHidden = wasTranscriptHidden || wasAmbient;
1747
1769
  const running = (toolUseId && findSubagent(session, toolUseId))
1748
1770
  || (taskId && findSubagent(session, taskId));
1749
1771
  if (!running) return wasHidden;
@@ -1754,13 +1776,24 @@ function finishStructuredSubagent(session, message, kind, text) {
1754
1776
  }
1755
1777
 
1756
1778
  function syncBackgroundSubagents(session, tasks) {
1757
- const live = (Array.isArray(tasks) ? tasks : []).filter((task) => {
1779
+ const snapshot = Array.isArray(tasks) ? tasks : [];
1780
+ session.ambientSubagentTasks = new Set(snapshot
1781
+ .filter((task) => task?.ambient === true)
1782
+ .map((task) => firstLine(task?.task_id || "", 80))
1783
+ .filter(Boolean));
1784
+ const live = snapshot.filter((task) => {
1758
1785
  const taskId = firstLine(task?.task_id || "", 80);
1759
- return !taskId || !session.hiddenSubagentTasks?.has(taskId);
1786
+ return task?.ambient !== true
1787
+ && (!taskId || !session.hiddenSubagentTasks?.has(taskId));
1760
1788
  });
1761
1789
  const liveIds = new Set(live.map((task) => firstLine(task?.task_id || "", 80)).filter(Boolean));
1762
1790
  let changed = false;
1763
1791
  for (const [id, running] of session.subagents) {
1792
+ if (running.taskId && session.ambientSubagentTasks.has(running.taskId)) {
1793
+ session.subagents.delete(id);
1794
+ changed = true;
1795
+ continue;
1796
+ }
1764
1797
  if (!running.background || !running.taskId || liveIds.has(running.taskId)) continue;
1765
1798
  session.subagents.delete(id);
1766
1799
  changed = true;
@@ -1799,12 +1832,14 @@ function syncBackgroundSubagents(session, tasks) {
1799
1832
 
1800
1833
  function processSubagentSystemMessage(session, message) {
1801
1834
  if (message.subtype === "task_started") {
1802
- if (message.skip_transcript === true) {
1835
+ if (message.skip_transcript === true || message.ambient === true) {
1803
1836
  const taskId = firstLine(message.task_id || "", 80);
1804
1837
  const toolUseId = firstLine(message.tool_use_id || "", 80);
1805
1838
  if (taskId) {
1806
- session.hiddenSubagentTasks ||= new Set();
1807
- session.hiddenSubagentTasks.add(taskId);
1839
+ const hidden = message.ambient === true
1840
+ ? (session.ambientSubagentTasks ||= new Set())
1841
+ : (session.hiddenSubagentTasks ||= new Set());
1842
+ hidden.add(taskId);
1808
1843
  }
1809
1844
  const running = (toolUseId && findSubagent(session, toolUseId))
1810
1845
  || (taskId && findSubagent(session, taskId));
@@ -1853,6 +1888,10 @@ function processSubagentSystemMessage(session, message) {
1853
1888
  return true;
1854
1889
  }
1855
1890
  if (message.subtype === "task_notification") {
1891
+ if (message.ambient === true && message.task_id) {
1892
+ session.ambientSubagentTasks ||= new Set();
1893
+ session.ambientSubagentTasks.add(firstLine(message.task_id, 80));
1894
+ }
1856
1895
  const status = firstLine(message.status || "completed", 40);
1857
1896
  return finishStructuredSubagent(
1858
1897
  session,
@@ -1961,6 +2000,7 @@ function clearSubagents(session) {
1961
2000
  const changed = session.subagents.size > 0;
1962
2001
  session.subagents.clear();
1963
2002
  session.hiddenSubagentTasks?.clear();
2003
+ session.ambientSubagentTasks?.clear();
1964
2004
  if (session.subagentPulse) {
1965
2005
  clearInterval(session.subagentPulse);
1966
2006
  session.subagentPulse = null;
@@ -2547,7 +2587,7 @@ function historyState(messages) {
2547
2587
  tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "작업", status: "pending", turnId: turn.id });
2548
2588
  } else if (block.name === "TaskUpdate") {
2549
2589
  applyTaskUpdate(tasks, block.input || {}, turn.id, undefined, messageTime(message));
2550
- } else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
2590
+ } else if (!["TaskGet", "TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
2551
2591
  }
2552
2592
  }
2553
2593
  } else if (message.type === "user") {
@@ -2681,101 +2721,101 @@ async function transcriptCwd(id) {
2681
2721
 
2682
2722
  /** The cwd to read `id`'s transcript with: the one the transcript itself records,
2683
2723
  * falling back to the host's when no transcript is on disk. */
2684
- async function readableCwd(id, cwd) {
2685
- return await transcriptCwd(id) || cwd;
2686
- }
2687
-
2688
- function sameCwd(left, right) {
2689
- if (!left || !right) return !left && !right;
2690
- const normalize = (value) => value.replaceAll("\\", "/").replace(/\/$/, "");
2691
- const normalizedLeft = normalize(left);
2692
- const normalizedRight = normalize(right);
2693
- return process.platform === "win32"
2694
- ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
2695
- : normalizedLeft === normalizedRight;
2696
- }
2697
-
2698
- function transcriptPreview(messages) {
2699
- const prompt = historyTurns(messages)
2700
- .flatMap((turn) => turn.items || [])
2701
- .find((item) => item.type === "userMessage");
2702
- return prompt?.content?.find((item) => item.type === "text")?.text || "Untitled Claude session";
2703
- }
2704
-
2705
- async function transcriptSessions(cwd) {
2706
- if (!cwd) return [];
2707
- let projects;
2708
- try {
2709
- projects = await readdir(claudeProjectsDir(), { withFileTypes: true });
2710
- } catch {
2711
- return [];
2712
- }
2713
- const rows = new Map();
2714
- for (const project of projects) {
2715
- if (!project.isDirectory()) continue;
2716
- const dir = join(claudeProjectsDir(), project.name);
2717
- let files;
2718
- try {
2719
- files = await readdir(dir, { withFileTypes: true });
2720
- } catch {
2721
- continue;
2722
- }
2723
- for (const file of files) {
2724
- if (!file.isFile() || !/^[0-9a-f-]{36}\.jsonl$/i.test(file.name)) continue;
2725
- const path = join(dir, file.name);
2726
- let recordedCwd;
2727
- try {
2728
- recordedCwd = await readTranscriptCwd(path);
2729
- } catch {
2730
- continue;
2731
- }
2732
- if (!recordedCwd || (cwd && !sameCwd(recordedCwd, cwd))) continue;
2733
- const id = file.name.slice(0, -".jsonl".length);
2734
- try {
2735
- const [messages, metadata] = await Promise.all([
2736
- getSessionMessages(id, { dir: recordedCwd, includeSystemMessages: true }),
2737
- stat(path),
2738
- ]);
2739
- if (!messages.length) continue;
2740
- const row = {
2741
- id: visibleSession(id),
2742
- preview: transcriptPreview(messages),
2743
- cwd: recordedCwd,
2744
- updatedAt: Math.floor(metadata.mtimeMs / 1000),
2745
- };
2746
- const previous = rows.get(row.id);
2747
- if (!previous || row.updatedAt >= previous.updatedAt) rows.set(row.id, row);
2748
- } catch {
2749
- continue;
2750
- }
2751
- }
2752
- }
2753
- return [...rows.values()];
2754
- }
2755
-
2756
- async function claudeSessionList(params) {
2757
- const offset = params.offset || 0;
2758
- const limit = params.limit || 100;
2759
- const found = await listSessions({
2760
- dir: params.cwd,
2761
- limit: offset + limit,
2762
- offset: 0,
2763
- includeProgrammatic: true,
2764
- });
2765
- const rows = new Map(found.map((session) => [visibleSession(session.sessionId), {
2766
- id: visibleSession(session.sessionId),
2767
- name: session.customTitle || undefined,
2768
- preview: session.summary || session.firstPrompt || "Untitled Claude session",
2769
- cwd: session.cwd || params.cwd || "",
2770
- updatedAt: Math.floor((session.lastModified || 0) / 1000),
2771
- }]));
2772
- for (const row of await transcriptSessions(params.cwd)) {
2773
- if (!rows.has(row.id)) rows.set(row.id, row);
2774
- }
2775
- return [...rows.values()]
2776
- .sort((left, right) => right.updatedAt - left.updatedAt)
2777
- .slice(offset, offset + limit);
2778
- }
2724
+ async function readableCwd(id, cwd) {
2725
+ return await transcriptCwd(id) || cwd;
2726
+ }
2727
+
2728
+ function sameCwd(left, right) {
2729
+ if (!left || !right) return !left && !right;
2730
+ const normalize = (value) => value.replaceAll("\\", "/").replace(/\/$/, "");
2731
+ const normalizedLeft = normalize(left);
2732
+ const normalizedRight = normalize(right);
2733
+ return process.platform === "win32"
2734
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
2735
+ : normalizedLeft === normalizedRight;
2736
+ }
2737
+
2738
+ function transcriptPreview(messages) {
2739
+ const prompt = historyTurns(messages)
2740
+ .flatMap((turn) => turn.items || [])
2741
+ .find((item) => item.type === "userMessage");
2742
+ return prompt?.content?.find((item) => item.type === "text")?.text || "Untitled Claude session";
2743
+ }
2744
+
2745
+ async function transcriptSessions(cwd) {
2746
+ if (!cwd) return [];
2747
+ let projects;
2748
+ try {
2749
+ projects = await readdir(claudeProjectsDir(), { withFileTypes: true });
2750
+ } catch {
2751
+ return [];
2752
+ }
2753
+ const rows = new Map();
2754
+ for (const project of projects) {
2755
+ if (!project.isDirectory()) continue;
2756
+ const dir = join(claudeProjectsDir(), project.name);
2757
+ let files;
2758
+ try {
2759
+ files = await readdir(dir, { withFileTypes: true });
2760
+ } catch {
2761
+ continue;
2762
+ }
2763
+ for (const file of files) {
2764
+ if (!file.isFile() || !/^[0-9a-f-]{36}\.jsonl$/i.test(file.name)) continue;
2765
+ const path = join(dir, file.name);
2766
+ let recordedCwd;
2767
+ try {
2768
+ recordedCwd = await readTranscriptCwd(path);
2769
+ } catch {
2770
+ continue;
2771
+ }
2772
+ if (!recordedCwd || (cwd && !sameCwd(recordedCwd, cwd))) continue;
2773
+ const id = file.name.slice(0, -".jsonl".length);
2774
+ try {
2775
+ const [messages, metadata] = await Promise.all([
2776
+ getSessionMessages(id, { dir: recordedCwd, includeSystemMessages: true }),
2777
+ stat(path),
2778
+ ]);
2779
+ if (!messages.length) continue;
2780
+ const row = {
2781
+ id: visibleSession(id),
2782
+ preview: transcriptPreview(messages),
2783
+ cwd: recordedCwd,
2784
+ updatedAt: Math.floor(metadata.mtimeMs / 1000),
2785
+ };
2786
+ const previous = rows.get(row.id);
2787
+ if (!previous || row.updatedAt >= previous.updatedAt) rows.set(row.id, row);
2788
+ } catch {
2789
+ continue;
2790
+ }
2791
+ }
2792
+ }
2793
+ return [...rows.values()];
2794
+ }
2795
+
2796
+ async function claudeSessionList(params) {
2797
+ const offset = params.offset || 0;
2798
+ const limit = params.limit || 100;
2799
+ const found = await listSessions({
2800
+ dir: params.cwd,
2801
+ limit: offset + limit,
2802
+ offset: 0,
2803
+ includeProgrammatic: true,
2804
+ });
2805
+ const rows = new Map(found.map((session) => [visibleSession(session.sessionId), {
2806
+ id: visibleSession(session.sessionId),
2807
+ name: session.customTitle || undefined,
2808
+ preview: session.summary || session.firstPrompt || "Untitled Claude session",
2809
+ cwd: session.cwd || params.cwd || "",
2810
+ updatedAt: Math.floor((session.lastModified || 0) / 1000),
2811
+ }]));
2812
+ for (const row of await transcriptSessions(params.cwd)) {
2813
+ if (!rows.has(row.id)) rows.set(row.id, row);
2814
+ }
2815
+ return [...rows.values()]
2816
+ .sort((left, right) => right.updatedAt - left.updatedAt)
2817
+ .slice(offset, offset + limit);
2818
+ }
2779
2819
 
2780
2820
  async function dispatch(method, params = {}) {
2781
2821
  if (method === "model/list") return loadModelCatalog(params);
@@ -2934,12 +2974,12 @@ async function dispatch(method, params = {}) {
2934
2974
  usage,
2935
2975
  tokenUsage,
2936
2976
  };
2937
- }
2938
- if (method === "session/list") {
2939
- return {
2940
- data: await claudeSessionList(params),
2941
- nextCursor: null,
2942
- };
2977
+ }
2978
+ if (method === "session/list") {
2979
+ return {
2980
+ data: await claudeSessionList(params),
2981
+ nextCursor: null,
2982
+ };
2943
2983
  }
2944
2984
  if (method === "session/history") {
2945
2985
  const id = liveSessionId(params.sessionId);
@@ -3018,22 +3058,37 @@ async function dispatch(method, params = {}) {
3018
3058
  throw new Error(`지원하지 않는 Claude 브리지 메서드: ${method}`);
3019
3059
  }
3020
3060
 
3021
- async function runSelfTest() {
3022
- const equivalentCwd = process.platform === "win32"
3023
- ? sameCwd("D:\\Repo", "d:/repo/")
3024
- : sameCwd("/tmp/repo", "/tmp/repo/");
3025
- const preview = transcriptPreview([{
3026
- type: "user",
3027
- uuid: "session-list-prompt",
3028
- message: { content: "세션 목록 질문" },
3029
- }]);
3030
- if (!equivalentCwd || preview !== "세션 목록 질문") {
3031
- throw new Error(`Claude session list self-test failed: ${JSON.stringify({ equivalentCwd, preview })}`);
3032
- }
3033
- if (permissionMode("dontAsk") !== "dontAsk"
3061
+ async function runSelfTest() {
3062
+ const equivalentCwd = process.platform === "win32"
3063
+ ? sameCwd("D:\\Repo", "d:/repo/")
3064
+ : sameCwd("/tmp/repo", "/tmp/repo/");
3065
+ const preview = transcriptPreview([{
3066
+ type: "user",
3067
+ uuid: "session-list-prompt",
3068
+ message: { content: "세션 목록 질문" },
3069
+ }]);
3070
+ if (!equivalentCwd || preview !== "세션 목록 질문") {
3071
+ throw new Error(`Claude session list self-test failed: ${JSON.stringify({ equivalentCwd, preview })}`);
3072
+ }
3073
+ if (permissionMode("dontAsk") !== "dontAsk"
3034
3074
  || permissionMode("not-a-mode", "auto") !== "auto") {
3035
3075
  throw new Error("Claude permission mode self-test failed");
3036
3076
  }
3077
+ const latestOptions = makeOptions({ cwd: process.cwd() }, "00000000-0000-4000-8000-000000000000");
3078
+ if (latestOptions.perTaskStopAffordance !== true
3079
+ || CLAUDE_TASK_TOOLS.some((tool) => !latestOptions.allowedTools.includes(tool))) {
3080
+ throw new Error(`Claude latest SDK options self-test failed: ${JSON.stringify(latestOptions.allowedTools)}`);
3081
+ }
3082
+ const lookupSession = {
3083
+ turn: { id: "task-get-turn" },
3084
+ tools: new Map(),
3085
+ tasks: new Map(),
3086
+ planCreatePending: false,
3087
+ };
3088
+ processToolUse(lookupSession, { id: "task-get", name: "TaskGet", input: { taskId: "1" } });
3089
+ if (lookupSession.tools.get("task-get")?.suppressed !== true) {
3090
+ throw new Error("Claude TaskGet transcript suppression self-test failed");
3091
+ }
3037
3092
  const pluginCatalog = buildClaudePluginCatalog(
3038
3093
  [{ id: "cloudflare@official", enabled: true, scope: "user" }],
3039
3094
  [{
@@ -3474,6 +3529,7 @@ async function runSelfTest() {
3474
3529
  subagents: new Map(),
3475
3530
  knownSubagents: new Map(),
3476
3531
  hiddenSubagentTasks: new Set(),
3532
+ ambientSubagentTasks: new Set(),
3477
3533
  subagentPulse: null,
3478
3534
  };
3479
3535
  processSubagentSystemMessage(structuredSession, {
@@ -3595,6 +3651,42 @@ async function runSelfTest() {
3595
3651
  summary: "Hidden task finished",
3596
3652
  });
3597
3653
 
3654
+ processSubagentSystemMessage(structuredSession, {
3655
+ type: "system",
3656
+ subtype: "task_started",
3657
+ task_id: "agent-ambient",
3658
+ tool_use_id: "toolu_ambient",
3659
+ task_type: "agent",
3660
+ subagent_type: "Explore",
3661
+ description: "Refresh housekeeping",
3662
+ ambient: true,
3663
+ });
3664
+ processSubagentSystemMessage(structuredSession, {
3665
+ type: "system",
3666
+ subtype: "background_tasks_changed",
3667
+ tasks: [{
3668
+ task_id: "agent-ambient",
3669
+ task_type: "agent",
3670
+ description: "Refresh housekeeping",
3671
+ ambient: true,
3672
+ }],
3673
+ });
3674
+ if (structuredSession.subagents.size !== 0
3675
+ || !structuredSession.ambientSubagentTasks.has("agent-ambient")) {
3676
+ throw new Error("Claude ambient task was shown as a subagent row");
3677
+ }
3678
+ processSubagentSystemMessage(structuredSession, {
3679
+ type: "system",
3680
+ subtype: "task_notification",
3681
+ task_id: "agent-ambient",
3682
+ status: "completed",
3683
+ summary: "Ambient task finished",
3684
+ ambient: true,
3685
+ });
3686
+ if (structuredSession.ambientSubagentTasks.size !== 0) {
3687
+ throw new Error("Claude ambient task completion did not clear hidden state");
3688
+ }
3689
+
3598
3690
  processSubagentSystemMessage(structuredSession, {
3599
3691
  type: "system",
3600
3692
  subtype: "task_started",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.6.36",
3
+ "version": "1.6.38",
4
4
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
5
  "keywords": [
6
6
  "codex",
@@ -36,6 +36,6 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
- "@anthropic-ai/claude-agent-sdk": "0.3.231"
39
+ "@anthropic-ai/claude-agent-sdk": "0.3.247"
40
40
  }
41
41
  }