wave-code 1.6.0 → 1.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,16 +29,14 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@anthropic-ai/sandbox-runtime": "^0.0.81",
32
- "@vscode/ripgrep": "^1.18.0",
33
- "qrcode": "^1.5.4",
34
- "sharp": "^0.35.4"
32
+ "qrcode": "^1.5.4"
35
33
  },
36
34
  "devDependencies": {
37
35
  "@types/qrcode": "^1.5.6",
38
36
  "@types/react": "^19.3.0",
39
37
  "@types/semver": "^7.7.1",
40
38
  "@types/yargs": "^17.0.0",
41
- "@vitest/coverage-v8": "^4.1.7",
39
+ "@vitest/coverage-v8": "^5.1.0",
42
40
  "chalk": "^5.6.2",
43
41
  "diff": "^8.0.2",
44
42
  "esbuild": "^0.28.1",
@@ -55,11 +53,11 @@
55
53
  "semver": "^7.7.4",
56
54
  "string-width": "^8.2.2",
57
55
  "tsx": "^4.20.4",
58
- "vitest": "^4.1.7",
56
+ "vitest": "^5.1.0",
59
57
  "wrap-ansi": "^10.0.0",
60
58
  "yargs": "^17.7.2",
61
59
  "zod": "^3.23.8",
62
- "wave-agent-sdk": "1.6.0"
60
+ "wave-agent-sdk": "1.6.2"
63
61
  },
