wave-code 1.6.0 → 1.6.1
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/README.md +3 -2
- package/dist/bundle/wave.mjs +656 -642
- package/package.json +3 -5
- package/src/cli.tsx +50 -0
- package/src/components/App.tsx +13 -13
- package/src/components/ChatInterface.tsx +4 -2
- package/src/components/InputBox.tsx +6 -3
- package/src/components/Notifications.tsx +6 -3
- package/src/contexts/useChat.tsx +41 -10
- package/src/hooks/useExitSignals.ts +83 -0
- package/src/hooks/useRemoteControl.ts +16 -4
- package/src/remote/remoteControlCliHost.ts +20 -4
- package/src/remote/remoteControlService.ts +8 -2
- package/src/remote/remoteNotifications.ts +26 -5
- package/src/stdio/agentBridge.ts +107 -7
- package/src/stdio/startupStage.ts +22 -0
- package/src/stdio-cli.ts +9 -1
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
updateUserPreferenceSettings,
|
|
61
61
|
type UserPreferenceSettings,
|
|
62
62
|
type UserPreferenceSettingsView,
|
|
63
|
+
type AccountCredential,
|
|
63
64
|
type SubagentConfiguration,
|
|
64
65
|
type SkillMetadata,
|
|
65
66
|
type PluginReloadResult,
|
|
@@ -838,6 +839,25 @@ export class AgentBridge {
|
|
|
838
839
|
if (entry) {
|
|
839
840
|
this.pendingPermissions.delete(p.requestId);
|
|
840
841
|
entry.resolve(p.decision);
|
|
842
|
+
// 不带反馈的拒绝 = 用户关掉了确认卡(关闭键 / Esc),本轮在用户这边就
|
|
843
|
+
// 结束:与桌面端 / VS Code / JetBrains 的 `denied → abortMessage()` 是
|
|
844
|
+
// 同一条语义,也与 Claude Code 的 `cancelAndAbort`(无 feedback 直接
|
|
845
|
+
// abort 整轮)一致。**带 message 的拒绝不中断**——那是用户给出理由后
|
|
846
|
+
// 拒绝这一次工具调用,模型要照反馈继续或改问(spec
|
|
847
|
+
// ui/confirm-ui.md 场景 2 / 5、remote/remote-web.md 场景 24)。
|
|
848
|
+
// 中断本身不阻塞回包,失败(会话正被重建 / 已销毁)也不影响这次拒绝
|
|
849
|
+
// 已经生效,因此静默。
|
|
850
|
+
const rejectionFeedback = p.decision?.message?.trim();
|
|
851
|
+
if (
|
|
852
|
+
p.decision?.behavior === "deny" &&
|
|
853
|
+
!rejectionFeedback &&
|
|
854
|
+
entry.sessionId
|
|
855
|
+
) {
|
|
856
|
+
const session = this.sessions.get(entry.sessionId);
|
|
857
|
+
if (session && !session.transitioning) {
|
|
858
|
+
void session.agent.abortMessage();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
841
861
|
}
|
|
842
862
|
}
|
|
843
863
|
}
|
|
@@ -1345,11 +1365,12 @@ export class AgentBridge {
|
|
|
1345
1365
|
*/
|
|
1346
1366
|
private emitContextUsage(agent: Agent): void {
|
|
1347
1367
|
const tokens = agent.latestTotalTokens;
|
|
1348
|
-
const max = agent.
|
|
1368
|
+
const max = agent.getContextWindow();
|
|
1369
|
+
const maxOutput = agent.getMaxOutputTokens?.() ?? 32_000;
|
|
1349
1370
|
if (tokens > 0 && max > 0) {
|
|
1350
1371
|
this.emit(
|
|
1351
1372
|
"contextUsage",
|
|
1352
|
-
remoteWire.contextUsage(tokens, max),
|
|
1373
|
+
remoteWire.contextUsage(tokens, max, maxOutput),
|
|
1353
1374
|
agent.sessionId,
|
|
1354
1375
|
);
|
|
1355
1376
|
}
|
|
@@ -1364,9 +1385,10 @@ export class AgentBridge {
|
|
|
1364
1385
|
*/
|
|
1365
1386
|
private contextUsagePercentOf(agent: Agent): number | undefined {
|
|
1366
1387
|
const tokens = agent.latestTotalTokens;
|
|
1367
|
-
const max = agent.
|
|
1388
|
+
const max = agent.getContextWindow();
|
|
1389
|
+
const maxOutput = agent.getMaxOutputTokens?.() ?? 32_000;
|
|
1368
1390
|
if (tokens > 0 && max > 0) {
|
|
1369
|
-
return percentOf(tokens, max);
|
|
1391
|
+
return percentOf(tokens, max, maxOutput);
|
|
1370
1392
|
}
|
|
1371
1393
|
return undefined;
|
|
1372
1394
|
}
|
|
@@ -2790,20 +2812,77 @@ export class AgentBridge {
|
|
|
2790
2812
|
}
|
|
2791
2813
|
|
|
2792
2814
|
// ── Auth (global) ────────────────────────────────────────────
|
|
2815
|
+
/**
|
|
2816
|
+
* 认证状态 + **凭据形态**(spec desktop-account.md「账户卡片 · 认证方式」)。
|
|
2817
|
+
*
|
|
2818
|
+
* `isAuthenticated` 只回答「有没有 SSO token」;宿主据此渲染卡片与发送入口会
|
|
2819
|
+
* 把「key 写在 settings.json 里」的人判成没有凭据(卡片显示登录按钮、输入框
|
|
2820
|
+
* 被锁)。故同一次查询里一并给出 `credential`,优先级与 SDK 的
|
|
2821
|
+
* `resolveGatewayConfig` 对齐:企业账号 > 自定义网关 > 无凭据。
|
|
2822
|
+
*/
|
|
2793
2823
|
private async getAuthStatus(): Promise<{
|
|
2794
2824
|
isAuthenticated: boolean;
|
|
2825
|
+
credential: AccountCredential;
|
|
2795
2826
|
user: { id: string; email?: string } | undefined;
|
|
2796
2827
|
serverUrl: string;
|
|
2797
2828
|
}> {
|
|
2798
2829
|
const authService = AuthService.getInstance();
|
|
2799
2830
|
await authService.checkAndRefreshTokenIfNeeded();
|
|
2831
|
+
const isAuthenticated = authService.isSSOAuthenticated();
|
|
2800
2832
|
return {
|
|
2801
|
-
isAuthenticated
|
|
2833
|
+
isAuthenticated,
|
|
2834
|
+
credential: this.resolveCredential(isAuthenticated),
|
|
2802
2835
|
user: authService.getAuthUser(),
|
|
2803
2836
|
serverUrl: authService.getServerUrl(),
|
|
2804
2837
|
};
|
|
2805
2838
|
}
|
|
2806
2839
|
|
|
2840
|
+
/**
|
|
2841
|
+
* 凭据形态。**判据只有这一处**——消费方(宿主 / webview)不得自己读配置推断。
|
|
2842
|
+
*
|
|
2843
|
+
* 网关层的取值顺序与 `resolveGatewayConfig` 一致:
|
|
2844
|
+
* - 无 SSO:先问一个活着的会话的 `getGatewayConfig()`(与真实请求同源,已合并
|
|
2845
|
+
* 用户 / 项目 / 远端三层 `env`),没有会话时退回「用户级 settings.json 的
|
|
2846
|
+
* `env`(`loadUserConfigEnv`)→ OS 环境变量」。
|
|
2847
|
+
* - 有 SSO:**不能**用 `getGatewayConfig()`(它此时返回 SSO 凭据),回落的那份
|
|
2848
|
+
* 网关只能从配置层读——卡片要靠它给「退出登录」项写后果。
|
|
2849
|
+
*
|
|
2850
|
+
* 注意 `setEnvironmentVars` 只把 `WAVE_SERVER_URL` 镜像进 `process.env`,网关
|
|
2851
|
+
* 凭据留在会话快照里,所以只看 `process.env` 会把配好的人判成无凭据。
|
|
2852
|
+
*/
|
|
2853
|
+
private resolveCredential(isAuthenticated: boolean): AccountCredential {
|
|
2854
|
+
const gateway = isAuthenticated
|
|
2855
|
+
? this.configuredGatewayCredential()
|
|
2856
|
+
: (this.sessionGatewayCredential() ?? this.configuredGatewayCredential());
|
|
2857
|
+
if (isAuthenticated) {
|
|
2858
|
+
// 企业账号生效中;`gateway` 仅表示「退出登录后会落到的那份」。
|
|
2859
|
+
return gateway ? { kind: "sso", gateway } : { kind: "sso" };
|
|
2860
|
+
}
|
|
2861
|
+
return gateway ? { kind: "gateway", gateway } : { kind: "none" };
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
/** 活着的会话真正解析出来的网关配置(无 SSO 时它就是要发出去的那一份)。 */
|
|
2865
|
+
private sessionGatewayCredential(): { host: string | null } | null {
|
|
2866
|
+
for (const entry of this.sessions.values()) {
|
|
2867
|
+
const gateway = entry.agent.getGatewayConfig();
|
|
2868
|
+
if (gateway.apiKey) return { host: hostOfBaseUrl(gateway.baseURL) };
|
|
2869
|
+
}
|
|
2870
|
+
return null;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
/**
|
|
2874
|
+
* 配置层的网关凭据:用户级 settings.json 的 `env` 优先于 OS 环境变量(与
|
|
2875
|
+
* `resolveGatewayConfig` 的 `env(settings.json)> process.env` 同序)。
|
|
2876
|
+
*/
|
|
2877
|
+
private configuredGatewayCredential(): { host: string | null } | null {
|
|
2878
|
+
const userEnv = loadUserConfigEnv();
|
|
2879
|
+
const pick = (key: "WAVE_API_KEY" | "WAVE_BASE_URL"): string | undefined =>
|
|
2880
|
+
userEnv[key]?.trim() || process.env[key]?.trim() || undefined;
|
|
2881
|
+
const apiKey = pick("WAVE_API_KEY");
|
|
2882
|
+
if (!apiKey) return null;
|
|
2883
|
+
return { host: hostOfBaseUrl(pick("WAVE_BASE_URL")) };
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2807
2886
|
private async getAccountInfo(): Promise<{
|
|
2808
2887
|
/**
|
|
2809
2888
|
* 计费结论(套餐两根条 + 到期日 + 降级原因),形状见 codechat `GET /api/v1/account` → `billing`
|
|
@@ -3285,11 +3364,12 @@ export class AgentBridge {
|
|
|
3285
3364
|
// request (fresh total) and on session restore (the persisted total),
|
|
3286
3365
|
// so the webview button shows real usage without waiting for a turn —
|
|
3287
3366
|
// aligned with the CLI, which reads the same callback (spec 场景 5/6).
|
|
3288
|
-
const max = ctx.agent?.
|
|
3367
|
+
const max = ctx.agent?.getContextWindow() ?? 0;
|
|
3368
|
+
const maxOutput = ctx.agent?.getMaxOutputTokens?.() ?? 32_000;
|
|
3289
3369
|
if (max > 0) {
|
|
3290
3370
|
this.emit(
|
|
3291
3371
|
"contextUsage",
|
|
3292
|
-
remoteWire.contextUsage(tokens, max),
|
|
3372
|
+
remoteWire.contextUsage(tokens, max, maxOutput),
|
|
3293
3373
|
ctx.registeredSessionId,
|
|
3294
3374
|
);
|
|
3295
3375
|
}
|
|
@@ -3355,6 +3435,13 @@ export class AgentBridge {
|
|
|
3355
3435
|
onMcpServersChange: (servers: McpServerStatus[]) => {
|
|
3356
3436
|
this.emit("mcpServersChange", { servers }, ctx.registeredSessionId);
|
|
3357
3437
|
},
|
|
3438
|
+
onQuotaHeaders: (headers: Record<string, string>) => {
|
|
3439
|
+
// 额度响应头(codechat proxy 每次模型调用都发,spec:
|
|
3440
|
+
// desktop/desktop-account.md「额度响应头」):桌面端宿主据此刷新账户卡片的
|
|
3441
|
+
// 用量读数,是 60s 轮询删除后的主要刷新源。原样透传 —— 字段解析与映射都
|
|
3442
|
+
// 在消费方。不进 remoteWire:远程页不渲染账户卡片。
|
|
3443
|
+
this.emit("quotaHeaders", { headers }, ctx.registeredSessionId);
|
|
3444
|
+
},
|
|
3358
3445
|
onNotificationMessageAdded: (params) => {
|
|
3359
3446
|
// The LAST matching message, not the first: one task can notify more
|
|
3360
3447
|
// than once under the same task id (a workflow warns about its size
|
|
@@ -3488,6 +3575,19 @@ function isSessionRecoveryError(error: unknown): boolean {
|
|
|
3488
3575
|
);
|
|
3489
3576
|
}
|
|
3490
3577
|
|
|
3578
|
+
/**
|
|
3579
|
+
* `baseURL` → 网关主机名(`api.example.com`)。缺省 / 不可解析一律 null(卡片读
|
|
3580
|
+
* 「默认端点」)——这里只做展示,不做任何可用性判断。
|
|
3581
|
+
*/
|
|
3582
|
+
function hostOfBaseUrl(baseURL: string | undefined): string | null {
|
|
3583
|
+
if (!baseURL) return null;
|
|
3584
|
+
try {
|
|
3585
|
+
return new URL(baseURL).host || null;
|
|
3586
|
+
} catch {
|
|
3587
|
+
return null;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3491
3591
|
/** Read a text file, returning "" when it does not exist. */
|
|
3492
3592
|
async function readTextFileSafe(filePath: string): Promise<string> {
|
|
3493
3593
|
try {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 首次启动的进度上报(spec `ui/stdio-transport.md`「首次启动的进度文案」)。
|
|
3
|
+
*
|
|
4
|
+
* 走的是同一条 stdout 上的 JSON-RPC 通知通道(一条一行),只是没有 `sessionId`
|
|
5
|
+
* ——这些阶段发生在任何会话存在之前。宿主侧 `NotificationRouter` 的全局处理器
|
|
6
|
+
* 收下它,把它变成加载动画下方那一行文案。
|
|
7
|
+
*
|
|
8
|
+
* **旁路**:通知发不出去(stdout 已关、宿主已经退出)不是启动错误,写失败一律
|
|
9
|
+
* 吞掉;上报也不参与任何完成条件,没有宿主消费时启动照旧。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StartupStage } from "wave-agent-sdk";
|
|
13
|
+
|
|
14
|
+
export function reportStartupStage(stage: StartupStage): void {
|
|
15
|
+
try {
|
|
16
|
+
process.stdout.write(
|
|
17
|
+
`${JSON.stringify({ method: "startupStage", params: { stage } })}\n`,
|
|
18
|
+
);
|
|
19
|
+
} catch {
|
|
20
|
+
// 见文件头:上报失败不影响启动。
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/stdio-cli.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { ensureRuntimeDeps, setTelemetryStdoutProtected } from "wave-agent-sdk";
|
|
10
10
|
import { StdioServer } from "./stdio/stdioServer.js";
|
|
11
|
+
import { reportStartupStage } from "./stdio/startupStage.js";
|
|
11
12
|
import { createRemoteControlSinkFactory } from "./remote/remoteControlSink.js";
|
|
12
13
|
import { logger } from "./utils/logger.js";
|
|
13
14
|
|
|
@@ -64,7 +65,11 @@ export async function startStdioCli(): Promise<void> {
|
|
|
64
65
|
// answering any RPC (see cli.tsx). Blocking here is what closes the race: the
|
|
65
66
|
// host's `initialize` — and with it the webview's initial state — only lands
|
|
66
67
|
// once a pasted image can be handled. Failures are non-fatal.
|
|
67
|
-
|
|
68
|
+
//
|
|
69
|
+
// The ~25MB this can pull is the longest silent stretch of a first launch, so
|
|
70
|
+
// it is reported (a warm cache reports nothing: `ensureRuntimeDeps` only
|
|
71
|
+
// fires `onStage` when something is actually missing).
|
|
72
|
+
await ensureRuntimeDeps({ onStage: reportStartupStage });
|
|
68
73
|
// Registered here (stdio mode only): the interactive CLI keeps Node's
|
|
69
74
|
// default behavior so the terminal shows the crash stack directly.
|
|
70
75
|
process.on("uncaughtException", (error) =>
|
|
@@ -79,6 +84,9 @@ export async function startStdioCli(): Promise<void> {
|
|
|
79
84
|
// same machine — is what serves a paired phone.
|
|
80
85
|
createRemoteSink: createRemoteControlSinkFactory(process.cwd()),
|
|
81
86
|
});
|
|
87
|
+
// Last startup stage: from here the process is up and what the user is waiting
|
|
88
|
+
// for is `initialize` (and, on the restore path, the transcript replay).
|
|
89
|
+
reportStartupStage("startingSession");
|
|
82
90
|
server.start();
|
|
83
91
|
|
|
84
92
|
// Keep the process alive — readline on stdin handles the event loop.
|