devez-vibe 1.6.35 → 1.6.37

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, 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") {
@@ -2685,6 +2725,98 @@ async function readableCwd(id, cwd) {
2685
2725
  return await transcriptCwd(id) || cwd;
2686
2726
  }
2687
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
+ }
2819
+
2688
2820
  async function dispatch(method, params = {}) {
2689
2821
  if (method === "model/list") return loadModelCatalog(params);
2690
2822
  if (method === "mcpServerStatus/list") return claudeMcpStatus(params);
@@ -2844,20 +2976,8 @@ async function dispatch(method, params = {}) {
2844
2976
  };
2845
2977
  }
2846
2978
  if (method === "session/list") {
2847
- const found = await listSessions({
2848
- dir: params.cwd,
2849
- limit: params.limit || 100,
2850
- offset: params.offset || 0,
2851
- includeProgrammatic: true,
2852
- });
2853
2979
  return {
2854
- data: found.map((session) => ({
2855
- id: visibleSession(session.sessionId),
2856
- name: session.customTitle || undefined,
2857
- preview: session.summary || session.firstPrompt || "Untitled Claude session",
2858
- cwd: session.cwd || params.cwd || "",
2859
- updatedAt: Math.floor((session.lastModified || 0) / 1000),
2860
- })),
2980
+ data: await claudeSessionList(params),
2861
2981
  nextCursor: null,
2862
2982
  };
2863
2983
  }
@@ -2939,10 +3059,36 @@ async function dispatch(method, params = {}) {
2939
3059
  }
2940
3060
 
2941
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
+ }
2942
3073
  if (permissionMode("dontAsk") !== "dontAsk"
2943
3074
  || permissionMode("not-a-mode", "auto") !== "auto") {
2944
3075
  throw new Error("Claude permission mode self-test failed");
2945
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
+ }
2946
3092
  const pluginCatalog = buildClaudePluginCatalog(
2947
3093
  [{ id: "cloudflare@official", enabled: true, scope: "user" }],
2948
3094
  [{
@@ -3383,6 +3529,7 @@ async function runSelfTest() {
3383
3529
  subagents: new Map(),
3384
3530
  knownSubagents: new Map(),
3385
3531
  hiddenSubagentTasks: new Set(),
3532
+ ambientSubagentTasks: new Set(),
3386
3533
  subagentPulse: null,
3387
3534
  };
3388
3535
  processSubagentSystemMessage(structuredSession, {
@@ -3504,6 +3651,42 @@ async function runSelfTest() {
3504
3651
  summary: "Hidden task finished",
3505
3652
  });
3506
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
+
3507
3690
  processSubagentSystemMessage(structuredSession, {
3508
3691
  type: "system",
3509
3692
  subtype: "task_started",
package/package.json CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "devez-vibe",
3
- "version": "1.6.35",
4
- "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
- "keywords": [
6
- "codex",
7
- "cli",
8
- "tui",
9
- "terminal",
10
- "app-server",
11
- "claude-agent-sdk"
12
- ],
13
- "homepage": "https://github.com/MrHoje/Devez-vibe#readme",
14
- "bugs": "https://github.com/MrHoje/Devez-vibe/issues",
15
- "repository": {
16
- "type": "git",
17
- "url": "git+https://github.com/MrHoje/Devez-vibe.git"
18
- },
19
- "license": "MIT",
20
- "bin": {
21
- "dvz": "bin/dvz.exe"
22
- },
23
- "files": [
24
- "bin/dvz.exe",
25
- "bridge/claude-agent-sdk-bridge.mjs",
26
- "README.md",
27
- "LICENSE"
28
- ],
29
- "os": [
30
- "win32"
31
- ],
32
- "cpu": [
33
- "x64"
34
- ],
35
- "engines": {
36
- "node": ">=18"
37
- },
38
- "dependencies": {
39
- "@anthropic-ai/claude-agent-sdk": "0.3.231"
40
- }
41
- }
1
+ {
2
+ "name": "devez-vibe",
3
+ "version": "1.6.37",
4
+ "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
+ "keywords": [
6
+ "codex",
7
+ "cli",
8
+ "tui",
9
+ "terminal",
10
+ "app-server",
11
+ "claude-agent-sdk"
12
+ ],
13
+ "homepage": "https://github.com/MrHoje/Devez-vibe#readme",
14
+ "bugs": "https://github.com/MrHoje/Devez-vibe/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/MrHoje/Devez-vibe.git"
18
+ },
19
+ "license": "MIT",
20
+ "bin": {
21
+ "dvz": "bin/dvz.exe"
22
+ },
23
+ "files": [
24
+ "bin/dvz.exe",
25
+ "bridge/claude-agent-sdk-bridge.mjs",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "os": [
30
+ "win32"
31
+ ],
32
+ "cpu": [
33
+ "x64"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "dependencies": {
39
+ "@anthropic-ai/claude-agent-sdk": "0.3.247"
40
+ }
41
+ }