wave-code 1.0.6 → 1.0.8

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.
Files changed (54) hide show
  1. package/dist/components/AgentsManager.d.ts +7 -0
  2. package/dist/components/AgentsManager.js +109 -0
  3. package/dist/components/ChatInterface.js +1 -1
  4. package/dist/components/ConfirmationDetails.d.ts +1 -0
  5. package/dist/components/ConfirmationDetails.js +5 -3
  6. package/dist/components/ConfirmationSelector.js +17 -3
  7. package/dist/components/InputBox.js +7 -21
  8. package/dist/components/LoginCommand.js +31 -2
  9. package/dist/components/MarketplaceAddForm.js +16 -2
  10. package/dist/components/RewindCommand.js +11 -4
  11. package/dist/constants/commands.js +6 -0
  12. package/dist/contexts/useChat.d.ts +18 -2
  13. package/dist/contexts/useChat.js +114 -9
  14. package/dist/daemon/commands.d.ts +49 -0
  15. package/dist/daemon/commands.js +341 -0
  16. package/dist/daemon/jsonRpcClient.d.ts +38 -0
  17. package/dist/daemon/jsonRpcClient.js +129 -0
  18. package/dist/daemon/socketClient.d.ts +13 -0
  19. package/dist/daemon/socketClient.js +26 -0
  20. package/dist/hooks/useInputManager.d.ts +2 -0
  21. package/dist/hooks/useInputManager.js +8 -0
  22. package/dist/index.js +88 -0
  23. package/dist/managers/inputHandlers.js +3 -0
  24. package/dist/managers/inputReducer.d.ts +4 -0
  25. package/dist/managers/inputReducer.js +8 -0
  26. package/dist/reducers/agentsManagerReducer.d.ts +26 -0
  27. package/dist/reducers/agentsManagerReducer.js +54 -0
  28. package/dist/stdio/agentBridge.d.ts +15 -0
  29. package/dist/stdio/agentBridge.js +101 -20
  30. package/dist/stdio/protocol.d.ts +1 -1
  31. package/dist/utils/usageSummary.d.ts +0 -4
  32. package/dist/utils/usageSummary.js +1 -34
  33. package/package.json +2 -2
  34. package/src/components/AgentsManager.tsx +290 -0
  35. package/src/components/ChatInterface.tsx +2 -0
  36. package/src/components/ConfirmationDetails.tsx +6 -0
  37. package/src/components/ConfirmationSelector.tsx +18 -3
  38. package/src/components/InputBox.tsx +54 -45
  39. package/src/components/LoginCommand.tsx +35 -2
  40. package/src/components/MarketplaceAddForm.tsx +17 -2
  41. package/src/components/RewindCommand.tsx +10 -4
  42. package/src/constants/commands.ts +6 -0
  43. package/src/contexts/useChat.tsx +146 -7
  44. package/src/daemon/commands.ts +444 -0
  45. package/src/daemon/jsonRpcClient.ts +158 -0
  46. package/src/daemon/socketClient.ts +34 -0
  47. package/src/hooks/useInputManager.ts +8 -0
  48. package/src/index.ts +130 -0
  49. package/src/managers/inputHandlers.ts +2 -0
  50. package/src/managers/inputReducer.ts +10 -0
  51. package/src/reducers/agentsManagerReducer.ts +91 -0
  52. package/src/stdio/agentBridge.ts +123 -19
  53. package/src/stdio/protocol.ts +4 -0
  54. package/src/utils/usageSummary.ts +2 -46
@@ -14,7 +14,7 @@
14
14
  * - Implement the canUseTool permission flow over the stdio protocol
15
15
  * - Handle config updates by destroying and recreating the Agent
16
16
  */
17
- import { Agent, listSessions, searchFiles, generateRandomName, getDefaultRemoteBranch, getMessageContent, PromptHistoryManager, AuthService, PluginCore, validateWorktreeRemovalPath, } from "wave-agent-sdk";
17
+ import { Agent, listSessions, searchFiles, generateRandomName, getDefaultRemoteBranch, getMessageContent, PromptHistoryManager, AuthService, PluginCore, validateWorktreeRemovalPath, loadUserConfigEnv, } from "wave-agent-sdk";
18
18
  import { INVALID_PARAMS as PROTOCOL_INVALID_PARAMS, INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR, METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND, } from "./protocol.js";
