devez-vibe 0.1.48 → 1.2.0

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
@@ -14,6 +14,10 @@ import {
14
14
 
15
15
  const VERSION = process.env.DEVEZ_VIBE_VERSION || "dev";
16
16
  const sessions = new Map();
17
+ // The id this bridge proposes is not always the id the CLI persists the
18
+ // transcript under, so a session can be renamed mid-flight. Old id → live id,
19
+ // which keeps ids the host already handed out (or wrote to disk) resolvable.
20
+ const sessionAliases = new Map();
17
21
  const pendingHostRequests = new Map();
18
22
  const modelCatalogs = new Map();
19
23
  let nextHostRequest = 1;
@@ -215,6 +219,44 @@ function rawSession(id) {
215
219
  return id.startsWith("claude:") ? id.slice("claude:".length) : id;
216
220
  }
217
221
 
222
+ /** Follows the rename chain from an id the host still remembers to the live one. */
223
+ function liveSessionId(id) {
224
+ let current = rawSession(String(id ?? ""));
225
+ const seen = new Set();
226
+ while (sessionAliases.has(current) && !seen.has(current)) {
227
+ seen.add(current);
228
+ current = sessionAliases.get(current);
229
+ }
230
+ return current;
231
+ }
232
+
233
+ function lookupSession(id) {
234
+ return sessions.get(liveSessionId(id));
235
+ }
236
+
237
+ /**
238
+ * Binds the session to the id the CLI actually persists under. `options.sessionId`
239
+ * is a request, not a guarantee: a session that gets rotated (a pre-warmed process
240
+ * the first turn does not reuse, a resume the CLI declines) writes its transcript
241
+ * under a different uuid, and everything downstream — `session/resume`,
242
+ * `session/history`, DevezCode's `-r` on the next launch — keys off the persisted
243
+ * id. Adopting it here and telling the host is what keeps a Claude-backed session
244
+ * resumable at all.
245
+ */
246
+ function adoptSessionId(session, incoming) {
247
+ const real = rawSession(String(incoming ?? ""));
248
+ if (!real || real === session.id) return;
249
+ const previous = session.id;
250
+ sessions.delete(previous);
251
+ sessionAliases.set(previous, real);
252
+ session.id = real;
253
+ sessions.set(real, session);
254
+ notify("thread/rebound", {
255
+ threadId: visibleSession(previous),
256
+ newThreadId: visibleSession(real),
257
+ });
258
+ }
259
+
218
260
  function makeOptions(params, sessionId, resume) {
219
261
  const options = {
220
262
  cwd: params.cwd || process.cwd(),
@@ -341,6 +383,7 @@ async function createSession(params, resumeId) {
341
383
  streamBlocks: new Map(),
342
384
  tools: new Map(),
343
385
  tasks: new Map(),
386
+ subagents: new Map(),
344
387
  lastContextUsage: null,
345
388
  lastContextWindow: 0,
346
389
  };
@@ -482,7 +525,11 @@ function processStreamEvent(session, message) {
482
525
  }
483
526
 
484
527
  function processAssistant(session, message) {
485
- if (!session.turn || message.parent_tool_use_id) return;
528
+ if (!session.turn) return;
529
+ if (message.parent_tool_use_id) {
530
+ recordSubagentMessage(session, message);
531
+ return;
532
+ }
486
533
  session.lastContextUsage = tokenBreakdown(message.message?.usage);
487
534
  const capabilities = modelCapabilities(
488
535
  session.models,
@@ -523,6 +570,7 @@ function processToolUse(session, block) {
523
570
  const item = toolItem(session, block.id, name, input);
524
571
  session.tools.set(block.id, { name, input, item });
525
572
  emitItem(session, "started", item);
573
+ if (SUBAGENT_TOOLS.includes(name)) startSubagent(session, block);
526
574
  }
527
575
 
528
576
  function toolItem(session, id, name, input) {
@@ -643,10 +691,123 @@ function numberedTaskSubject(subject, index) {
643
691
  return /^\d+\.\s/.test(text) ? text : `${index + 1}. ${text}`;
644
692
  }
645
693
 
694
+ // 서브에이전트는 자기 메시지를 부모 Task 툴콜의 `parent_tool_use_id`와 함께 흘려보낸다.
695
+ // 그 ID로 묶어 두면 지금 어떤 에이전트가 무슨 도구를 돌리는지 그대로 복원할 수 있다.
696
+ const SUBAGENT_TOOLS = ["Agent", "Task"];
697
+
698
+ function startSubagent(session, block) {
699
+ const input = block.input || {};
700
+ session.subagents.set(block.id, {
701
+ id: block.id,
702
+ name: firstLine(input.subagent_type || input.agentType || "agent", 40),
703
+ description: firstLine(input.description || input.prompt || "", 120),
704
+ tool: "",
705
+ startedAt: Date.now(),
706
+ });
707
+ emitSubagents(session);
708
+ }
709
+
710
+ // 서브에이전트가 실제로 무엇을 했는지는 자식 메시지에만 남는다. 열람용 기록은 여기서
711
+ // 한 줄씩 흘려보내고, 목록 행에 쓸 현재 도구만 따로 갱신한다.
712
+ function recordSubagentMessage(session, message) {
713
+ const running = session.subagents.get(message.parent_tool_use_id);
714
+ if (!running) return;
715
+ const content = Array.isArray(message.message?.content) ? message.message.content : [];
716
+ let toolChanged = false;
717
+ for (const block of content) {
718
+ if (block.type === "text") {
719
+ const text = String(block.text || "").trim();
720
+ if (text) emitSubagentLine(session, running.id, { kind: "text", text });
721
+ } else if (block.type === "tool_use") {
722
+ running.tool = subagentToolLabel(block);
723
+ toolChanged = true;
724
+ emitSubagentLine(session, running.id, {
725
+ kind: "tool",
726
+ text: running.tool,
727
+ toolUseId: block.id,
728
+ });
729
+ }
730
+ }
731
+ if (toolChanged) emitSubagents(session);
732
+ }
733
+
734
+ function recordSubagentResult(session, message) {
735
+ const running = session.subagents.get(message.parent_tool_use_id);
736
+ if (!running) return;
737
+ const content = Array.isArray(message.message?.content) ? message.message.content : [];
738
+ for (const block of content) {
739
+ if (block.type !== "tool_result") continue;
740
+ emitSubagentLine(session, running.id, {
741
+ kind: block.is_error ? "error" : "result",
742
+ text: firstLine(toolOutput(block.content, message.tool_use_result), 200),
743
+ toolUseId: block.tool_use_id,
744
+ });
745
+ }
746
+ }
747
+
748
+ function emitSubagentLine(session, parentToolUseId, line) {
749
+ notify("turn/subagent/line", {
750
+ threadId: session.id,
751
+ turnId: session.turn?.id,
752
+ parentToolUseId,
753
+ line,
754
+ });
755
+ }
756
+
757
+ function subagentToolLabel(block) {
758
+ const name = block.name || "Tool";
759
+ const input = block.input || {};
760
+ const detail = input.command
761
+ ?? input.pattern
762
+ ?? input.file_path
763
+ ?? input.description
764
+ ?? input.query
765
+ ?? input.url
766
+ ?? "";
767
+ const text = firstLine(detail, 60);
768
+ return text ? `${name}(${text})` : name;
769
+ }
770
+
771
+ function finishSubagent(session, toolUseId) {
772
+ if (!session.subagents.delete(toolUseId)) return;
773
+ emitSubagents(session);
774
+ }
775
+
776
+ function clearSubagents(session) {
777
+ if (!session.subagents.size) return;
778
+ session.subagents.clear();
779
+ emitSubagents(session);
780
+ }
781
+
782
+ function firstLine(value, limit) {
783
+ return String(value ?? "").split("\n")[0].trim().slice(0, limit);
784
+ }
785
+
786
+ function emitSubagents(session) {
787
+ notify("turn/subagents/updated", {
788
+ threadId: session.id,
789
+ turnId: session.turn?.id,
790
+ subagents: [...session.subagents.values()].map((agent) => ({
791
+ id: agent.id,
792
+ name: agent.name,
793
+ description: agent.description,
794
+ tool: agent.tool,
795
+ elapsedMs: Date.now() - agent.startedAt,
796
+ })),
797
+ });
798
+ }
799
+
646
800
  function processUser(session, message) {
801
+ // 자식 tool_result의 tool_use_id는 부모 세션의 것과 다른 공간이므로, 부모 흐름에
802
+ // 섞이기 전에 서브에이전트 기록으로 보낸다.
803
+ if (message.parent_tool_use_id) {
804
+ recordSubagentResult(session, message);
805
+ return;
806
+ }
647
807
  const content = Array.isArray(message.message?.content) ? message.message.content : [];
648
808
  for (const block of content) {
649
809
  if (block.type !== "tool_result") continue;
810
+ finishSubagent(session, block.tool_use_id);
650
811
  const pending = session.tools.get(block.tool_use_id);
651
812
  if (!pending) continue;
652
813
  pending.toolUseId = block.tool_use_id;
@@ -735,6 +896,7 @@ async function runPendingPrompt(session) {
735
896
 
736
897
  function finishTurn(session, error, durationMs) {
737
898
  if (!session.turn) return;
899
+ clearSubagents(session);
738
900
  const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
739
901
  if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
740
902
  if (durationMs != null) turn.durationMs = durationMs;
@@ -745,6 +907,7 @@ function finishTurn(session, error, durationMs) {
745
907
 
746
908
  async function consume(session) {
747
909
  for await (const message of session.query) {
910
+ adoptSessionId(session, message.session_id);
748
911
  if (message.type === "stream_event") {
749
912
  if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
750
913
  if (session.turn) session.turn.sawStreamText = true;
@@ -807,7 +970,7 @@ async function inputContent(input, handoffContext) {
807
970
  }
808
971
 
809
972
  async function startPrompt(params) {
810
- const id = rawSession(params.sessionId);
973
+ const id = liveSessionId(params.sessionId);
811
974
  const session = sessions.get(id);
812
975
  if (!session) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
813
976
  // Claude runs one turn at a time, so extra input waits its turn instead of
@@ -966,7 +1129,7 @@ async function dispatch(method, params = {}) {
966
1129
  };
967
1130
  }
968
1131
  if (method === "session/resume") {
969
- const id = rawSession(params.sessionId);
1132
+ const id = liveSessionId(params.sessionId);
970
1133
  const existing = sessions.get(id);
971
1134
  if (existing) {
972
1135
  const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
@@ -1022,13 +1185,13 @@ async function dispatch(method, params = {}) {
1022
1185
  };
1023
1186
  }
1024
1187
  if (method === "session/history") {
1025
- const id = rawSession(params.sessionId);
1188
+ const id = liveSessionId(params.sessionId);
1026
1189
  const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
1027
1190
  return { data: historyTurns(messages), nextCursor: null };
1028
1191
  }
1029
1192
  if (method === "session/prompt") return startPrompt(params);
1030
1193
  if (method === "session/interrupt") {
1031
- const session = sessions.get(rawSession(params.sessionId));
1194
+ const session = lookupSession(params.sessionId);
1032
1195
  // Stopping the run drops what was waiting behind it too, so nothing the user
1033
1196
  // just cancelled starts on its own afterwards.
1034
1197
  if (session) session.pendingPrompts.length = 0;
@@ -1048,7 +1211,7 @@ async function dispatch(method, params = {}) {
1048
1211
  return startPrompt({ ...params, input: [{ type: "text", text: "/compact" }] });
1049
1212
  }
1050
1213
  if (method === "session/fork") {
1051
- const source = rawSession(params.sessionId);
1214
+ const source = liveSessionId(params.sessionId);
1052
1215
  const forked = await forkSession(source, { dir: params.cwd });
1053
1216
  const id = forked.sessionId || forked;
1054
1217
  const { session, account, usage } = await createSession(params, id);
@@ -1063,17 +1226,20 @@ async function dispatch(method, params = {}) {
1063
1226
  };
1064
1227
  }
1065
1228
  if (method === "session/close") {
1066
- const session = sessions.get(rawSession(params.sessionId));
1229
+ const session = lookupSession(params.sessionId);
1067
1230
  if (session) {
1068
1231
  session.queue.close();
1069
1232
  session.query.close();
1070
1233
  sessions.delete(session.id);
1234
+ for (const [from, to] of sessionAliases) {
1235
+ if (to === session.id) sessionAliases.delete(from);
1236
+ }
1071
1237
  if (params.delete) await deleteSession(session.id, { dir: session.cwd });
1072
1238
  }
1073
1239
  return {};
1074
1240
  }
1075
1241
  if (method === "account/usage") {
1076
- const session = sessions.get(rawSession(params.sessionId)) || [...sessions.values()][0];
1242
+ const session = lookupSession(params.sessionId) || [...sessions.values()][0];
1077
1243
  if (!session) return { account: null, usage: null };
1078
1244
  return { account: await safeAccount(session.query), usage: await safeUsage(session.query) };
1079
1245
  }
@@ -1083,6 +1249,7 @@ async function dispatch(method, params = {}) {
1083
1249
  session.query.close();
1084
1250
  }
1085
1251
  sessions.clear();
1252
+ sessionAliases.clear();
1086
1253
  return {};
1087
1254
  }
1088
1255
  throw new Error(`지원하지 않는 Claude 브리지 메서드: ${method}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "0.1.48",
3
+ "version": "1.2.0",
4
4
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
5
5
  "keywords": [
6
6
  "codex",