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.
- package/dist/components/AgentsManager.d.ts +7 -0
- package/dist/components/AgentsManager.js +109 -0
- package/dist/components/ConfirmationSelector.js +17 -3
- package/dist/components/InputBox.js +7 -21
- package/dist/components/LoginCommand.js +31 -2
- package/dist/components/MarketplaceAddForm.js +16 -2
- package/dist/constants/commands.js +6 -0
- package/dist/contexts/useChat.d.ts +2 -1
- package/dist/contexts/useChat.js +15 -2
- package/dist/hooks/useInputManager.d.ts +2 -0
- package/dist/hooks/useInputManager.js +8 -0
- package/dist/managers/inputHandlers.js +3 -0
- package/dist/managers/inputReducer.d.ts +4 -0
- package/dist/managers/inputReducer.js +8 -0
- package/dist/reducers/agentsManagerReducer.d.ts +26 -0
- package/dist/reducers/agentsManagerReducer.js +54 -0
- package/dist/stdio/agentBridge.d.ts +3 -0
- package/dist/stdio/agentBridge.js +38 -10
- package/dist/stdio/protocol.d.ts +1 -1
- package/dist/utils/usageSummary.d.ts +0 -4
- package/dist/utils/usageSummary.js +1 -34
- package/package.json +2 -2
- package/src/components/AgentsManager.tsx +290 -0
- package/src/components/ConfirmationSelector.tsx +18 -3
- package/src/components/InputBox.tsx +54 -45
- package/src/components/LoginCommand.tsx +35 -2
- package/src/components/MarketplaceAddForm.tsx +17 -2
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +23 -2
- package/src/hooks/useInputManager.ts +8 -0
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +10 -0
- package/src/reducers/agentsManagerReducer.ts +91 -0
- package/src/stdio/agentBridge.ts +47 -9
- package/src/stdio/protocol.ts +2 -0
- 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" &&
|
package/dist/stdio/protocol.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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,
|