19
19
  import { execFileSync } from "node:child_process";
20
20
  import { mkdirSync, existsSync, writeFileSync } from "node:fs";
@@ -32,6 +32,15 @@ export class AgentBridge {
32
32
  this.pendingPermissions = new Map();
33
33
  this.permissionCounter = 0;
34
34
  this.emit = options.emit;
35
+ // Mirror the user-level settings env WAVE_SERVER_URL into process.env
36
+ // before any agent initializes. getAuthStatus (webviewReady →
37
+ // pushInitialState) can run before the first agent, and AuthService falls
38
+ // back to the default URL otherwise — refreshing a custom-domain token
39
+ // against the wrong host 401s into a logged-out state.
40
+ const userEnv = loadUserConfigEnv();
41
+ if (userEnv.WAVE_SERVER_URL) {
42
+ process.env.WAVE_SERVER_URL = userEnv.WAVE_SERVER_URL;
43
+ }
35
44
  }
36
45
  // ── Public API ────────────────────────────────────────────────
37
46
  async handleRequest(method, params, sessionId) {
@@ -50,8 +59,14 @@ export class AgentBridge {
50
59
  return this.getSessionInfo(sessionId);
51
60
  case "listPendingPermissions":
52
61
  return this.listPendingPermissions();
62
+ case "listDaemonSessions":
63
+ return this.listDaemonSessions();
53
64
  case "updateConfig":
54
65
  return this.updateConfig(p, sessionId);
66
+ case "getConfiguredModels":
67
+ return this.getConfiguredModels(sessionId);
68
+ case "setModel":
69
+ return this.setModel(p.model, sessionId);
55
70
  // ── Messages ──
56
71
  case "sendMessage":
57
72
  return this.sendMessage(p, sessionId);
@@ -92,6 +107,8 @@ export class AgentBridge {
92
107
  // ── Commands ──
93
108
  case "getSlashCommands":
94
109
  return this.getSlashCommands(sessionId);
110
+ case "getSubagentConfigurations":
111
+ return this.getSubagentConfigurations(sessionId);
95
112
  // ── File / History (global — no session required) ──
96
113
  case "searchFiles":
97
114
  return this.searchFiles(p, sessionId);
@@ -430,6 +447,22 @@ export class AgentBridge {
430
447
  });
431
448
  return { sessionId: agent.sessionId };
432
449
  }
450
+ getConfiguredModels(sessionId) {
451
+ const entry = this.requireSession(sessionId);
452
+ return {
453
+ models: entry.agent.getConfiguredModels(),
454
+ currentModel: entry.agent.getModelConfig().model,
455
+ };
456
+ }
457
+ async setModel(model, sessionId) {
458
+ const entry = this.requireSession(sessionId);
459
+ entry.agent.setModel(model);
460
+ // Keep storedConfig in sync: updateConfig recreates the agent from
461
+ // storedConfig, so without this a later config save would revert the
462
+ // model chosen here.
463
+ entry.storedConfig = { ...entry.storedConfig, model };
464
+ return null;
465
+ }
433
466
  // ── Messages ──────────────────────────────────────────────────
434
467
  async sendMessage(params, sessionId) {
435
468
  const entry = this.requireSession(sessionId);
@@ -443,9 +476,39 @@ export class AgentBridge {
443
476
  catch {
444
477
  // Best-effort; don't block message sending on history save failure
445
478
  }
446
- await entry.agent.sendMessage(params.text, params.images);
479
+ await entry.agent.sendMessage(params.text, this.persistDataUrlImages(params.images));
447
480
  return null;
448
481
  }
482
+ /**
483
+ * Webview hosts (desktop/vscode/jetbrains) send pasted images as inline
484
+ * data URLs — there is no local file behind them. Persist each to a temp
485
+ * file so the model gets a real path it can reference with tools (aligned
486
+ * with Claude Code's `[Image source: <path>]` metadata). Real paths pass
487
+ * through untouched; unparseable data URLs pass through as-is and are
488
+ * skipped by the SDK rather than blocking the message.
489
+ */
490
+ persistDataUrlImages(images) {
491
+ if (!images || images.length === 0)
492
+ return images;
493
+ return images.map((img) => {
494
+ if (!img.path.startsWith("data:"))
495
+ return img;
496
+ const match = /^data:([^;,]+);base64,(.*)$/s.exec(img.path);
497
+ if (!match)
498
+ return img;
499
+ try {
500
+ const mimeType = match[1];
501
+ const ext = mimeType.split("/")[1]?.replace("jpeg", "jpg") || "png";
502
+ const filePath = join(tmpdir(), `wave-image-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`);
503
+ writeFileSync(filePath, Buffer.from(match[2], "base64"));
504
+ return { path: filePath, mimeType };
505
+ }
506
+ catch (error) {
507
+ logger.warn("Failed to persist pasted image to temp file:", error);
508
+ return img;
509
+ }
510
+ });
511
+ }
449
512
  async bang(command, sessionId) {
450
513
  const entry = this.requireSession(sessionId);
451
514
  await entry.agent.bang(command);
@@ -480,7 +543,10 @@ export class AgentBridge {
480
543
  async rewindToMessage(messageId, sessionId) {
481
544
  const entry = this.requireSession(sessionId);
482
545
  const { messages } = await entry.agent.getFullMessageThread();
483
- const index = messages.findIndex((m) => m.id === messageId);
546
+ // 压缩是 append-only:同 id 消息(压缩前历史 + 压缩后 append 的重复)会
547
+ // 在磁盘完整线程中出现多次。用户看到的折叠视图对应最后一次出现,
548
+ // 因此匹配最后一个而非第一个,避免回滚时连压缩摘要一起删掉。
549
+ const index = messages.map((m) => m.id).lastIndexOf(messageId);
484
550
  if (index === -1) {
485
551
  throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Message not found: ${messageId}`);
486
552
  }
@@ -492,13 +558,19 @@ export class AgentBridge {
492
558
  async listRewindCheckpoints(sessionId) {
493
559
  const entry = this.requireSession(sessionId);
494
560
  const { messages } = await entry.agent.getFullMessageThread();
495
- const checkpoints = messages
496
- .filter((m) => isUserCheckpointMessage(m) && m.id)
497
- .map((m) => ({
498
- id: m.id,
499
- content: getMessageContent(m).replace(/\s+/g, " ").trim(),
500
- }));
501
- return { checkpoints };
561
+ // 压缩 append-only 后同 id 消息在磁盘完整线程中重复出现(压缩前历史 +
562
+ // 压缩后 append 的重复)。按 id 去重并保留最后一次出现(与折叠后的
563
+ // UI/内存视图一致),避免弹窗把同一条用户消息显示两遍。
564
+ const checkpointMap = new Map();
565
+ for (const m of messages) {
566
+ if (isUserCheckpointMessage(m) && m.id) {
567
+ checkpointMap.set(m.id, {
568
+ id: m.id,
569
+ content: getMessageContent(m).replace(/\s+/g, " ").trim(),
570
+ });
571
+ }
572
+ }
573
+ return { checkpoints: Array.from(checkpointMap.values()) };
502
574
  }
503
575
  deleteQueuedMessage(index, sessionId) {
504
576
  const entry = this.requireSession(sessionId);
@@ -509,7 +581,7 @@ export class AgentBridge {
509
581
  const entry = this.requireSession(sessionId);
510
582
  const ok = entry.agent.updateQueuedMessageById(id, {
511
583
  content: text,
512
- images,
584
+ images: this.persistDataUrlImages(images),
513
585
  });
514
586
  return { ok };
515
587
  }
@@ -592,6 +664,10 @@ export class AgentBridge {
592
664
  const entry = this.requireSession(sessionId);
593
665
  return { commands: entry.agent.getSlashCommands() };
594
666
  }
667
+ getSubagentConfigurations(sessionId) {
668
+ const entry = this.requireSession(sessionId);
669
+ return { configurations: entry.agent.getSubagentConfigurations() };
670
+ }
595
671
  // ── File / History (global) ───────────────────────────────────
596
672
  async searchFiles(params, sessionId) {
597
673
  const files = await searchFiles(params.query, {
@@ -663,16 +739,21 @@ export class AgentBridge {
663
739
  })),
664
740
  };
665
741
  }
742
+ /** Daemon list: expose the in-memory session registry (live sessions only,
743
+ * not disk-scanning). Registration order is preserved. */
744
+ listDaemonSessions() {
745
+ return {
746
+ sessions: [...this.sessions.entries()].map(([sessionId, entry]) => ({
747
+ sessionId,
748
+ workingDirectory: entry.agent.workingDirectory,
749
+ isLoading: entry.agent.isLoading,
750
+ messageCount: entry.agent.messages.length,
751
+ })),
752
+ };
753
+ }
666
754
  // ── Auth (global) ────────────────────────────────────────────
667
755
  async getAuthStatus() {
668
756
  const authService = AuthService.getInstance();
669
- // A stale-but-refreshable token still means "logged in" — the daemon may
670
- // have started with an expired access token (hourly expiry) and only
671
- // refreshes lazily on the first API call. Without this proactive refresh a
672
- // fresh client querying right after daemon start gets a false
673
- // isAuthenticated and e.g. the desktop welcome page keeps showing the
674
- // login button for an authenticated host. Mirrors the refresh that
675
- // createAuthAwareFetch does before every real request.
676
757
  await authService.checkAndRefreshTokenIfNeeded();
677
758
  return {
678
759
  isAuthenticated: authService.isSSOAuthenticated(),
@@ -868,8 +949,8 @@ export class AgentBridge {
868
949
  onUpdateBangMessage: (command, output, messageId) => {
869
950
  this.emit("bangMessageUpdated", { command, output, messageId }, ctx.registeredSessionId);
870
951
  },
871
- onCompleteBangMessage: (command, exitCode, messageId) => {
872
- this.emit("bangMessageCompleted", { command, exitCode, messageId }, ctx.registeredSessionId);
952
+ onCompleteBangMessage: (command, exitCode, messageId, output) => {
953
+ this.emit("bangMessageCompleted", { command, exitCode, messageId, output }, ctx.registeredSessionId);
873
954
  },
874
955
  onNotificationMessageAdded: (params) => {
875
956
  const msg = ctx.agent?.messages.find((m) => m.role === "user" &&
@@ -34,7 +34,7 @@ export declare const INVALID_REQUEST = -32600;
34
34
  export declare const METHOD_NOT_FOUND = -32601;
35
35
  export declare const INVALID_PARAMS = -32602;
36
36
  export declare const INTERNAL_ERROR = -32603;
37
- export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "writeArtifactFile" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "listPendingPermissions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
37
+ export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "writeArtifactFile" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getConfiguredModels" | "setModel" | "listPendingPermissions" | "listDaemonSessions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
38
38
  export type ClientNotificationMethod = "permissionResponse";
39
39
  export type ServerNotificationMethod = "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded" | "compactionStateChange" | "backgroundTasksChange" | "btwContent";
40
40
  export declare function isRequest(msg: unknown): msg is JsonRpcRequest;
@@ -13,10 +13,6 @@ export interface TokenSummary {
13
13
  };
14
14
  cache_read_input_tokens?: number;
15
15
  cache_creation_input_tokens?: number;
16
- cache_creation?: {
17
- ephemeral_5m_input_tokens: number;
18
- ephemeral_1h_input_tokens: number;
19
- };
20
16
  }
21
17
  /**
22
18
  * Calculate token usage summary by model from usage array
@@ -34,20 +34,6 @@ export function calculateTokenSummary(usages) {
34
34
  (summary.cache_creation_input_tokens || 0) +
35
35
  usage.cache_creation_input_tokens;
36
36
  }
37
- if (usage.cache_creation &&
38
- (usage.cache_creation.ephemeral_5m_input_tokens > 0 ||
39
- usage.cache_creation.ephemeral_1h_input_tokens > 0)) {
40
- if (!summary.cache_creation) {
41
- summary.cache_creation = {
42
- ephemeral_5m_input_tokens: 0,
43
- ephemeral_1h_input_tokens: 0,
44
- };
45
- }
46
- summary.cache_creation.ephemeral_5m_input_tokens +=
47
- usage.cache_creation.ephemeral_5m_input_tokens || 0;
48
- summary.cache_creation.ephemeral_1h_input_tokens +=
49
- usage.cache_creation.ephemeral_1h_input_tokens || 0;
50
- }
51
37
  // Track operation types
52
38
  if (usage.operation_type === "agent") {
53
39
  summary.operations.agent_calls += 1;
@@ -86,8 +72,6 @@ export function displayUsageSummary(usages, sessionFilePath) {
86
72
  let totalCompactions = 0;
87
73
  let totalCacheRead = 0;
88
74
  let totalCacheCreation = 0;
89
- let totalCache5m = 0;
90
- let totalCache1h = 0;
91
75
  let hasCacheData = false;
92
76
  for (const [, summary] of Object.entries(summaries)) {
93
77
  console.log(`Model: ${summary.model}`);
@@ -96,8 +80,7 @@ export function displayUsageSummary(usages, sessionFilePath) {
96
80
  console.log(` Total tokens: ${summary.total_tokens.toLocaleString()}`);
97
81
  // Display cache information if available
98
82
  if (summary.cache_read_input_tokens ||
99
- summary.cache_creation_input_tokens ||
100
- summary.cache_creation) {
83
+ summary.cache_creation_input_tokens) {
101
84
  hasCacheData = true;
102
85
  console.log(" Cache Usage:");
103
86
  if (summary.cache_read_input_tokens &&
@@ -110,16 +93,6 @@ export function displayUsageSummary(usages, sessionFilePath) {
110
93
  console.log(` Created cache: ${summary.cache_creation_input_tokens.toLocaleString()} tokens`);
111
94
  totalCacheCreation += summary.cache_creation_input_tokens;
112
95
  }
113
- if (summary.cache_creation) {
114
- if (summary.cache_creation.ephemeral_5m_input_tokens > 0) {
115
- console.log(` 5m cache: ${summary.cache_creation.ephemeral_5m_input_tokens.toLocaleString()} tokens`);
116
- totalCache5m += summary.cache_creation.ephemeral_5m_input_tokens;
117
- }
118
- if (summary.cache_creation.ephemeral_1h_input_tokens > 0) {
119
- console.log(` 1h cache: ${summary.cache_creation.ephemeral_1h_input_tokens.toLocaleString()} tokens`);
120
- totalCache1h += summary.cache_creation.ephemeral_1h_input_tokens;
121
- }
122
- }
123
96
  }
124
97
  console.log(` Operations: ${summary.operations.agent_calls} agent calls, ${summary.operations.compactions} compactions`);
125
98
  console.log();
@@ -142,12 +115,6 @@ export function displayUsageSummary(usages, sessionFilePath) {
142
115
  if (totalCacheCreation > 0) {
143
116
  console.log(` Created cache: ${totalCacheCreation.toLocaleString()} tokens`);
144
117
  }
145
- if (totalCache5m > 0) {
146
- console.log(` 5m cache: ${totalCache5m.toLocaleString()} tokens`);
147
- }
148
- if (totalCache1h > 0) {
149
- console.log(` 1h cache: ${totalCache1h.toLocaleString()} tokens`);
150
- }
151
118
  }
152
119
  console.log(` Operations: ${totalAgentCalls} agent calls, ${totalCompactions} compactions`);
153
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "wrap-ansi": "^10.0.0",
44
44
  "yargs": "^17.7.2",
45
45
  "zod": "^3.23.8",
46
- "wave-agent-sdk": "1.0.6"
46
+ "wave-agent-sdk": "1.0.8"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/react": "^19.1.8",
@@ -0,0 +1,290 @@
1
+ import React, { useEffect, useMemo, useReducer } from "react";
2
+ import { Box, Text, useInput, useStdout } from "ink";
3
+ import type { SubagentConfiguration } from "wave-agent-sdk";
4
+ import { Markdown } from "./Markdown.js";
5
+ import {
6
+ agentsManagerReducer,
7
+ type AgentsManagerState,
8
+ } from "../reducers/agentsManagerReducer.js";
9
+
10
+ export interface AgentsManagerProps {
11
+ onCancel: () => void;
12
+ agentDefinitions: SubagentConfiguration[];
13
+ }
14
+
15
+ interface DisplayEntry {
16
+ kind: "header" | "definition" | "empty";
17
+ label: string;
18
+ sub?: string;
19
+ model?: string;
20
+ scope?: SubagentConfiguration["scope"];
21
+ selectableIndex: number; // -1 for non-selectable rows
22
+ definition?: SubagentConfiguration;
23
+ }
24
+
25
+ const SCOPE_LABELS: Record<SubagentConfiguration["scope"], string> = {
26
+ builtin: "Built-in agents",
27
+ user: "User agents",
28
+ project: "Project agents",
29
+ plugin: "Plugin agents",
30
+ };
31
+
32
+ const SCOPE_ORDER: SubagentConfiguration["scope"][] = [
33
+ "builtin",
34
+ "user",
35
+ "project",
36
+ "plugin",
37
+ ];
38
+
39
+ const initialState: AgentsManagerState = {
40
+ selectedIndex: 0,
41
+ viewMode: "list",
42
+ pendingEffect: null,
43
+ };
44
+
45
+ export const AgentsManager: React.FC<AgentsManagerProps> = ({
46
+ onCancel,
47
+ agentDefinitions,
48
+ }) => {
49
+ const [state, dispatch] = useReducer(agentsManagerReducer, initialState);
50
+ const { stdout } = useStdout();
51
+
52
+ // Handle pending effects
53
+ useEffect(() => {
54
+ if (!state.pendingEffect) return;
55
+ const effect = state.pendingEffect;
56
+ dispatch({ type: "CLEAR_PENDING_EFFECT" });
57
+ if (effect.type === "CANCEL") {
58
+ onCancel();
59
+ }
60
+ }, [state.pendingEffect, onCancel]);
61
+
62
+ // Flatten definitions (grouped by scope) into one navigable list. Headers
63
+ // and the empty-state line are non-selectable.
64
+ const entries = useMemo<DisplayEntry[]>(() => {
65
+ const result: DisplayEntry[] = [];
66
+ let selectableCount = 0;
67
+
68
+ result.push({ kind: "header", label: "AGENTS", selectableIndex: -1 });
69
+ let definitionCount = 0;
70
+ for (const scope of SCOPE_ORDER) {
71
+ const defs = agentDefinitions
72
+ .filter((d) => d.scope === scope)
73
+ .sort((a, b) => a.name.localeCompare(b.name));
74
+ if (defs.length === 0) continue;
75
+ result.push({
76
+ kind: "header",
77
+ label: SCOPE_LABELS[scope],
78
+ scope,
79
+ selectableIndex: -1,
80
+ });
81
+ for (const def of defs) {
82
+ result.push({
83
+ kind: "definition",
84
+ label: def.name,
85
+ model: def.model,
86
+ sub: def.description,
87
+ scope: def.scope,
88
+ selectableIndex: selectableCount++,
89
+ definition: def,
90
+ });
91
+ definitionCount++;
92
+ }
93
+ }
94
+ if (definitionCount === 0) {
95
+ result.push({
96
+ kind: "empty",
97
+ label: "No agents available",
98
+ selectableIndex: -1,
99
+ });
100
+ }
101
+
102
+ return result;
103
+ }, [agentDefinitions]);
104
+
105
+ const itemCount = entries.filter((e) => e.selectableIndex >= 0).length;
106
+
107
+ // Window slice: center the selected item within the visible area, clamping
108
+ // to the terminal's available rows (reusable pattern from
109
+ // BackgroundTaskManager).
110
+ const availableRows = stdout?.rows ?? 24;
111
+ const maxVisible = Math.max(3, Math.min(15, availableRows - 12));
112
+ const selectedFlatIndex = entries.findIndex(
113
+ (e) => e.selectableIndex === state.selectedIndex,
114
+ );
115
+ const startIndex = Math.max(
116
+ 0,
117
+ Math.min(
118
+ selectedFlatIndex - Math.floor(maxVisible / 2),
119
+ Math.max(0, entries.length - maxVisible),
120
+ ),
121
+ );
122
+ const visibleEntries = entries.slice(startIndex, startIndex + maxVisible);
123
+
124
+ useInput((input, key) => {
125
+ dispatch({ type: "HANDLE_KEY", input, key, itemCount });
126
+ });
127
+
128
+ const selectedEntry = entries.find(
129
+ (e) => e.selectableIndex === state.selectedIndex,
130
+ );
131
+
132
+ // Detail view — body renders fully expanded with no height limit, no
133
+ // clipping and no scrolling (aligned with Claude Code's AgentDetail).
134
+ if (state.viewMode === "detail" && selectedEntry) {
135
+ const def = selectedEntry.definition;
136
+ return (
137
+ <Box
138
+ flexDirection="column"
139
+ borderStyle="single"
140
+ borderColor="cyan"
141
+ borderBottom={false}
142
+ borderLeft={false}
143
+ borderRight={false}
144
+ paddingTop={1}
145
+ gap={1}
146
+ >
147
+ <Box>
148
+ <Text color="cyan" bold>
149
+ Agent: {selectedEntry.label}
150
+ </Text>
151
+ </Box>
152
+
153
+ <Box flexDirection="column" gap={1}>
154
+ {def?.description && (
155
+ <Box>
156
+ <Text>
157
+ <Text color="blue">Description:</Text> {def.description}
158
+ </Text>
159
+ </Box>
160
+ )}
161
+ <Box>
162
+ <Text>
163
+ <Text color="blue">Model:</Text>{" "}
164
+ {def?.model || "default (not explicitly configured)"}
165
+ </Text>
166
+ </Box>
167
+ <Box>
168
+ <Text>
169
+ <Text color="blue">Scope:</Text>{" "}
170
+ {def ? SCOPE_LABELS[def.scope] : ""}
171
+ </Text>
172
+ </Box>
173
+ {def?.tools && def.tools.length > 0 && (
174
+ <Box>
175
+ <Text wrap="wrap">
176
+ <Text color="blue">Tools:</Text> {def.tools.join(", ")}
177
+ </Text>
178
+ </Box>
179
+ )}
180
+ {def?.filePath && (
181
+ <Box>
182
+ <Text wrap="wrap">
183
+ <Text color="blue">File:</Text> {def.filePath}
184
+ </Text>
185
+ </Box>
186
+ )}
187
+ </Box>
188
+
189
+ {def && (
190
+ <Box flexDirection="column" marginTop={1}>
191
+ <Text color="blue" bold>
192
+ System Prompt:
193
+ </Text>
194
+ <Box marginLeft={2} marginRight={2}>
195
+ <Markdown>{def.systemPrompt}</Markdown>
196
+ </Box>
197
+ </Box>
198
+ )}
199
+
200
+ <Box marginTop={1}>
201
+ <Text dimColor>Esc or Enter to go back</Text>
202
+ </Box>
203
+ </Box>
204
+ );
205
+ }
206
+
207
+ if (itemCount === 0) {
208
+ return (
209
+ <Box
210
+ flexDirection="column"
211
+ borderStyle="single"
212
+ borderColor="cyan"
213
+ borderBottom={false}
214
+ borderLeft={false}
215
+ borderRight={false}
216
+ paddingTop={1}
217
+ >
218
+ <Text color="cyan" bold>
219
+ Agents
220
+ </Text>
221
+ <Text>No agents available</Text>
222
+ <Text dimColor>Press Escape to close</Text>
223
+ </Box>
224
+ );
225
+ }
226
+
227
+ return (
228
+ <Box
229
+ flexDirection="column"
230
+ borderStyle="single"
231
+ borderColor="cyan"
232
+ borderBottom={false}
233
+ borderLeft={false}
234
+ borderRight={false}
235
+ paddingTop={1}
236
+ gap={1}
237
+ >
238
+ <Box>
239
+ <Text color="cyan" bold>
240
+ Agents
241
+ </Text>
242
+ </Box>
243
+ <Text dimColor>Select an agent to view details</Text>
244
+
245
+ <Box flexDirection="column">
246
+ {visibleEntries.map((entry, index) => {
247
+ const isSelected = entry.selectableIndex === state.selectedIndex;
248
+ if (entry.kind === "header") {
249
+ return (
250
+ <Text key={`${entry.kind}-${entry.label}-${index}`} dimColor bold>
251
+ {entry.label}
252
+ </Text>
253
+ );
254
+ }
255
+ if (entry.kind === "empty") {
256
+ return (
257
+ <Text key={`empty-${index}`} dimColor>
258
+ {entry.label}
259
+ </Text>
260
+ );
261
+ }
262
+ return (
263
+ <Text
264
+ key={`${entry.kind}-${entry.selectableIndex}`}
265
+ color={isSelected ? "black" : "white"}
266
+ backgroundColor={isSelected ? "cyan" : undefined}
267
+ wrap="truncate-end"
268
+ >
269
+ {isSelected ? "▶ " : " "}
270
+ {entry.selectableIndex + 1}. {entry.label}
271
+ {entry.model ? (
272
+ <Text color={isSelected ? "black" : "gray"}>
273
+ {" "}
274
+ · {entry.model}
275
+ </Text>
276
+ ) : null}
277
+ {entry.sub ? ` · ${entry.sub}` : ""}
278
+ </Text>
279
+ );
280
+ })}
281
+ </Box>
282
+
283
+ <Box marginTop={1}>
284
+ <Text dimColor>
285
+ ↑/↓ to select · Enter to view details · Esc to close
286
+ </Text>
287
+ </Box>
288
+ </Box>
289
+ );
290
+ };
@@ -142,6 +142,7 @@ export const ChatInterface: React.FC = () => {
142
142
  toolName={confirmingTool!.name}
143
143
  toolInput={confirmingTool!.input}
144
144
  planContent={confirmingTool!.planContent}
145
+ warning={confirmingTool!.warning}
145
146
  isExpanded={isExpanded}
146
147
  />
147
148
  )}
@@ -151,6 +152,7 @@ export const ChatInterface: React.FC = () => {
151
152
  toolName={confirmingTool!.name}
152
153
  toolInput={confirmingTool!.input}
153
154
  planContent={confirmingTool!.planContent}
155
+ warning={confirmingTool!.warning}
154
156
  isExpanded={isExpanded}
155
157
  />
156
158
  )}
@@ -7,6 +7,7 @@ import {
7
7
  EXIT_PLAN_MODE_TOOL_NAME,
8
8
  ENTER_PLAN_MODE_TOOL_NAME,
9
9
  ASK_USER_QUESTION_TOOL_NAME,
10
+ ARTIFACT_TOOL_NAME,
10
11
  } from "wave-agent-sdk";
11
12
  import { DiffDisplay } from "./DiffDisplay.js";
12
13
  import { PlanDisplay } from "./PlanDisplay.js";
@@ -34,6 +35,8 @@ const getActionDescription = (
34
35
  return "Enter plan mode for complex task planning";
35
36
  case ASK_USER_QUESTION_TOOL_NAME:
36
37
  return "Answer questions to clarify intent";
38
+ case ARTIFACT_TOOL_NAME:
39
+ return `Publish file: ${toolInput.file_path || "unknown file"}`;
37
40
  default:
38
41
  return "Execute operation";
39
42
  }
@@ -43,6 +46,7 @@ export interface ConfirmationDetailsProps {
43
46
  toolName: string;
44
47
  toolInput?: Record<string, unknown>;
45
48
  planContent?: string;
49
+ warning?: string;
46
50
  isExpanded?: boolean;
47
51
  }
48
52
 
@@ -50,6 +54,7 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
50
54
  toolName,
51
55
  toolInput,
52
56
  planContent,
57
+ warning,
53
58
  isExpanded = false,
54
59
  }) => {
55
60
  const startLineNumber =
@@ -69,6 +74,7 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
69
74
  Tool: {toolName}
70
75
  </Text>
71
76
  <Text color="yellow">{getActionDescription(toolName, toolInput)}</Text>
77
+ {warning && <Text color="red">⚠ {warning}</Text>}
72
78
 
73
79
  <DiffDisplay
74
80
  toolName={toolName}