wave-code 1.0.6 → 1.0.7

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 (36) hide show
  1. package/dist/components/AgentsManager.d.ts +7 -0
  2. package/dist/components/AgentsManager.js +109 -0
  3. package/dist/components/ConfirmationSelector.js +17 -3
  4. package/dist/components/InputBox.js +7 -21
  5. package/dist/components/LoginCommand.js +31 -2
  6. package/dist/components/MarketplaceAddForm.js +16 -2
  7. package/dist/constants/commands.js +6 -0
  8. package/dist/contexts/useChat.d.ts +2 -1
  9. package/dist/contexts/useChat.js +15 -2
  10. package/dist/hooks/useInputManager.d.ts +2 -0
  11. package/dist/hooks/useInputManager.js +8 -0
  12. package/dist/managers/inputHandlers.js +3 -0
  13. package/dist/managers/inputReducer.d.ts +4 -0
  14. package/dist/managers/inputReducer.js +8 -0
  15. package/dist/reducers/agentsManagerReducer.d.ts +26 -0
  16. package/dist/reducers/agentsManagerReducer.js +54 -0
  17. package/dist/stdio/agentBridge.d.ts +3 -0
  18. package/dist/stdio/agentBridge.js +38 -10
  19. package/dist/stdio/protocol.d.ts +1 -1
  20. package/dist/utils/usageSummary.d.ts +0 -4
  21. package/dist/utils/usageSummary.js +1 -34
  22. package/package.json +2 -2
  23. package/src/components/AgentsManager.tsx +290 -0
  24. package/src/components/ConfirmationSelector.tsx +18 -3
  25. package/src/components/InputBox.tsx +54 -45
  26. package/src/components/LoginCommand.tsx +35 -2
  27. package/src/components/MarketplaceAddForm.tsx +17 -2
  28. package/src/constants/commands.ts +6 -0
  29. package/src/contexts/useChat.tsx +23 -2
  30. package/src/hooks/useInputManager.ts +8 -0
  31. package/src/managers/inputHandlers.ts +2 -0
  32. package/src/managers/inputReducer.ts +10 -0
  33. package/src/reducers/agentsManagerReducer.ts +91 -0
  34. package/src/stdio/agentBridge.ts +47 -9
  35. package/src/stdio/protocol.ts +2 -0
  36. 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) {
@@ -52,6 +61,10 @@ export class AgentBridge {
52
61
  return this.listPendingPermissions();
53
62
  case "updateConfig":
54
63
  return this.updateConfig(p, sessionId);
64
+ case "getConfiguredModels":
65
+ return this.getConfiguredModels(sessionId);
66
+ case "setModel":
67
+ return this.setModel(p.model, sessionId);
55
68
  // ── Messages ──
56
69
  case "sendMessage":
57
70
  return this.sendMessage(p, sessionId);
@@ -92,6 +105,8 @@ export class AgentBridge {
92
105
  // ── Commands ──
93
106
  case "getSlashCommands":
94
107
  return this.getSlashCommands(sessionId);
108
+ case "getSubagentConfigurations":
109
+ return this.getSubagentConfigurations(sessionId);
95
110
  // ── File / History (global — no session required) ──
96
111
  case "searchFiles":
97
112
  return this.searchFiles(p, sessionId);
@@ -430,6 +445,22 @@ export class AgentBridge {
430
445
  });
431
446
  return { sessionId: agent.sessionId };
432
447
  }
448
+ getConfiguredModels(sessionId) {
449
+ const entry = this.requireSession(sessionId);
450
+ return {
451
+ models: entry.agent.getConfiguredModels(),
452
+ currentModel: entry.agent.getModelConfig().model,
453
+ };
454
+ }
455
+ async setModel(model, sessionId) {
456
+ const entry = this.requireSession(sessionId);
457
+ entry.agent.setModel(model);
458
+ // Keep storedConfig in sync: updateConfig recreates the agent from
459
+ // storedConfig, so without this a later config save would revert the
460
+ // model chosen here.
461
+ entry.storedConfig = { ...entry.storedConfig, model };
462
+ return null;
463
+ }
433
464
  // ── Messages ──────────────────────────────────────────────────
434
465
  async sendMessage(params, sessionId) {
435
466
  const entry = this.requireSession(sessionId);
@@ -592,6 +623,10 @@ export class AgentBridge {
592
623
  const entry = this.requireSession(sessionId);
593
624
  return { commands: entry.agent.getSlashCommands() };
594
625
  }
626
+ getSubagentConfigurations(sessionId) {
627
+ const entry = this.requireSession(sessionId);
628
+ return { configurations: entry.agent.getSubagentConfigurations() };
629
+ }
595
630
  // ── File / History (global) ───────────────────────────────────
596
631
  async searchFiles(params, sessionId) {
597
632
  const files = await searchFiles(params.query, {
@@ -666,13 +701,6 @@ export class AgentBridge {
666
701
  // ── Auth (global) ────────────────────────────────────────────
667
702
  async getAuthStatus() {
668
703
  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
704
  await authService.checkAndRefreshTokenIfNeeded();
677
705
  return {
678
706
  isAuthenticated: authService.isSSOAuthenticated(),
@@ -868,8 +896,8 @@ export class AgentBridge {
868
896
  onUpdateBangMessage: (command, output, messageId) => {
869
897
  this.emit("bangMessageUpdated", { command, output, messageId }, ctx.registeredSessionId);
870
898
  },
871
- onCompleteBangMessage: (command, exitCode, messageId) => {
872
- this.emit("bangMessageCompleted", { command, exitCode, messageId }, ctx.registeredSessionId);
899
+ onCompleteBangMessage: (command, exitCode, messageId, output) => {
900
+ this.emit("bangMessageCompleted", { command, exitCode, messageId, output }, ctx.registeredSessionId);
873
901
  },
874
902
  onNotificationMessageAdded: (params) => {
875
903
  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" | "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.7",
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.7"
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
+ };
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useReducer } from "react";
1
+ import React, { useEffect, useReducer, useRef } from "react";
2
2
  import { Box, Text, useInput } from "ink";
3
3
  import type {
4
4
  PermissionDecision,
@@ -13,6 +13,7 @@ import {
13
13
  } from "wave-agent-sdk";
14
14
  import { confirmationReducer } from "../reducers/confirmationReducer.js";
15
15
  import { questionReducer } from "../reducers/questionReducer.js";
16
+ import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
16
17
 
17
18
  const getHeaderColor = (header: string) => {
18
19
  const colors = ["red", "green", "blue", "magenta", "cyan"] as const;
@@ -103,23 +104,37 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
103
104
  return "Yes, and auto-accept edits";
104
105
  };
105
106
 
107
+ const pasteDetectorRef = useRef(createBracketedPasteDetector());
108
+
106
109
  useInput((input, key) => {
107
110
  if (key.escape) {
108
111
  onCancel();
109
112
  return;
110
113
  }
111
114
 
115
+ const result = pasteDetectorRef.current.process(input);
116
+ if (result.kind === "consume") {
117
+ // Content of an in-flight bracketed paste: hold it, never submit.
118
+ return;
119
+ }
120
+ let cleanInput: string;
121
+ if (result.kind === "paste") {
122
+ cleanInput = (result.leadingInput ?? "") + result.text;
123
+ } else {
124
+ cleanInput = result.input;
125
+ }
126
+
112
127
  if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
113
128
  questionDispatch({
114
129
  type: "HANDLE_KEY",
115
- input,
130
+ input: cleanInput,
116
131
  key,
117
132
  questions,
118
133
  });
119
134
  } else {
120
135
  dispatch({
121
136
  type: "HANDLE_KEY",
122
- input,
137
+ input: cleanInput,
123
138
  key,
124
139
  toolName,
125
140
  toolInput,