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.
- package/dist/components/AgentsManager.d.ts +7 -0
- package/dist/components/AgentsManager.js +109 -0
- package/dist/components/ChatInterface.js +1 -1
- package/dist/components/ConfirmationDetails.d.ts +1 -0
- package/dist/components/ConfirmationDetails.js +5 -3
- 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/components/RewindCommand.js +11 -4
- package/dist/constants/commands.js +6 -0
- package/dist/contexts/useChat.d.ts +18 -2
- package/dist/contexts/useChat.js +114 -9
- package/dist/daemon/commands.d.ts +49 -0
- package/dist/daemon/commands.js +341 -0
- package/dist/daemon/jsonRpcClient.d.ts +38 -0
- package/dist/daemon/jsonRpcClient.js +129 -0
- package/dist/daemon/socketClient.d.ts +13 -0
- package/dist/daemon/socketClient.js +26 -0
- package/dist/hooks/useInputManager.d.ts +2 -0
- package/dist/hooks/useInputManager.js +8 -0
- package/dist/index.js +88 -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 +15 -0
- package/dist/stdio/agentBridge.js +101 -20
- 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/ChatInterface.tsx +2 -0
- package/src/components/ConfirmationDetails.tsx +6 -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/components/RewindCommand.tsx +10 -4
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +146 -7
- package/src/daemon/commands.ts +444 -0
- package/src/daemon/jsonRpcClient.ts +158 -0
- package/src/daemon/socketClient.ts +34 -0
- package/src/hooks/useInputManager.ts +8 -0
- package/src/index.ts +130 -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 +123 -19
- package/src/stdio/protocol.ts +4 -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) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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" &&
|
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" | "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.
|
|
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.
|
|
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}
|