64
62
  "engines": {
65
63
  "node": ">=22"
package/src/cli.tsx CHANGED
@@ -2,10 +2,51 @@ import React from "react";
2
2
  import { render } from "ink";
3
3
  import { ensureRuntimeDeps } from "wave-agent-sdk";
4
4
  import { App } from "./components/App.js";
5
+ import { getPendingAgentDestroy } from "./contexts/useChat.js";
5
6
  import { cleanupLogs } from "./utils/logger.js";
6
7
  import { describeWorktreeSurvival, removeWorktree } from "./utils/worktree.js";
7
8
  import { BaseAppProps } from "./types.js";
8
9
 
10
+ /**
11
+ * Upper bound on waiting for `Agent.destroy()` after the UI is gone.
12
+ *
13
+ * The teardown is bounded on its own side (its async-work drain has a timeout),
14
+ * but a wedged MCP child or LSP server must not turn closing the window into a
15
+ * process that never exits. Past this point the orphan risk is accepted in
16
+ * exchange for a terminal that actually comes back.
17
+ */
18
+ const AGENT_DESTROY_TIMEOUT_MS = 5_000;
19
+
20
+ /**
21
+ * Await the teardown that the unmount cleanup started.
22
+ *
23
+ * React effect cleanups are synchronous, so unmounting only *begins*
24
+ * `Agent.destroy()`; without this wait, `process.exit()` below terminates the
25
+ * process while the bash process trees, MCP children and LSP servers that
26
+ * `destroy()` was about to signal are still alive. Detached bash children then
27
+ * outlive their parent as launchd-reparented orphans.
28
+ */
29
+ async function awaitAgentDestroy(): Promise<void> {
30
+ const pending = getPendingAgentDestroy();
31
+ if (!pending) return;
32
+
33
+ let timeoutHandle: NodeJS.Timeout | undefined;
34
+ const timeout = new Promise<void>((resolve) => {
35
+ timeoutHandle = setTimeout(() => {
36
+ console.warn(
37
+ "Agent teardown timed out; exiting without waiting further.",
38
+ );
39
+ resolve();
40
+ }, AGENT_DESTROY_TIMEOUT_MS);
41
+ });
42
+
43
+ try {
44
+ await Promise.race([pending, timeout]);
45
+ } finally {
46
+ if (timeoutHandle) clearTimeout(timeoutHandle);
47
+ }
48
+ }
49
+
9
50
  export interface CliOptions extends BaseAppProps {
10
51
  restoreSessionId?: string;
11
52
  continueLastSession?: boolean;
@@ -40,8 +81,13 @@ export async function startCli(options: CliOptions): Promise<void> {
40
81
 
41
82
  // Continue with ink-based UI for normal mode
42
83
  let shouldRemoveWorktree = false;
84
+ let isExiting = false;
43
85
 
44
86
  const handleExit = (shouldRemove: boolean) => {
87
+ // A single close can arrive twice (SIGHUP plus the TTY-revocation poll, or
88
+ // an outside kill racing the keypress). Only the first one may unmount.
89
+ if (isExiting) return;
90
+ isExiting = true;
45
91
  shouldRemoveWorktree = shouldRemove;
46
92
  unmount();
47
93
  };
@@ -83,6 +129,10 @@ export async function startCli(options: CliOptions): Promise<void> {
83
129
  // Wait for the app to finish unmounting
84
130
  await waitUntilExit();
85
131
 
132
+ // ...which only *started* the agent teardown. Let it finish before exiting,
133
+ // otherwise the bash trees it was about to signal survive as orphans.
134
+ await awaitAgentDestroy();
135
+
86
136
  try {
87
137
  // Clean up old log files
88
138
  await cleanupLogs().catch((error) => {
@@ -1,9 +1,10 @@
1
- import React, { useState, useEffect, useCallback } from "react";
1
+ import React, { useState, useCallback } from "react";
2
2
  import { useInput } from "ink";
3
3
  import { ChatInterface } from "./ChatInterface.js";
4
4
  import { ChatProvider } from "../contexts/useChat.js";
5
5
  import { AppProvider } from "../contexts/useAppConfig.js";
6
6
  import { WorktreeExitPrompt } from "./WorktreeExitPrompt.js";
7
+ import { useExitSignals } from "../hooks/useExitSignals.js";
7
8
  import {
8
9
  hasUncommittedChanges,
9
10
  hasNewCommits,
@@ -58,18 +59,11 @@ const ChatWithExitPrompt: React.FC<{
58
59
  }
59
60
  });
60
61
 
61
- useEffect(() => {
62
- const onSigInt = () => handleSignal();
63
- const onSigTerm = () => handleSignal();
64
-
65
- process.on("SIGINT", onSigInt);
66
- process.on("SIGTERM", onSigTerm);
67
-
68
- return () => {
69
- process.off("SIGINT", onSigInt);
70
- process.off("SIGTERM", onSigTerm);
71
- };
72
- }, [handleSignal]);
62
+ // Terminal close / outside kill: the worktree prompt still applies, because
63
+ // the same uncommitted-changes check has to run for an exit nobody typed.
64
+ useExitSignals(() => {
65
+ void handleSignal();
66
+ });
73
67
 
74
68
  if (isExiting && worktreeStatus) {
75
69
  return (
@@ -116,6 +110,12 @@ const AppWithProviders: React.FC<AppWithProvidersProps> = ({
116
110
  }
117
111
  });
118
112
 
113
+ // An outside kill or a terminal close never reaches `useInput` — it arrives
114
+ // as a signal, or on macOS as nothing at all. With a worktree, the prompt
115
+ // component owns that handler (it has uncommitted changes to check first), so
116
+ // this registration stays out of the way.
117
+ useExitSignals(() => onExit(false), !worktreeSession);
118
+
119
119
  if (worktreeSession) {
120
120
  return (
121
121
  <ChatProvider
@@ -26,7 +26,8 @@ export const ChatInterface: React.FC = () => {
26
26
  isExpanded,
27
27
  sessionId,
28
28
  latestTotalTokens,
29
- maxInputTokens,
29
+ contextWindow,
30
+ maxOutputTokens,
30
31
  slashCommands,
31
32
  hasSlashCommand,
32
33
  hooks,
@@ -112,7 +113,8 @@ export const ChatInterface: React.FC = () => {
112
113
  hasSlashCommand={hasSlashCommand}
113
114
  hooks={hooks}
114
115
  latestTotalTokens={latestTotalTokens}
115
- maxInputTokens={maxInputTokens}
116
+ contextWindow={contextWindow}
117
+ maxOutputTokens={maxOutputTokens}
116
118
  showLoginHint={showLoginHint}
117
119
  />
118
120
  </>
@@ -6,6 +6,7 @@ import {
6
6
  selectorReducer,
7
7
  type SelectorState,
8
8
  } from "../reducers/selectorReducer.js";
9
+ import { toOneLine } from "../utils/oneLine.js";
9
10
 
10
11
  export interface CommandSelectorProps {
11
12
  searchQuery: string;
@@ -140,9 +141,11 @@ export const CommandSelector: React.FC<CommandSelectorProps> = ({
140
141
  {isSelected ? "▶ " : " "}/{command.id}
141
142
  </Text>
142
143
  {isSelected && (
143
- <Box marginLeft={4}>
144
- <Text color="gray" dimColor>
145
- {command.description}
144
+ <Box marginLeft={4} minHeight={1}>
145
+ {/* One line, always: the slot keeps the menu height stable
146
+ whatever the description's length. */}
147
+ <Text color="gray" dimColor wrap="truncate">
148
+ {toOneLine(command.description ?? "")}
146
149
  </Text>
147
150
  </Box>
148
151
  )}
@@ -5,6 +5,7 @@ import {
5
5
  selectorReducer,
6
6
  type SelectorState,
7
7
  } from "../reducers/selectorReducer.js";
8
+ import { toOneLine } from "../utils/oneLine.js";
8
9
 
9
10
  export type { FileSelectorItem };
10
11
 
@@ -119,15 +120,25 @@ export const FileSelector: React.FC<FileSelectorProps> = ({
119
120
 
120
121
  return (
121
122
  <Box key={fileItem.path}>
122
- <Text
123
- color={isSelected ? "black" : "white"}
124
- backgroundColor={isSelected ? "cyan" : undefined}
125
- >
126
- {isSelected ? "▶ " : " "}
127
- {fileItem.agentName ? `@${fileItem.agentName}` : fileItem.path}
128
- </Text>
123
+ {/* The name keeps its width; the description gives way. */}
124
+ <Box flexShrink={0}>
125
+ <Text
126
+ color={isSelected ? "black" : "white"}
127
+ backgroundColor={isSelected ? "cyan" : undefined}
128
+ >
129
+ {isSelected ? "▶ " : " "}
130
+ {fileItem.agentName
131
+ ? `@${fileItem.agentName}`
132
+ : fileItem.path}
133
+ </Text>
134
+ </Box>
129
135
  {fileItem.agentDescription && (
130
- <Text dimColor> {fileItem.agentDescription}</Text>
136
+ <Box flexShrink={1}>
137
+ <Text dimColor wrap="truncate">
138
+ {" "}
139
+ {toOneLine(fileItem.agentDescription)}
140
+ </Text>
141
+ </Box>
131
142
  )}
132
143
  </Box>
133
144
  );
@@ -63,7 +63,8 @@ export interface InputBoxProps {
63
63
  >;
64
64
  // Token usage
65
65
  latestTotalTokens?: number;
66
- maxInputTokens?: number;
66
+ contextWindow?: number;
67
+ maxOutputTokens?: number;
67
68
  // Login hint
68
69
  showLoginHint?: boolean;
69
70
  }
@@ -81,7 +82,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
81
82
  hasSlashCommand = () => false,
82
83
  hooks = {},
83
84
  latestTotalTokens = 0,
84
- maxInputTokens = 200000,
85
+ contextWindow = 200000,
86
+ maxOutputTokens = 32000,
85
87
  showLoginHint = false,
86
88
  }) => {
87
89
  const {
@@ -509,7 +511,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
509
511
  />
510
512
  <Notifications
511
513
  latestTotalTokens={latestTotalTokens}
512
- maxInputTokens={maxInputTokens}
514
+ contextWindow={contextWindow}
515
+ maxOutputTokens={maxOutputTokens}
513
516
  showLoginHint={showLoginHint}
514
517
  pluginHint={pluginHint}
515
518
  />
@@ -1,9 +1,11 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
+ import { percentOf } from "../remote/remoteNotifications.js";
3
4
 
4
5
  export interface NotificationsProps {
5
6
  latestTotalTokens?: number;
6
- maxInputTokens?: number;
7
+ contextWindow?: number;
8
+ maxOutputTokens?: number;
7
9
  showLoginHint?: boolean;
8
10
  /**
9
11
  * 一次性中性提示(插件变更待应用 / 重载完成)。由宿主显式设置并在数秒后清除,
@@ -14,13 +16,14 @@ export interface NotificationsProps {
14
16
 
15
17
  export const Notifications: React.FC<NotificationsProps> = ({
16
18
  latestTotalTokens = 0,
17
- maxInputTokens = 200000,
19
+ contextWindow = 200000,
20
+ maxOutputTokens = 32000,
18
21
  showLoginHint = false,
19
22
  pluginHint = null,
20
23
  }) => {
21
24
  const percentage =
22
25
  latestTotalTokens > 0
23
- ? Math.min(Math.round((latestTotalTokens / maxInputTokens) * 100), 100)
26
+ ? percentOf(latestTotalTokens, contextWindow, maxOutputTokens)
24
27
  : 0;
25
28
 
26
29
  const contextColor =
@@ -101,7 +101,8 @@ export interface ChatContextType {
101
101
  recallQueuedMessage: () => QueuedMessage | null;
102
102
  removeQueuedMessageById: (id: string) => boolean;
103
103
  latestTotalTokens: number;
104
- maxInputTokens: number;
104
+ contextWindow: number;
105
+ maxOutputTokens: number;
105
106
  // Model functionality
106
107
  currentModel: string;
107
108
  configuredModels: string[];
@@ -202,6 +203,23 @@ export interface ChatContextType {
202
203
 
203
204
  const ChatContext = createContext<ChatContextType | null>(null);
204
205
 
206
+ /**
207
+ * Handoff for the agent teardown that React unmount can only *start*.
208
+ *
209
+ * A React effect cleanup is synchronous: it cannot await `Agent.destroy()`,
210
+ * whose teardown (bash process trees, MCP children, LSP, async work drain) is
211
+ * async. `cli.tsx` used to call `process.exit(0)` the moment `waitUntilExit()`
212
+ * resolved, which cut that teardown short — the detached bash process group
213
+ * never received its SIGTERM and survived as an orphan. The promise is parked
214
+ * here so the exit path can await it before exiting.
215
+ */
216
+ let pendingAgentDestroy: Promise<void> | null = null;
217
+
218
+ /** The in-flight teardown started by the unmount cleanup, if any. */
219
+ export function getPendingAgentDestroy(): Promise<void> | null {
220
+ return pendingAgentDestroy;
221
+ }
222
+
205
223
  export const useChat = () => {
206
224
  const context = useContext(ChatContext);
207
225
  if (!context) {
@@ -425,7 +443,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
425
443
 
426
444
  const [messages, setMessages] = useState<Message[]>([]);
427
445
  const [latestTotalTokens, setLatestTotalTokens] = useState(0);
428
- const [maxInputTokens, setMaxInputTokens] = useState(200000);
446
+ const [contextWindow, setContextWindow] = useState(200000);
447
+ const [maxOutputTokens, setMaxOutputTokens] = useState(32000);
429
448
 
430
449
  // All message-state updates apply IMMEDIATELY via setMessages — no data-layer
431
450
  // throttling (removed 2026-10-15 after observing fully-immediate rendering in
@@ -921,7 +940,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
921
940
  setWorkingDirectory(agent.currentWorkdir);
922
941
  setCurrentModelState(agent.getModelConfig().model || "");
923
942
  setConfiguredModels(agent.getConfiguredModels().map((m) => m.name));
924
- setMaxInputTokens(agent.getMaxInputTokens());
943
+ setContextWindow(agent.getContextWindow());
944
+ setMaxOutputTokens(agent.getMaxOutputTokens?.() ?? 32_000);
925
945
 
926
946
  // Get initial MCP servers state
927
947
  const initialMcpServers = agent.getMcpServers?.() || [];
@@ -1030,16 +1050,23 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1030
1050
  useEffect(() => {
1031
1051
  return () => {
1032
1052
  if (agentRef.current) {
1053
+ const agent = agentRef.current;
1033
1054
  try {
1034
1055
  // Display usage summary before cleanup
1035
- const usages = agentRef.current.usages;
1036
- const sessionFilePath = agentRef.current.sessionFilePath;
1056
+ const usages = agent.usages;
1057
+ const sessionFilePath = agent.sessionFilePath;
1037
1058
  displayUsageSummary(usages, sessionFilePath);
1038
1059
  } catch {
1039
1060
  // Silently ignore usage summary errors during cleanup
1040
1061
  }
1041
1062
 
1042
- agentRef.current.destroy();
1063
+ // Park the promise instead of dropping it: the exit path in `cli.tsx`
1064
+ // awaits it before `process.exit()` so the teardown actually runs
1065
+ // (see `pendingAgentDestroy`). Failures are swallowed so a broken
1066
+ // teardown cannot turn a clean exit into an unhandled rejection.
1067
+ pendingAgentDestroy = agent.destroy().catch((error: unknown) => {
1068
+ logger.warn("Agent teardown failed during exit:", error);
1069
+ });
1043
1070
  }
1044
1071
  };
1045
1072
  }, []);
@@ -1456,7 +1483,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1456
1483
  isLoading,
1457
1484
  permissionMode,
1458
1485
  latestTotalTokens,
1459
- maxInputTokens,
1486
+ contextWindow,
1487
+ maxOutputTokens,
1460
1488
  // 手机侧会话列表的标题材料:`customTitle` 不走 React state(agent 自己存着,
1461
1489
  // 重命名后即刻生效),所以按「取当前值」的函数递给门面。
1462
1490
  customTitle: () => agentRef.current?.customTitle,
@@ -1481,8 +1509,10 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1481
1509
  renameSession: (title) => agentRef.current?.renameSession(title),
1482
1510
  compact: (instructions) => compact(instructions),
1483
1511
  slashCommands: () => agentRef.current?.getSlashCommands() ?? [],
1484
- // 手机页取首屏尾段 / 补差都读 agent 手上那份列表(压缩后的上下文);它这一侧
1485
- // 不提供上翻,所以不会带来 `before` 锚点。
1512
+ // 手机页取数都读 agent 手上那份列表(压缩后的上下文):首屏尾段(`limit`)、
1513
+ // 重连补差(`since`)、滚到顶往回翻(`before` + `limit`)三种窗口原样交给
1514
+ // SDK,切片与 `hasMore` / `anchorLost` 都由它判。分页是 2026-11-06 起远控页
1515
+ // 也有的能力,所以这里不能只认前两种。
1486
1516
  messagePage: (window) =>
1487
1517
  agentRef.current?.getMessagePage(window) ??
1488
1518
  Promise.resolve({ messages: [] }),
@@ -1603,7 +1633,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
1603
1633
  recallQueuedMessage,
1604
1634
  removeQueuedMessageById,
1605
1635
  latestTotalTokens,
1606
- maxInputTokens,
1636
+ contextWindow,
1637
+ maxOutputTokens,
1607
1638
  currentModel,
1608
1639
  configuredModels,
1609
1640
  getConfiguredModels,
@@ -0,0 +1,83 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Process-level exit detection for the interactive CLI.
5
+ *
6
+ * Ink runs the terminal in raw mode, so Ctrl-C normally arrives as a keypress
7
+ * (`useInput`) rather than a signal — but the signals still arrive when the
8
+ * process is killed from outside, and the terminal itself sends one when the
9
+ * user closes the window. Every one of those paths has to unwind through React
10
+ * unmount, because that is what triggers `Agent.destroy()`; without a handler
11
+ * Node applies the default action and terminates on the spot, leaving the
12
+ * agent's async teardown — and every child process it owns — unreaped.
13
+ *
14
+ * `SIGHUP` is the one that actually bit us. The Bash tool spawns its shell with
15
+ * `detached: true`, which makes the child the leader of its own process group
16
+ * and therefore immune to the terminal's SIGHUP broadcast. If nothing reaps it,
17
+ * the shell and everything under it survive as launchd-reparented orphans
18
+ * (observed: one `zsh -> ping -> tail` group alive for almost three days).
19
+ * Claude Code registers the same signal for the same reason.
20
+ *
21
+ * The polling branch covers macOS specifically: closing a terminal there
22
+ * revokes the TTY file descriptors *instead of* delivering a signal, leaving
23
+ * the process alive but unable to read or write. Nothing else would ever
24
+ * notice, so the stdio handles are probed on the same 30s cadence Claude Code
25
+ * uses in `gracefulShutdown`.
26
+ */
27
+ const TTY_REVOCATION_CHECK_INTERVAL_MS = 30_000;
28
+
29
+ /**
30
+ * Invoke `onExitSignal` on SIGINT / SIGTERM / SIGHUP, and when the controlling
31
+ * TTY is revoked out from under the process.
32
+ *
33
+ * `onExitSignal` is held in a ref, so callers do not need to memoize it and a
34
+ * re-render does not churn the signal registrations. Callers are responsible
35
+ * for making the teardown itself idempotent: a single terminal close can arrive
36
+ * as a signal *and* as a revoked TTY.
37
+ *
38
+ * @param enabled - set false to skip registration entirely (e.g. a wrapper
39
+ * component that renders a child which owns the handler).
40
+ */
41
+ export function useExitSignals(onExitSignal: () => void, enabled = true): void {
42
+ const handlerRef = useRef(onExitSignal);
43
+ handlerRef.current = onExitSignal;
44
+
45
+ useEffect(() => {
46
+ if (!enabled) return;
47
+
48
+ const handle = () => handlerRef.current();
49
+
50
+ // Windows has no SIGHUP: the event never fires there, so registering it
51
+ // would only imply a coverage the platform cannot provide.
52
+ const signals: NodeJS.Signals[] =
53
+ process.platform === "win32"
54
+ ? ["SIGINT", "SIGTERM"]
55
+ : ["SIGINT", "SIGTERM", "SIGHUP"];
56
+ for (const signal of signals) {
57
+ process.on(signal, handle);
58
+ }
59
+
60
+ // Only meaningful for an interactive terminal; piping either stream out
61
+ // means the handles legitimately stop being readable/writable.
62
+ let ttyRevocationPoll: NodeJS.Timeout | undefined;
63
+ if (process.stdin.isTTY) {
64
+ ttyRevocationPoll = setInterval(() => {
65
+ if (!process.stdout.writable || !process.stdin.readable) {
66
+ // Stop polling before handing off: the revoked TTY stays revoked, so
67
+ // a surviving interval would re-fire the teardown every 30s.
68
+ if (ttyRevocationPoll) clearInterval(ttyRevocationPoll);
69
+ handle();
70
+ }
71
+ }, TTY_REVOCATION_CHECK_INTERVAL_MS);
72
+ // Never keep the process alive just to poll for its own death.
73
+ ttyRevocationPoll.unref();
74
+ }
75
+
76
+ return () => {
77
+ for (const signal of signals) {
78
+ process.off(signal, handle);
79
+ }
80
+ if (ttyRevocationPoll) clearInterval(ttyRevocationPoll);
81
+ };
82
+ }, [enabled]);
83
+ }
@@ -61,7 +61,8 @@ export interface UseRemoteControlOptions {
61
61
  isLoading: boolean;
62
62
  permissionMode: PermissionMode;
63
63
  latestTotalTokens: number;
64
- maxInputTokens: number;
64
+ contextWindow: number;
65
+ maxOutputTokens: number;
65
66
  /**
66
67
  * The title `/rename` sets, read live off the agent — the page's session list
67
68
  * labels the row with it (and the page itself may be the one that renamed it).
@@ -194,10 +195,20 @@ export function useRemoteControl(
194
195
  if (!sink || !sessionId) return;
195
196
  sink.sendNotification(
196
197
  "contextUsage",
197
- remoteWire.contextUsage(latestTotalTokens, options.maxInputTokens),
198
+ remoteWire.contextUsage(
199
+ latestTotalTokens,
200
+ options.contextWindow,
201
+ options.maxOutputTokens,
202
+ ),
198
203
  sessionId,
199
204
  );
200
- }, [sink, sessionId, latestTotalTokens, options.maxInputTokens]);
205
+ }, [
206
+ sink,
207
+ sessionId,
208
+ latestTotalTokens,
209
+ options.contextWindow,
210
+ options.maxOutputTokens,
211
+ ]);
201
212
 
202
213
  // Approvals are announced once, when they appear. The page answers with
203
214
  // `permissionResponse`, which the host routes back into the same promise the
@@ -318,7 +329,8 @@ function adapterOf(latest: {
318
329
  contextUsagePercent: () =>
319
330
  percentOf(
320
331
  latest.current.latestTotalTokens,
321
- latest.current.maxInputTokens,
332
+ latest.current.contextWindow,
333
+ latest.current.maxOutputTokens,
322
334
  ),
323
335
  latestTotalTokens: () => latest.current.latestTotalTokens,
324
336
  permissionMode: () => latest.current.permissionMode,
@@ -39,7 +39,7 @@ export interface FileSelectorItem {
39
39
  type: "file" | "directory" | "agent";
40
40
  /** 子代理名(`type === "agent"` 时有值),也是插入正文的名字。 */
41
41
  agentName?: string;
42
- /** 子代理定义的 description,作为条目的第二行说明。 */
42
+ /** 子代理定义的 description,接在名字后面同一行显示;超宽截断成一行。 */
43
43
  agentDescription?: string;
44
44
  }
45
45