refactor(core,cli): rename generic Gemini identifiers to Llm (#10124)

* refactor(core,cli): rename Gemini residue in memory/spinner/leaf ids

PR 1 of #4063 item 6 (de-Google naming). Renames three independent families plus the leaf LLM types:

- Memory filename: GeminiMd* -> Memory* (project memory file, not an LLM client)

- UI spinners: GeminiRespondingSpinner/GeminiSpinner -> RespondingSpinner/Spinner

- Leaf types: GeminiCodeRequest/GeminiChatSendOptions/GeminiErrorEventValue/GeminiFinishedEventValue -> Llm*

- geminiRequest.ts -> llm-request.ts (and its collocated test)

No behavior change. Renamed symbols typecheck clean in core+cli; eslint clean on renamed files.

Refs #4063

* fix(cli): resolve rename build failure

* docs(serve): fix memory filename references

* test(cli): pin primary workspace QWEN.md init fallback

Assert that the primary daemon workspace service receives the
hard-coded 'QWEN.md' context filename when boot settings carry no
context.fileName. Previously only the secondary workspace's explicit
SECONDARY.md resolution was asserted, so swapping the fallback literal
at the createDaemonWorkspaceService call site survived the suite.

* refactor(core,cli): finish Gemini residue rename in memoryDiscovery

Complete the rename flagged in review: GeminiFileContent -> MemoryFileContent (module-local interface), includeDirectoriesToReadGemini -> includeDirectoriesToReadMemory (parameter only; all call sites are positional, zero cross-package impact), plus test-local variable names and the stale ORIGINAL_GEMINI_MD_FILENAME test title.

* docs(design): route loadHierarchicalGeminiMemory to Memory naming

Per exception #1 the Llm prefix is reserved for the generic LLM-client surface; the symbol is a memory-file loader (thin wrapper around core's loadServerHierarchicalMemory), so the PR-2 symbol map targets loadHierarchicalMemory instead of loadHierarchicalLlmMemory. Doc-only: the code symbol is not renamed by this PR.

* docs(cli): narrow extractContextFilename fallback description

The undefined fallback first inherits the primary workspace's configured context.fileName snapshot (contextFilenameForInit) at the secondary startup and dynamically added workspace call sites, before the hard-coded QWEN.md. Describe the actual chain instead of the hard-coded default only. Comment-only: the inheritance behavior predates this PR and is unchanged.

* refactor(cli): rename loadHierarchicalGeminiMemory to loadHierarchicalMemory

The design doc's symbol map routes the memory loader to
loadHierarchicalMemory (memory family, exception #1), but no phasing
bullet performed the rename and a prior round left the mixed signature.
Complete the rename across the definition (config.ts), the AppContainer
call site, and the AppContainer test mocks, and update the design doc's
exception #1, symbol map, and PR-1 phasing bullet so the map row is no
longer orphaned.

* fix(core): preserve Gemini rename compatibility

* docs(core): extend Gemini deprecation window

* refactor(core,cli): rename Gemini LLM identifiers

* fix(core): retain Gemini content generator aliases

* docs(core): document legacy content generator paths

* ci: trigger checks after retargeting to main

* test(cli): update renamed LLM expectations

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
This commit is contained in:
易良andqwen-code-dev-bot authored and GitHub committed 2026-08-28 03:05:49 +00:00
1 parent b91c281e34
commit 983fafd1ed
238 files changed
+10542 -10473

No files matched your search

@@ -98,7 +98,8 @@ Non-test files; `gemini-converter.ts` is intentionally NOT renamed (see above).
| `packages/core/src/core/geminiContentGenerator/index.ts` | `packages/core/src/core/llm-content-generator/index.ts` |
| `packages/core/src/core/geminiRequest.ts` | `packages/core/src/core/llm-request.ts` |
The old `geminiRequest.ts` path remains as a deprecated re-export shim until a
The old `geminiRequest.ts`, `geminiChat.ts`, and
`geminiContentGenerator/` paths remain as deprecated re-export shims until a
future major release.
## Phasing
@@ -133,6 +134,9 @@ move as one atomic PR.
- Stream layer: `useGeminiStream` → `useLlmStream` (`use-llm-stream.ts`),
`gemini.tsx` → `llm.tsx`.
- Protocol converters: `convert*ToGemini*` / `convertGemini*To*` → `Llm`.
- Deprecated compatibility aliases for the published core classes, event
types, `Config` client access/initialization option, and old chat and content
generator module paths.
## Risks
@@ -158,5 +162,5 @@ move as one atomic PR.
- `cd packages/cli && npx tsc --noEmit`
- Targeted unit tests per renamed module
- Legacy-name grep results are confined to the documented compatibility aliases
and `geminiRequest.ts` shim; active repository consumers use the new names.
and re-export shims; active repository consumers use the new names.
- `npm run lint` (kebab-case filenames are enforced)
@@ -16,7 +16,7 @@ Dream scheduled once after a complete, successful logical user turn.
This design deliberately does **not** add a new coordinator class. Every ACP
`Session` already has a per-session `Config`, and that `Config` already owns one
`GeminiClient`. The `GeminiClient` is therefore the existing correct owner for
`LlmClient`. The `LlmClient` is therefore the existing correct owner for
the pending Recall handle, surfaced-document deduplication, recent-tool context,
cancellation, and Recall delivery telemetry. The ACP `Session` calls a small
public lifecycle API on that existing object.
@@ -37,7 +37,7 @@ older tree and then mechanically applied to the target.
At the target baseline:
- `GeminiClient.sendMessageStream()` owns the complete managed Recall state
- `LlmClient.sendMessageStream()` owns the complete managed Recall state
machine.
- A fresh interactive `UserQuery` starts Recall after `UserPromptSubmit` has
allowed the request, but queries with the original pre-hook prompt text.
@@ -48,13 +48,13 @@ At the target baseline:
delivered by the fast phase are removed before refined delivery.
- Pending Recall is cancelled on a new query, parent abort, reset, shutdown, or
a turn with no later safe delivery point.
- `GeminiClient` tracks surfaced memory paths for session-level deduplication and
- `LlmClient` tracks surfaced memory paths for session-level deduplication and
recent completed tool names for Recall relevance filtering.
- Extract and Dream are implemented and guarded by `MemoryManager`; their
cursor, trailing-request merge, locks, pressure gates, and Dream thresholds do
not depend on the UI.
ACP bypasses the orchestration above by calling `GeminiChat.sendMessageStream()`
ACP bypasses the orchestration above by calling `LlmChat.sendMessageStream()`
from `Session.#sendMessageStreamWithAutoCompression()`. The comment above
`Session.#buildInitialSystemReminders()` explicitly records that managed Recall
is absent. ACP also never schedules Extract or Dream after its complete tool and
@@ -79,7 +79,7 @@ algorithm.
- Reimplementing Recall, Extract, or Dream in ACP, a hook, a sidecar, or a proxy.
- Adding an ACP method or notification for memory task progress.
- Refactoring all `GeminiClient` background work, including auto-skill, into a
- Refactoring all `LlmClient` background work, including auto-skill, into a
new shared scheduler.
- Changing memory paths, workspace scoping, file formats, settings defaults, or
Dream thresholds.
@@ -94,14 +94,14 @@ algorithm.
## Ownership
| Concern | Owner | Reason |
| -------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------- |
| Pending Recall promise and abort controller | Per-session `GeminiClient` | It already owns this state for interactive turns and is shared by the ACP `Session`. |
| Fast/refined arbitration, surfaced-path dedup, telemetry | Per-session `GeminiClient` | These invariants must not be copied into ACP. |
| Fresh/retry/continue/runtime turn classification | ACP `Session` | Only `Session` knows ACP admission metadata and Goal origin. |
| ToolResult provider-send ordering | ACP `Session` | It owns the autonomous tool loop and final outgoing `Part[]`. |
| Successful logical-turn completion | ACP `Session` | It alone spans model calls, tools, mid-turn input, Stop hooks, and todo-stop continuations. |
| Extract/Dream jobs and durable state | Existing `MemoryManager` | It already owns cursors, merging, locks, gates, and task records. |
| Concern | Owner | Reason |
| -------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------- |
| Pending Recall promise and abort controller | Per-session `LlmClient` | It already owns this state for interactive turns and is shared by the ACP `Session`. |
| Fast/refined arbitration, surfaced-path dedup, telemetry | Per-session `LlmClient` | These invariants must not be copied into ACP. |
| Fresh/retry/continue/runtime turn classification | ACP `Session` | Only `Session` knows ACP admission metadata and Goal origin. |
| ToolResult provider-send ordering | ACP `Session` | It owns the autonomous tool loop and final outgoing `Part[]`. |
| Successful logical-turn completion | ACP `Session` | It alone spans model calls, tools, mid-turn input, Stop hooks, and todo-stop continuations. |
| Extract/Dream jobs and durable state | Existing `MemoryManager` | It already owns cursors, merging, locks, gates, and task records. |
This ownership split avoids both bad extremes: copying the Recall state machine
into `Session.ts`, and introducing a general coordinator that would merely wrap
@@ -157,10 +157,10 @@ they need no extra flags.
## Design
### 1. Expose the existing Recall lifecycle on `GeminiClient`
### 1. Expose the existing Recall lifecycle on `LlmClient`
Extract the current inline/private orchestration into three public methods on
the already-exported `GeminiClient`:
the already-exported `LlmClient`:
```ts
beginManagedAutoMemoryRecall(query: string, signal: AbortSignal): void;
@@ -192,7 +192,7 @@ The current lower-level helper may remain private so the interactive Cron path
can retain its existing zero-wait initial poll. No wait constant or new Recall
type is exported.
`GeminiClient.sendMessageStream()` is migrated to these methods in the same
`LlmClient.sendMessageStream()` is migrated to these methods in the same
change, so the interactive path continues to exercise the shared API. Existing
Recall tests then protect both callers instead of leaving the new public path
ACP-only and weakly tested.
@@ -200,7 +200,7 @@ ACP-only and weakly tested.
The complete downstream consumer list for the new methods is intentionally
small. It consists of three owning methods:
1. `GeminiClient.sendMessageStream()` for interactive/headless UserQuery and
1. `LlmClient.sendMessageStream()` for interactive/headless UserQuery and
ToolResult sends.
2. `Session.#executePromptInner()` for ACP begin, initial consume, and final
cleanup.
@@ -223,7 +223,7 @@ In `Session.#executePromptInner()`:
```ts
this.config
.getGeminiClient()
.getLlmClient()
.beginManagedAutoMemoryRecall(promptText, pendingSend.signal);
```
@@ -242,7 +242,7 @@ consume the initial result:
```ts
const memory = await this.config
.getGeminiClient()
.getLlmClient()
.consumeManagedAutoMemoryRecall('initial');
if (memory?.prompt) {
@@ -275,7 +275,7 @@ Instead, extend `Session.#sendMessageStreamWithAutoCompression()` after all of
its explicit send-drop gates have passed: `prepareBeforeCompression`, automatic
compression, the session-token limit, the compression diagnostic,
`beforeSend`, and the final abort check. Immediately before constructing the
request and calling `GeminiChat.sendMessageStream()`:
request and calling `LlmChat.sendMessageStream()`:
1. Detect that the outgoing message starts with one or more
`functionResponse` parts.
@@ -369,7 +369,7 @@ void memoryManager
```
Do not await either task and do not store their promises in
`GeminiClient.pendingMemoryTaskPromises`; ACP has no TUI `memory_saved` item to
`LlmClient.pendingMemoryTaskPromises`; ACP has no TUI `memory_saved` item to
consume that queue. `MemoryManager` already tracks the work. The two rejection
handlers are required to prevent unhandled background rejections, not to add a
fallback path.
@@ -381,13 +381,13 @@ turn. Tool and Stop-hook continuations never pass that callsite independently.
### 7. Feed existing recent-tool state from ACP
ACP currently executes tools without calling the already-public
`GeminiClient.recordCompletedToolCall()`. Use `Session.runTool()`'s existing
`LlmClient.recordCompletedToolCall()`. Use `Session.runTool()`'s existing
outer `finally`, where `terminalStatus` and the effective `args` are already
known, to call it once when a registered tool reaches a non-cancelled terminal
state:
```ts
this.config.getGeminiClient().recordCompletedToolCall(toolName, args);
this.config.getLlmClient().recordCompletedToolCall(toolName, args);
```
This keeps Recall's recent-tool noise filter aligned with interactive sessions.
@@ -406,7 +406,7 @@ auto-skill behavior.
sequenceDiagram
participant C as ACP client
participant S as ACP Session
participant G as GeminiClient Recall owner
participant G as LlmClient Recall owner
participant M as MemoryManager
participant L as Main model
@@ -551,23 +551,23 @@ The implementation is complete only if all of these hold:
### New `ManagedMemoryTurnCoordinator` class
Rejected for this PR. It would move the same fields out of `GeminiClient`, add a
Rejected for this PR. It would move the same fields out of `LlmClient`, add a
new exported entity, and force constructor/lifecycle plumbing even though ACP
already shares the exact `GeminiClient` instance. Public lifecycle methods on the
already shares the exact `LlmClient` instance. Public lifecycle methods on the
existing state owner provide reuse with a smaller proof surface.
A standalone coordinator becomes justified only if a future runtime does not
own a `GeminiClient` but still needs the same Recall state machine.
own a `LlmClient` but still needs the same Recall state machine.
### Copy Recall state into `Session.ts`
Rejected. It duplicates fast/refined arbitration, path deduplication,
cancellation listeners, terminal telemetry, and every future Recall change.
### Put all post-turn scheduling in `GeminiClient`
### Put all post-turn scheduling in `LlmClient`
Rejected. ACP has the authoritative complete-turn boundary, while
`GeminiClient.sendMessageStream()` is a physical model-send boundary in
`LlmClient.sendMessageStream()` is a physical model-send boundary in
interactive flows. Forcing ACP to call the interactive background-task method
would also enqueue TUI-only notification promises and couple this change to
auto-skill scheduling.
@@ -606,7 +606,7 @@ The ACP/WebSocket proxy remains transport-only and requires no memory logic.
1. Rebase the fork branch onto target baseline `43d46be` or a newer upstream
commit and re-check the named symbols.
2. Refactor `GeminiClient` Recall into the public lifecycle methods with no
2. Refactor `LlmClient` Recall into the public lifecycle methods with no
behavior change; run the core tests.
3. Add ACP begin/initial/refined/finalize wiring and focused Session tests.
4. Add exactly-once Extract/Dream scheduling and recent-tool recording tests.
@@ -225,7 +225,7 @@ qwen serve
packages/cli/index.ts main()
|
v
gemini.tsx main() - parseArguments()
llm.tsx main() - parseArguments()
|
v (yargs assembly)
config/config.ts import { serveCommand } ...
@@ -276,7 +276,7 @@ Key facts:
- **`createServeApp` only builds; it does not listen.** It returns an `express()` instance with middleware and routes mounted. Ordinary-only embedders may continue to own `app.listen()`. Embedders that use Live/Conversations must bind the actual Node server to the exported app lifecycle before listening and await that lifecycle during shutdown.
- **`() => actualPort` is a lazy closure.** `actualPort` is assigned in the `server.listen` callback. The `hostAllowlist` middleware reads it on demand, so ephemeral ports (`--port 0`) still gate the `Host` header correctly.
- **`await blockForever()` is intentional.** If `yargs.parse()` resolves, the CLI top level falls through into the interactive TUI entrypoint (`gemini.tsx`). SIGINT / SIGTERM exit through `runQwenServe`'s `onSignal` path.
- **`await blockForever()` is intentional.** If `yargs.parse()` resolves, the CLI top level falls through into the interactive TUI entrypoint (`llm.tsx`). SIGINT / SIGTERM exit through `runQwenServe`'s `onSignal` path.
## 10. HTTP route file split
@@ -54,7 +54,7 @@ history 足够大时会产生峰值放大,需要再用默认 heap 长任务验
### 关键配置修改
`packages/core/src/core/geminiChat.ts` 中将 heap-pressure compaction 阈值从 0.7 改为 99.0(使其永远不触发),模拟 #4186 修复前的状态。
`packages/core/src/core/llm-chat.ts` 中将 heap-pressure compaction 阈值从 0.7 改为 99.0(使其永远不触发),模拟 #4186 修复前的状态。
---
@@ -63,7 +63,7 @@ history 足够大时会产生峰值放大,需要再用默认 heap 长任务验
### 崩溃时间线
```
[21:26:59] #1 RSS:193.6MB Ctx:0% → Read geminiChat.ts (1500 行)
[21:26:59] #1 RSS:193.6MB Ctx:0% → Read llm-chat.ts (1500 行)
[21:27:46] #2 RSS:270.4MB Ctx:4.2% → Read agent.ts
[21:28:32] #3 RSS:397.5MB Ctx:4.3% → grep + Read 3 个文件
[21:29:18] #4 RSS:452.7MB Ctx:5.7% → Read slashCommandProcessor.ts
@@ -363,7 +363,7 @@ sendMessage()
SESSION="$1"
TASKS=(
"用 Read 工具完整读取 packages/core/src/core/geminiChat.ts"
"用 Read 工具完整读取 packages/core/src/core/llm-chat.ts"
"用 Read 工具完整读取 packages/core/src/tools/agent/agent.ts"
"用 grep -rn structuredClone packages/core/src 然后 Read 前 3 个文件"
"用 Read 完整读取 packages/cli/src/ui/hooks/slashCommandProcessor.ts"
@@ -399,7 +399,7 @@ done
```bash
# 1. 禁用 heap-pressure safety net
# geminiChat.ts: HEAP_PRESSURE_COMPRESSION_RATIO = 99.0
# llm-chat.ts: HEAP_PRESSURE_COMPRESSION_RATIO = 99.0
# 2. Build
npm run build --workspace=packages/core && npm run build --workspace=packages/cli
+1 -1
View File
@@ -698,7 +698,7 @@ not an implementation issue. **Ready for Phase 7 code review.**
| ----------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Re-attach to existing worktree (G1.b) | `packages/cli/src/startup/worktreeStartup.ts` | Added pre-create check: if dir is a registered worktree on the expected branch, skip create + chdir |
| `getRegisteredWorktreeBranch()` helper | `packages/core/src/services/gitWorktreeService.ts` | Probes `git rev-parse --abbrev-ref HEAD` against the candidate path |
| Path normalization before chdir (G2) | `packages/cli/src/gemini.tsx` | Resolves `mcpConfig`, `openaiLoggingDir`, `jsonFile`, `inputFile`, `telemetryOutfile`, `includeDirectories` against launch cwd when `--worktree` is set |
| Path normalization before chdir (G2) | `packages/cli/src/llm.tsx` | Resolves `mcpConfig`, `openaiLoggingDir`, `jsonFile`, `inputFile`, `telemetryOutfile`, `includeDirectories` against launch cwd when `--worktree` is set |
| Documentation: yargs flag ordering tip + Limitations update | `docs/users/features/worktree.md` | Quick Start tip + new Limitations bullets (cross-slug, path-arg behavior) |
| Unit tests for re-attach | `packages/cli/src/startup/worktreeStartup.test.ts` | Added 2 tests: happy re-attach + "different branch occupies slot" guard |
+1 -1
View File
@@ -124,7 +124,7 @@ Three points that tend to trip up a first read of this feature:
1. **One generation per batch, shared by both display modes.** The fast-model call happens exactly once in `handleCompletedTools` when a tool batch finalizes. Toggling `Ctrl+O` expanded detail mode afterwards does **not** trigger a new call — both the collapsed and the expanded rendering read from the same `tool_use_summary` history entry that was captured the first time.
2. **No backfill on toggle or on session resume.** A `tool_group` that completed before the feature was enabled (or before you flipped the setting on, or in a resumed session — `ChatRecordingService` does not persist summary entries) will never get a label. There is no "sweep existing history" pass. If you turn this setting on mid-session, only _future_ batches will show a label; older groups keep the default rendering with no indicator that a label is missing.
3. **Main-agent batches only.** The trigger lives in the main session's turn loop (`useGeminiStream`), so:
3. **Main-agent batches only.** The trigger lives in the main session's turn loop (`useLlmStream`), so:
- ✅ Shell, MCP, file operations, and the `Task` / subagent tool _call itself_ (as it appears in the main batch) are summarized.
- ❌ A subagent's **internal** tool batches (run through `packages/core/src/agents/runtime/`) are not summarized.
-1
View File
@@ -440,7 +440,6 @@ export const legacyFilenames = [
'useFeedbackDialog',
'useFocus',
'useFolderTrust',
'useGeminiStream',
'useGitBranchName',
'useHistoryManager',
'useHooksDialog',
+1 -1
View File
@@ -11455,7 +11455,7 @@ export function createAcpSessionBridge(opts: BridgeOptions): AcpSessionBridge {
async generateSessionRecap(sessionId, _context) {
// Thin pass-through to `qwen/control/session/
// recap` — the ACP child runs `generateSessionRecap` against the
// session's GeminiClient history and returns `{sessionId, recap}`
// session's LlmClient history and returns `{sessionId, recap}`
// where `recap` may be `null` for too-short histories or transient
// model failures. The core helper is documented to never throw,
// so the only paths that surface as bridge errors are: unknown
@@ -558,7 +558,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({
refreshMemoryInstruction: vi.fn(
async (config: {
refreshHierarchicalMemory?: () => Promise<void>;
getGeminiClient?: () =>
getLlmClient?: () =>
| { refreshSystemInstruction?: () => Promise<void> }
| undefined;
}) => {
@@ -568,7 +568,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => ({
// Best-effort, matching the real helper.
}
try {
await config.getGeminiClient?.()?.refreshSystemInstruction?.();
await config.getLlmClient?.()?.refreshSystemInstruction?.();
} catch {
// Best-effort, matching the real helper.
}
@@ -1658,7 +1658,7 @@ describe('runAcpAgent shutdown cleanup', () => {
});
it('writes config startup warnings to stderr for the ACP client log', async () => {
// The ACP path exits gemini.tsx before its startup-warning printing
// The ACP path exits llm.tsx before its startup-warning printing
// runs; runAcpAgent must emit config warnings (e.g. the WebSearch
// enablement notices) itself or they vanish.
(mockConfig as unknown as { getWarnings: () => string[] }).getWarnings =
@@ -4049,7 +4049,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
getAuthType: vi.fn().mockReturnValue('api-key'),
getCurrentModelRegistryBaseUrl: vi.fn().mockReturnValue(undefined),
getAllConfiguredModels: vi.fn().mockReturnValue([]),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -4445,7 +4445,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
relocateWorkingDirectory: vi.fn().mockResolvedValue({}),
});
Object.assign(innerConfig.getGeminiClient(), {
Object.assign(innerConfig.getLlmClient(), {
addWorkingDirectoryChangedContext: vi.fn().mockResolvedValue(undefined),
});
vi.mocked(loadSettings).mockReturnValue(settings);
@@ -4863,7 +4863,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
isMcpServerDisabled: vi.fn().mockReturnValue(false),
setDisabledTools: vi.fn(),
getTargetDir: vi.fn().mockReturnValue('/tmp'),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(false),
setTools: vi.fn().mockResolvedValue(undefined),
}),
@@ -5054,7 +5054,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
relocateWorkingDirectory,
});
Object.assign(innerConfig.getGeminiClient(), {
Object.assign(innerConfig.getLlmClient(), {
addWorkingDirectoryChangedContext: vi.fn().mockResolvedValue(undefined),
});
const { agent, agentPromise } = await bootAcpAgent();
@@ -5100,7 +5100,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
mcpRefreshError: new Error('MCP failed'),
}),
});
Object.assign(innerConfig.getGeminiClient(), {
Object.assign(innerConfig.getLlmClient(), {
addWorkingDirectoryChangedContext: vi.fn().mockResolvedValue(undefined),
});
const { agent, agentPromise } = await bootAcpAgent();
@@ -5158,7 +5158,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
relocateWorkingDirectory,
});
Object.assign(innerConfig.getGeminiClient(), {
Object.assign(innerConfig.getLlmClient(), {
addWorkingDirectoryChangedContext: vi.fn().mockResolvedValue(undefined),
});
const { agent, agentPromise } = await bootAcpAgent();
@@ -6134,7 +6134,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
});
expect(innerConfig.refreshAuth).not.toHaveBeenCalled();
expect(innerConfig.getGeminiClient().initialize).not.toHaveBeenCalled();
expect(innerConfig.getLlmClient().initialize).not.toHaveBeenCalled();
expect(innerConfig.activateProvisionalWorkspace).not.toHaveBeenCalled();
expect(innerConfig.setFileSystemService).not.toHaveBeenCalled();
expect(
@@ -9366,7 +9366,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
isManagedMemoryAvailable: vi.fn().mockReturnValue(true),
getProjectRoot: vi.fn().mockReturnValue('/workspace'),
refreshHierarchicalMemory,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
refreshSystemInstruction,
}),
});
@@ -9425,7 +9425,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
...makeInnerConfig(),
getSessionId: vi.fn().mockReturnValue('remember-session'),
refreshHierarchicalMemory: sessionRefreshHierarchicalMemory,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -9504,7 +9504,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
...makeInnerConfig(),
getSessionId: vi.fn().mockReturnValue('remember-noop-session'),
refreshHierarchicalMemory: sessionRefreshHierarchicalMemory,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -9583,7 +9583,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
...makeInnerConfig(),
getSessionId: vi.fn().mockReturnValue('remember-fail-session'),
refreshHierarchicalMemory: sessionRefreshHierarchicalMemory,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -10804,7 +10804,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
const collapsed = `review this branch ${'x'.repeat(220)}`;
Object.assign(innerConfig, {
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -10877,7 +10877,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
const execute = vi.fn();
const build = vi.fn().mockReturnValue({ execute });
Object.assign(innerConfig, {
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -10948,7 +10948,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
const execute = vi.fn().mockResolvedValue({ llmContent: 'ok' });
const build = vi.fn().mockReturnValue({ execute });
Object.assign(innerConfig, {
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -15196,7 +15196,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
await vi.waitFor(() => expect(capturedAgentFactory).toBeDefined());
expect(mockConfig.initialize).toHaveBeenCalledWith({
skipGeminiInitialization: true,
skipLlmInitialization: true,
// F2 (#4175 commit 6 review fix — claude-opus-4-7 W119): also
// pins that the bootstrap path opts out of MCP discovery (so
// bootstrap + per-session don't double-spawn N stdio servers).
@@ -15305,7 +15305,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
innerConfig.hasHooksForEvent = vi.fn().mockReturnValue(true);
innerConfig.getModel = vi.fn().mockReturnValue('test-model');
innerConfig.getApprovalMode = vi.fn().mockReturnValue('default');
innerConfig.getGeminiClient = vi.fn().mockReturnValue({
innerConfig.getLlmClient = vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(false),
initialize,
});
@@ -15326,7 +15326,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
await agent.newSession({ cwd: '/tmp', mcpServers: [] });
expect(mockConfig.initialize).toHaveBeenCalledWith({
skipGeminiInitialization: true,
skipLlmInitialization: true,
// F2 (#4175 commit 6 review fix — claude-opus-4-7 W119): also
// pins that the bootstrap path opts out of MCP discovery (so
// bootstrap + per-session don't double-spawn N stdio servers).
@@ -15372,7 +15372,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
await agentPromise;
});
it('does not directly re-fire SessionStart for subsequent ACP sessions when GeminiClient is already initialized', async () => {
it('does not directly re-fire SessionStart for subsequent ACP sessions when LlmClient is already initialized', async () => {
const innerConfig = await setupSessionMocks(
'session-followup-session-start',
);
@@ -15385,7 +15385,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
innerConfig.hasHooksForEvent = vi.fn().mockReturnValue(true);
innerConfig.getModel = vi.fn().mockReturnValue('test-model');
innerConfig.getApprovalMode = vi.fn().mockReturnValue('default');
innerConfig.getGeminiClient = vi
innerConfig.getLlmClient = vi
.fn()
.mockReturnValueOnce({
isInitialized: vi.fn().mockReturnValue(false),
@@ -15446,7 +15446,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
innerConfigA.hasHooksForEvent = vi
.fn()
.mockImplementation((event: string) => event === 'SessionEnd');
innerConfigA.getGeminiClient = vi.fn().mockReturnValue({
innerConfigA.getLlmClient = vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(false),
initialize: vi.fn().mockResolvedValue(undefined),
});
@@ -15462,7 +15462,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
innerConfigB.hasHooksForEvent = vi
.fn()
.mockImplementation((event: string) => event === 'SessionEnd');
innerConfigB.getGeminiClient = vi.fn().mockReturnValue({
innerConfigB.getLlmClient = vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(false),
initialize: vi.fn().mockResolvedValue(undefined),
});
@@ -16189,7 +16189,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
// The warning must list both failed servers and mention "Warning:"
// exactly like the top-level path and the other non-interactive
// entry points (`gemini.tsx`, `session.ts`).
// entry points (`llm.tsx`, `session.ts`).
await vi.waitFor(() => {
const matchingWrite = stderrWrite.mock.calls.find(
([msg]) =>
@@ -16960,7 +16960,7 @@ describe('QwenAgent extMethod renameSession routing', () => {
getSessionId: vi.fn().mockReturnValue(liveSessionId),
getAuthType: vi.fn().mockReturnValue('api-key'),
getAllConfiguredModels: vi.fn().mockReturnValue([]),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -18643,7 +18643,7 @@ describe('QwenAgent loadSession / unstable_resumeSession', () => {
getSessionId: vi.fn().mockReturnValue('persisted-1'),
getAuthType: vi.fn().mockReturnValue('api-key'),
getAllConfiguredModels: vi.fn().mockReturnValue([]),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -21771,7 +21771,7 @@ describe('QwenAgent loadSession / unstable_resumeSession', () => {
configOptions: expect.anything(),
});
// resume semantic: model context is restored internally via
// geminiClient.initialize(), but UI replay is NOT triggered —
// llmClient.initialize(), but UI replay is NOT triggered —
// the SSE stream stays clean for clients that already have the
// history rendered.
expect(lastSessionMock?.replayHistory).not.toHaveBeenCalled();
@@ -22751,7 +22751,7 @@ describe('sessionLanguage multi-session propagation', () => {
getSessionId: vi.fn().mockReturnValue('sid'),
getAuthType: vi.fn().mockReturnValue('api-key'),
getAllConfiguredModels: vi.fn().mockReturnValue([]),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -22874,9 +22874,9 @@ describe('sessionLanguage multi-session propagation', () => {
expect(cfgC.refreshHierarchicalMemory).toHaveBeenCalled();
// All sessions' system instruction refreshed
expect(cfgA.getGeminiClient().refreshSystemInstruction).toHaveBeenCalled();
expect(cfgB.getGeminiClient().refreshSystemInstruction).toHaveBeenCalled();
expect(cfgC.getGeminiClient().refreshSystemInstruction).toHaveBeenCalled();
expect(cfgA.getLlmClient().refreshSystemInstruction).toHaveBeenCalled();
expect(cfgB.getLlmClient().refreshSystemInstruction).toHaveBeenCalled();
expect(cfgC.getLlmClient().refreshSystemInstruction).toHaveBeenCalled();
// Session C registered the global path
expect(cfgC.setOutputLanguageFilePath).toHaveBeenCalled();
@@ -23052,9 +23052,7 @@ describe('sessionLanguage multi-session propagation', () => {
// Both sessions still refreshed despite cfgFail's write failure
expect(cfgOk.refreshHierarchicalMemory).toHaveBeenCalled();
expect(cfgFail.refreshHierarchicalMemory).toHaveBeenCalled();
expect(
cfgFail.getGeminiClient().refreshSystemInstruction,
).toHaveBeenCalled();
expect(cfgFail.getLlmClient().refreshSystemInstruction).toHaveBeenCalled();
mockConnectionState.resolve();
await agentPromise;
@@ -23435,7 +23433,7 @@ describe('sessionLanguage multi-session propagation', () => {
refreshHierarchicalMemory,
});
const refreshSystemInstruction = vi.mocked(
cfg.getGeminiClient().refreshSystemInstruction,
cfg.getLlmClient().refreshSystemInstruction,
);
const sendAvailableCommandsUpdate = vi.fn().mockResolvedValue(undefined);
@@ -23599,9 +23597,7 @@ describe('sessionLanguage multi-session propagation', () => {
expect(skillManager.refreshCache).not.toHaveBeenCalled();
expect(extensionManager.refreshTools).toHaveBeenCalledOnce();
expect(cfg.refreshHierarchicalMemory).not.toHaveBeenCalled();
expect(
cfg.getGeminiClient().refreshSystemInstruction,
).toHaveBeenCalledOnce();
expect(cfg.getLlmClient().refreshSystemInstruction).toHaveBeenCalledOnce();
expect(sendAvailableCommandsUpdate).toHaveBeenCalledOnce();
mockConnectionState.resolve();
@@ -23666,9 +23662,7 @@ describe('sessionLanguage multi-session propagation', () => {
expect(skillManager.refreshCache).not.toHaveBeenCalled();
expect(extensionManager.refreshTools).toHaveBeenCalledOnce();
expect(cfg.refreshHierarchicalMemory).not.toHaveBeenCalled();
expect(
cfg.getGeminiClient().refreshSystemInstruction,
).toHaveBeenCalledOnce();
expect(cfg.getLlmClient().refreshSystemInstruction).toHaveBeenCalledOnce();
expect(sendAvailableCommandsUpdate).toHaveBeenCalledOnce();
mockConnectionState.resolve();
+23 -24
View File
@@ -2652,7 +2652,7 @@ export async function runAcpAgent(
beginAcpBootstrapConfigProfiling();
try {
await config.initialize({
skipGeminiInitialization: true,
skipLlmInitialization: true,
// Bootstrap skips MCP discovery — each session runs its own
// pool-routed discovery, so bootstrap-level spawns would be
// redundant subprocess leaks (W119).
@@ -2664,7 +2664,7 @@ export async function runAcpAgent(
} finally {
endAcpBootstrapConfigProfiling();
}
// The ACP path exits gemini.tsx before its startup-warning printing runs,
// The ACP path exits llm.tsx before its startup-warning printing runs,
// so config warnings (including initialize-time ones like the WebSearch
// enablement notice) would otherwise vanish. stderr lands in the client's
// logs without interfering with the ACP protocol on stdout.
@@ -3670,9 +3670,9 @@ class QwenAgent implements Agent {
const registry = config.getToolRegistry();
if (operation === 'discover') {
await registry?.discoverToolsForServer(serverName);
const geminiClient = config.getGeminiClient?.();
if (geminiClient?.isInitialized?.()) {
await geminiClient.setTools?.();
const llmClient = config.getLlmClient?.();
if (llmClient?.isInitialized?.()) {
await llmClient.setTools?.();
}
} else if (operation === 'disable') {
await registry?.disableMcpServer(serverName);
@@ -3760,7 +3760,7 @@ class QwenAgent implements Agent {
config.setMcpTransportPool(this.mcpPool);
try {
await config.initialize({
skipGeminiInitialization: true,
skipLlmInitialization: true,
skipFileCheckpointing: true,
skipHooks: true,
skipSkillManager: true,
@@ -3911,9 +3911,9 @@ class QwenAgent implements Agent {
}
await Promise.all(
this.getLiveMcpConfigs(serverName).map(async (config) => {
const geminiClient = config.getGeminiClient?.();
if (geminiClient?.isInitialized?.()) {
await geminiClient.setTools?.();
const llmClient = config.getLlmClient?.();
if (llmClient?.isInitialized?.()) {
await llmClient.setTools?.();
}
}),
);
@@ -8991,9 +8991,9 @@ class QwenAgent implements Agent {
});
await Promise.all(
this.getLiveMcpConfigs(serverName).map(async (liveConfig) => {
const geminiClient = liveConfig.getGeminiClient?.();
if (geminiClient?.isInitialized?.()) {
await geminiClient.setTools?.();
const llmClient = liveConfig.getLlmClient?.();
if (llmClient?.isInitialized?.()) {
await llmClient.setTools?.();
}
}),
);
@@ -10229,7 +10229,7 @@ class QwenAgent implements Agent {
try {
await config
.getGeminiClient()
.getLlmClient()
?.addWorkingDirectoryChangedContext(
settledPreviousCwd,
canonicalPath,
@@ -10443,7 +10443,7 @@ class QwenAgent implements Agent {
);
}
await cfg.refreshHierarchicalMemory();
await cfg.getGeminiClient()?.refreshSystemInstruction();
await cfg.getLlmClient()?.refreshSystemInstruction();
}),
);
const failedCount = results.filter(
@@ -10639,7 +10639,7 @@ class QwenAgent implements Agent {
let hasHistory = false;
try {
hasHistory =
(config.getGeminiClient().getHistoryShallow() ?? []).length > 0;
(config.getLlmClient().getHistoryShallow() ?? []).length > 0;
} catch (error) {
debugLogger.debug('Failed to read history before /fork:', error);
}
@@ -10680,7 +10680,7 @@ class QwenAgent implements Agent {
}
try {
config.getGeminiClient().addHistory({
config.getLlmClient().addHistory({
role: 'user',
parts: [
{
@@ -10713,10 +10713,10 @@ class QwenAgent implements Agent {
}
const session = this.sessionOrThrow(sessionId);
const config = session.getConfig();
const geminiClient = config.getGeminiClient()!;
const llmClient = config.getLlmClient()!;
const outputText =
typeof params['output'] === 'string' ? params['output'] : '';
geminiClient.addHistory({
llmClient.addHistory({
role: 'user',
parts: [
{
@@ -11177,8 +11177,7 @@ class QwenAgent implements Agent {
);
}
await runRefresh(
async () =>
await config.getGeminiClient()?.refreshSystemInstruction(),
async () => await config.getLlmClient()?.refreshSystemInstruction(),
);
await runRefresh(
async () => await session.sendAvailableCommandsUpdate(),
@@ -12093,7 +12092,7 @@ class QwenAgent implements Agent {
);
}
try {
await config.getGeminiClient()?.refreshSystemInstruction();
await config.getLlmClient()?.refreshSystemInstruction();
} catch (err) {
debugLogger.warn(
`reload: refreshSystemInstruction failed for session ${id}: ${err}`,
@@ -12714,11 +12713,11 @@ class QwenAgent implements Agent {
): Promise<Session> {
this.assertManagedSessionAdmission();
const sessionId = normalizeSessionIdForLookup(config.getSessionId());
const geminiClient = config.getGeminiClient();
const needsInitialize = !geminiClient.isInitialized();
const llmClient = config.getLlmClient();
const needsInitialize = !llmClient.isInitialized();
if (needsInitialize && options.deferWorkspaceActivation !== true) {
await geminiClient.initialize();
await llmClient.initialize();
}
this.assertManagedSessionAdmission();
@@ -357,7 +357,7 @@ describe('QwenAgent loadSession — Phase C worktree context restore', () => {
getSessionId: vi.fn().mockReturnValue(SESSION_ID),
getAuthType: vi.fn().mockReturnValue('api-key'),
getAllConfiguredModels: vi.fn().mockReturnValue([]),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
initialize: vi.fn().mockResolvedValue(undefined),
waitForMcpReady: vi.fn().mockResolvedValue(undefined),
@@ -6,7 +6,7 @@
/**
* Stateless, tool-free generation for the daemon request-scoped SSE endpoint.
* It deliberately bypasses GeminiChat so neither history nor recording is
* It deliberately bypasses LlmChat so neither history nor recording is
* read or mutated.
*/
import { getResponseText, type Config } from '@qwen-code/qwen-code-core';
@@ -21,7 +21,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Session } from './Session.js';
import type { Config, GeminiChat } from '@qwen-code/qwen-code-core';
import type { Config, LlmChat } from '@qwen-code/qwen-code-core';
import {
ApprovalMode,
AuthType,
@@ -64,7 +64,7 @@ describe('Session review-worktree lease sweep', () => {
/** promptIdContext store observed inside each model send. */
let observedPromptIds: Array<string | undefined>;
let mockChat: GeminiChat;
let mockChat: LlmChat;
let mockConfig: Config;
let mockClient: AgentSideConnection;
let mockSettings: LoadedSettings;
@@ -83,9 +83,9 @@ describe('Session review-worktree lease sweep', () => {
setHistory: vi.fn(),
truncateHistory: vi.fn(),
stripThoughtsFromHistory: vi.fn(),
} as unknown as GeminiChat;
} as unknown as LlmChat;
const mockGeminiClient = {
const mockLlmClient = {
getChat: vi.fn().mockReturnValue(mockChat),
tryCompressChat: vi.fn().mockResolvedValue({
originalTokenCount: 0,
@@ -147,7 +147,7 @@ describe('Session review-worktree lease sweep', () => {
getAuthType: vi.fn().mockReturnValue(AuthType.USE_OPENAI),
isCronEnabled: vi.fn().mockReturnValue(false),
getSessionTokenLimit: vi.fn().mockReturnValue(0),
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
getLlmClient: vi.fn().mockReturnValue(mockLlmClient),
getManagedAutoMemoryEnabled: vi.fn().mockReturnValue(false),
getDisableAllHooks: vi.fn().mockReturnValue(true),
hasHooksForEvent: vi.fn().mockReturnValue(false),
@@ -32,7 +32,7 @@ import type {
ChatRecord,
Config,
Extension,
GeminiChat,
LlmChat,
} from '@qwen-code/qwen-code-core';
import {
ApprovalMode,
@@ -412,7 +412,7 @@ function expectCompressBeforeSend(
}
describe('Session', () => {
let mockChat: GeminiChat;
let mockChat: LlmChat;
let mockConfig: Config;
let mockClient: AgentSideConnection;
let mockSettings: LoadedSettings;
@@ -448,7 +448,7 @@ describe('Session', () => {
restoreFromSnapshots: ReturnType<typeof vi.fn>;
rewind: ReturnType<typeof vi.fn>;
};
let mockGeminiClient: {
let mockLlmClient: {
getChat: ReturnType<typeof vi.fn>;
isInitialized: ReturnType<typeof vi.fn>;
refreshSystemInstruction: ReturnType<typeof vi.fn>;
@@ -660,8 +660,8 @@ describe('Session', () => {
stripThoughtsFromHistory: vi.fn(),
stripOrphanedUserEntriesFromHistory: vi.fn().mockReturnValue([]),
setTools: vi.fn(),
} as unknown as GeminiChat;
mockGeminiClient = {
} as unknown as LlmChat;
mockLlmClient = {
getChat: vi.fn().mockReturnValue(mockChat),
isInitialized: vi.fn().mockReturnValue(true),
refreshSystemInstruction: vi.fn().mockResolvedValue(undefined),
@@ -860,7 +860,7 @@ describe('Session', () => {
// Mirrors the resolved settings default (cli/config.ts passes
// `skipLoopDetection ?? true`): heuristics off unless a test opts in.
getSkipLoopDetection: vi.fn().mockReturnValue(true),
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
getLlmClient: vi.fn().mockReturnValue(mockLlmClient),
getManagedAutoMemoryEnabled: vi.fn().mockReturnValue(true),
getMemoryManager: vi.fn().mockReturnValue(mockMemoryManager),
getGoalRuntime: vi.fn().mockReturnValue(mockGoalRuntime),
@@ -941,11 +941,11 @@ describe('Session', () => {
core.Storage.setRuntimeBaseDir(null);
// Clear session reference to allow garbage collection
session = undefined as unknown as Session;
mockChat = undefined as unknown as GeminiChat;
mockChat = undefined as unknown as LlmChat;
mockConfig = undefined as unknown as Config;
mockClient = undefined as unknown as AgentSideConnection;
mockSettings = undefined as unknown as LoadedSettings;
mockGeminiClient = undefined as unknown as typeof mockGeminiClient;
mockLlmClient = undefined as unknown as typeof mockLlmClient;
mockMemoryManager = undefined as unknown as typeof mockMemoryManager;
mockToolRegistry = undefined as unknown as typeof mockToolRegistry;
vi.restoreAllMocks();
@@ -2038,7 +2038,7 @@ describe('Session', () => {
'qwen/control/live/speak-to-user',
{ callerSessionId: 'test-session-id', message: '测试语音' },
);
expect(mockGeminiClient.setTools).toHaveBeenCalledOnce();
expect(mockLlmClient.setTools).toHaveBeenCalledOnce();
} finally {
await fs.unlink(screenshotPath).catch(() => undefined);
}
@@ -2369,7 +2369,7 @@ describe('Session', () => {
{ role: 'user', parts: [{ text: 'hello' }] },
{ role: 'model', parts: [{ text: 'response' }] },
];
mockGeminiClient.consumeManagedAutoMemoryRecall.mockResolvedValueOnce({
mockLlmClient.consumeManagedAutoMemoryRecall.mockResolvedValueOnce({
prompt: memoryPrompt,
selectedDocs: [],
strategy: 'heuristic',
@@ -2384,9 +2384,10 @@ describe('Session', () => {
prompt: [{ type: 'text', text: 'hello' }],
});
expect(
mockGeminiClient.beginManagedAutoMemoryRecall,
).toHaveBeenCalledWith('hello', expect.any(AbortSignal));
expect(mockLlmClient.beginManagedAutoMemoryRecall).toHaveBeenCalledWith(
'hello',
expect.any(AbortSignal),
);
expect(textParts(firstSentMessage())).toEqual([memoryPrompt, 'hello']);
expect(mockMemoryManager.scheduleExtract).toHaveBeenCalledWith({
projectRoot: '/repo',
@@ -2402,13 +2403,13 @@ describe('Session', () => {
});
expect(mockMemoryManager.scheduleDream).toHaveBeenCalledOnce();
expect(
mockGeminiClient.finishManagedAutoMemoryRecall,
mockLlmClient.finishManagedAutoMemoryRecall,
).toHaveBeenCalledOnce();
});
it('delivers refined recall after tool responses and records the completed tool', async () => {
const memoryPrompt = '<system-reminder>refined memory</system-reminder>';
mockGeminiClient.consumeManagedAutoMemoryRecall
mockLlmClient.consumeManagedAutoMemoryRecall
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
prompt: memoryPrompt,
@@ -2462,9 +2463,9 @@ describe('Session', () => {
expect(followUp[0]?.functionResponse).toBeDefined();
expect(textParts(followUp)).toEqual([memoryPrompt]);
expect(
mockGeminiClient.consumeManagedAutoMemoryRecall,
mockLlmClient.consumeManagedAutoMemoryRecall,
).toHaveBeenNthCalledWith(2, 'tool_result');
expect(mockGeminiClient.recordCompletedToolCall).toHaveBeenCalledWith(
expect(mockLlmClient.recordCompletedToolCall).toHaveBeenCalledWith(
'read_file',
{ path: '/tmp/test.txt' },
);
@@ -2481,9 +2482,7 @@ describe('Session', () => {
retry: true,
} as PromptRequest);
expect(
mockGeminiClient.beginManagedAutoMemoryRecall,
).not.toHaveBeenCalled();
expect(mockLlmClient.beginManagedAutoMemoryRecall).not.toHaveBeenCalled();
expect(mockMemoryManager.scheduleExtract).not.toHaveBeenCalled();
expect(mockMemoryManager.scheduleDream).not.toHaveBeenCalled();
@@ -2497,13 +2496,11 @@ describe('Session', () => {
}),
).rejects.toThrow('provider failed');
expect(
mockGeminiClient.beginManagedAutoMemoryRecall,
).toHaveBeenCalledOnce();
expect(mockLlmClient.beginManagedAutoMemoryRecall).toHaveBeenCalledOnce();
expect(mockMemoryManager.scheduleExtract).not.toHaveBeenCalled();
expect(mockMemoryManager.scheduleDream).not.toHaveBeenCalled();
expect(
mockGeminiClient.finishManagedAutoMemoryRecall,
mockLlmClient.finishManagedAutoMemoryRecall,
).toHaveBeenCalledOnce();
});
});
@@ -3078,7 +3075,7 @@ describe('Session', () => {
});
it('rejects when the gemini client is not initialized', async () => {
vi.mocked(mockGeminiClient.isInitialized).mockReturnValue(false);
vi.mocked(mockLlmClient.isInitialized).mockReturnValue(false);
const promptSpy = vi
.spyOn(session, 'prompt')
.mockResolvedValue({ stopReason: 'end_turn' });
@@ -3097,7 +3094,7 @@ describe('Session', () => {
// Force the continuation send to fail NON-cancelled (session token limit)
// so it hits the `!responseStream` branch — the data-loss window.
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat.mockResolvedValue({
mockLlmClient.tryCompressChat.mockResolvedValue({
originalTokenCount: 999,
newTokenCount: 999,
compressionStatus: core.CompressionStatus.NOOP,
@@ -7077,8 +7074,8 @@ describe('Session', () => {
});
it('fires MessageDisplay with cumulative non-thought text and is_final on the ACP prompt path', async () => {
// Regression: the ACP surface consumes GeminiChat's stream directly
// (never entering GeminiClient.sendMessageStream), so it must fire the
// Regression: the ACP surface consumes LlmChat's stream directly
// (never entering LlmClient.sendMessageStream), so it must fire the
// MessageDisplay hook itself — without this, an IDE/daemon client sees
// the hook advertised but never receives an event.
const messageBus = { request: vi.fn().mockResolvedValue({}) };
@@ -7912,7 +7909,7 @@ describe('Session', () => {
const notificationCompression = {
signal: undefined as AbortSignal | undefined,
};
mockGeminiClient.tryCompressChat = vi
mockLlmClient.tryCompressChat = vi
.fn()
.mockResolvedValueOnce({
originalTokenCount: 0,
@@ -7957,7 +7954,7 @@ describe('Session', () => {
});
await vi.waitFor(() => {
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledTimes(2);
});
await session.cancelPendingPrompt();
@@ -7982,7 +7979,7 @@ describe('Session', () => {
compressionStatus: core.CompressionStatus.NOOP,
};
let notificationSignal: AbortSignal | undefined;
mockGeminiClient.tryCompressChat = vi
mockLlmClient.tryCompressChat = vi
.fn()
.mockResolvedValueOnce(noopCompression)
.mockImplementationOnce(
@@ -8039,7 +8036,7 @@ describe('Session', () => {
compressionStatus: core.CompressionStatus.NOOP,
};
let notificationSignal: AbortSignal | undefined;
mockGeminiClient.tryCompressChat = vi
mockLlmClient.tryCompressChat = vi
.fn()
.mockImplementationOnce(
async (_promptId: string, _force: boolean, signal: AbortSignal) => {
@@ -8163,15 +8160,14 @@ describe('Session', () => {
expect(mockToolRegistry.pinDeferredToolReveal).toHaveBeenCalledWith(
'create_sub_session',
);
expect(mockGeminiClient.setTools).toHaveBeenCalledTimes(1);
expect(mockLlmClient.setTools).toHaveBeenCalledTimes(1);
// Order is load-bearing: reveal before the declaration refresh, both
// after the registration.
const registerOrder =
mockToolRegistry.registerTool.mock.invocationCallOrder[0];
const revealOrder =
mockToolRegistry.revealDeferredTool.mock.invocationCallOrder[0];
const setToolsOrder =
mockGeminiClient.setTools.mock.invocationCallOrder[0];
const setToolsOrder = mockLlmClient.setTools.mock.invocationCallOrder[0];
expect(registerOrder).toBeLessThan(revealOrder);
expect(revealOrder).toBeLessThan(setToolsOrder);
});
@@ -8187,7 +8183,7 @@ describe('Session', () => {
await registerCreateSubSessionTool(mockConfig);
expect(mockToolRegistry.registerTool).not.toHaveBeenCalled();
expect(mockGeminiClient.setTools).not.toHaveBeenCalled();
expect(mockLlmClient.setTools).not.toHaveBeenCalled();
});
it('skips create_sub_session when the permission manager disables it', async () => {
@@ -8255,7 +8251,7 @@ describe('Session', () => {
expect(mockToolRegistry.registerTool).not.toHaveBeenCalled();
expect(mockToolRegistry.revealDeferredTool).not.toHaveBeenCalled();
expect(mockToolRegistry.pinDeferredToolReveal).not.toHaveBeenCalled();
expect(mockGeminiClient.setTools).toHaveBeenCalledTimes(1);
expect(mockLlmClient.setTools).toHaveBeenCalledTimes(1);
});
it('wires the sub-session spawner only on daemon-backed sessions', () => {
@@ -8858,7 +8854,7 @@ describe('Session', () => {
expect(mockChatRecordingService.recordUserMessage).toHaveBeenCalledWith(
'3',
);
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledWith(
'test-session-id########3',
false,
expect.any(AbortSignal),
@@ -9364,7 +9360,7 @@ describe('Session', () => {
chunk.includes('Routing this image turn'),
),
).toBe(true);
expect(mockGeminiClient.tryCompressChat).not.toHaveBeenCalled();
expect(mockLlmClient.tryCompressChat).not.toHaveBeenCalled();
await session.prompt({
sessionId: 'test-session-id',
@@ -9376,7 +9372,7 @@ describe('Session', () => {
expect.any(Object),
expect.any(String),
);
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledOnce();
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledOnce();
});
it('clamps full-turn images before selecting the ACP route', async () => {
@@ -12525,7 +12521,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: 'hello' }],
});
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledWith(
'test-session-id########1',
false,
expect.any(AbortSignal),
@@ -12535,7 +12531,7 @@ describe('Session', () => {
typeof vi.fn
>;
expectCompressBeforeSend(
mockGeminiClient.tryCompressChat,
mockLlmClient.tryCompressChat,
sendMessageStream,
0,
);
@@ -12548,13 +12544,13 @@ describe('Session', () => {
getHistory: vi.fn().mockReturnValue([]),
getHistoryShallow: vi.fn().mockReturnValue([]),
getLastModelMessageText: vi.fn().mockReturnValue(''),
} as unknown as GeminiChat;
} as unknown as LlmChat;
mockChat.sendMessageStream = vi
.fn()
.mockResolvedValue(createEmptyStream());
mockGeminiClient.tryCompressChat.mockImplementation(async () => {
mockGeminiClient.getChat.mockReturnValue(compressedChat);
mockLlmClient.tryCompressChat.mockImplementation(async () => {
mockLlmClient.getChat.mockReturnValue(compressedChat);
return {
originalTokenCount: 1000,
newTokenCount: 200,
@@ -12579,7 +12575,7 @@ describe('Session', () => {
});
it('emits an ACP-visible update when automatic compression succeeds', async () => {
mockGeminiClient.tryCompressChat.mockResolvedValueOnce({
mockLlmClient.tryCompressChat.mockResolvedValueOnce({
originalTokenCount: 1200,
newTokenCount: 450,
compressionStatus: core.CompressionStatus.COMPRESSED,
@@ -12608,7 +12604,7 @@ describe('Session', () => {
});
it('labels the notice as screenshot-triggered when triggerReason is image_overflow', async () => {
mockGeminiClient.tryCompressChat.mockResolvedValueOnce({
mockLlmClient.tryCompressChat.mockResolvedValueOnce({
originalTokenCount: 1200,
newTokenCount: 450,
compressionStatus: core.CompressionStatus.COMPRESSED,
@@ -12638,7 +12634,7 @@ describe('Session', () => {
});
it('marks estimated compression counts in the ACP notice (#9309)', async () => {
mockGeminiClient.tryCompressChat.mockResolvedValueOnce({
mockLlmClient.tryCompressChat.mockResolvedValueOnce({
originalTokenCount: 26600,
newTokenCount: 14500,
originalTokenCountIsEstimated: true,
@@ -12669,7 +12665,7 @@ describe('Session', () => {
});
it('continues sending when automatic compression fails', async () => {
mockGeminiClient.tryCompressChat.mockRejectedValueOnce(
mockLlmClient.tryCompressChat.mockRejectedValueOnce(
new Error('compression rate limited'),
);
mockChat.sendMessageStream = vi
@@ -12681,7 +12677,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: 'hello' }],
});
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledWith(
'test-session-id########1',
false,
expect.any(AbortSignal),
@@ -12702,7 +12698,7 @@ describe('Session', () => {
core.uiTelemetryService,
'getLastPromptTokenCount',
).mockReturnValue(101);
mockGeminiClient.tryCompressChat.mockRejectedValueOnce(
mockLlmClient.tryCompressChat.mockRejectedValueOnce(
new Error('compression rate limited'),
);
mockChat.sendMessageStream = vi
@@ -12731,7 +12727,7 @@ describe('Session', () => {
it('returns cancelled when automatic compression is aborted', async () => {
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat.mockImplementation(
mockLlmClient.tryCompressChat.mockImplementation(
async (_promptId: string, _force: boolean, signal: AbortSignal) =>
new Promise((_, reject) => {
signal.addEventListener('abort', () => {
@@ -12750,7 +12746,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: 'hello' }],
});
await vi.waitFor(() => {
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalled();
expect(mockLlmClient.tryCompressChat).toHaveBeenCalled();
});
await session.cancelPendingPrompt();
@@ -12780,7 +12776,7 @@ describe('Session', () => {
it('surfaces an automatic compression AbortError without an aborted signal', async () => {
const error = new Error('compression transport aborted unexpectedly');
error.name = 'AbortError';
mockGeminiClient.tryCompressChat.mockRejectedValueOnce(error);
mockLlmClient.tryCompressChat.mockRejectedValueOnce(error);
mockChat.sendMessageStream = vi
.fn()
.mockResolvedValue(createEmptyStream());
@@ -12801,7 +12797,7 @@ describe('Session', () => {
core.uiTelemetryService,
'getLastPromptTokenCount',
).mockReturnValue(999);
mockGeminiClient.tryCompressChat.mockResolvedValueOnce({
mockLlmClient.tryCompressChat.mockResolvedValueOnce({
originalTokenCount: 50,
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
@@ -12820,7 +12816,7 @@ describe('Session', () => {
it('falls back to the previous prompt token count when compression returns zero token info', async () => {
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat.mockResolvedValue({
mockLlmClient.tryCompressChat.mockResolvedValue({
originalTokenCount: 0,
newTokenCount: 0,
compressionStatus: core.CompressionStatus.NOOP,
@@ -12860,7 +12856,7 @@ describe('Session', () => {
it('falls back to the previous prompt token count when compressed token info is zero', async () => {
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce({
originalTokenCount: 50,
newTokenCount: 50,
@@ -12906,7 +12902,7 @@ describe('Session', () => {
it('records prompt token count instead of total token count for later session-limit checks', async () => {
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce({
originalTokenCount: 0,
newTokenCount: 0,
@@ -12953,9 +12949,9 @@ describe('Session', () => {
getHistory: vi.fn().mockReturnValue([]),
getHistoryShallow: vi.fn().mockReturnValue([]),
getLastModelMessageText: vi.fn().mockReturnValue(''),
} as unknown as GeminiChat;
} as unknown as LlmChat;
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce({
originalTokenCount: 50,
newTokenCount: 50,
@@ -12983,7 +12979,7 @@ describe('Session', () => {
}),
).resolves.toEqual({ stopReason: 'end_turn' });
mockGeminiClient.getChat.mockReturnValue(clearedChat);
mockLlmClient.getChat.mockReturnValue(clearedChat);
await expect(
session.prompt({
@@ -12996,7 +12992,7 @@ describe('Session', () => {
});
it('continues sending when the compression notification fails', async () => {
mockGeminiClient.tryCompressChat.mockResolvedValueOnce({
mockLlmClient.tryCompressChat.mockResolvedValueOnce({
originalTokenCount: 1200,
newTokenCount: 450,
compressionStatus: core.CompressionStatus.COMPRESSED,
@@ -13019,7 +13015,7 @@ describe('Session', () => {
it('stops before sending when the compressed prompt exceeds the session token limit', async () => {
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat.mockResolvedValueOnce({
mockLlmClient.tryCompressChat.mockResolvedValueOnce({
originalTokenCount: 1200,
newTokenCount: 101,
compressionStatus: core.CompressionStatus.COMPRESSED,
@@ -13035,7 +13031,7 @@ describe('Session', () => {
}),
).resolves.toEqual({ stopReason: 'max_tokens' });
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalled();
expect(mockLlmClient.tryCompressChat).toHaveBeenCalled();
expect(mockChat.sendMessageStream).not.toHaveBeenCalled();
expect(mockChat.addHistory).not.toHaveBeenCalled();
expect(mockClient.sessionUpdate).not.toHaveBeenCalledWith({
@@ -13066,7 +13062,7 @@ describe('Session', () => {
it('stops without throwing when the token-limit diagnostic fails', async () => {
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat.mockResolvedValueOnce({
mockLlmClient.tryCompressChat.mockResolvedValueOnce({
originalTokenCount: 101,
newTokenCount: 101,
compressionStatus: core.CompressionStatus.NOOP,
@@ -13135,8 +13131,8 @@ describe('Session', () => {
});
expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(2);
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockGeminiClient.tryCompressChat).toHaveBeenNthCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockLlmClient.tryCompressChat).toHaveBeenNthCalledWith(
2,
'test-session-id########1',
false,
@@ -13147,7 +13143,7 @@ describe('Session', () => {
typeof vi.fn
>;
expectCompressBeforeSend(
mockGeminiClient.tryCompressChat,
mockLlmClient.tryCompressChat,
sendMessageStream,
1,
);
@@ -15247,7 +15243,7 @@ describe('Session', () => {
mockToolRegistry.getTool.mockReturnValue(tool);
mockConfig.getApprovalMode = vi.fn().mockReturnValue(ApprovalMode.YOLO);
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce({
originalTokenCount: 50,
newTokenCount: 50,
@@ -15286,8 +15282,8 @@ describe('Session', () => {
).resolves.toEqual({ stopReason: 'max_tokens' });
expect(executeSpy).toHaveBeenCalledTimes(1);
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockGeminiClient.tryCompressChat).toHaveBeenNthCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockLlmClient.tryCompressChat).toHaveBeenNthCalledWith(
2,
'test-session-id########1',
false,
@@ -15359,7 +15355,7 @@ describe('Session', () => {
});
expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(2);
expect(mockGeminiClient.tryCompressChat).toHaveBeenNthCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenNthCalledWith(
2,
'test-session-id########1_stop_hook_1',
false,
@@ -15370,7 +15366,7 @@ describe('Session', () => {
typeof vi.fn
>;
expectCompressBeforeSend(
mockGeminiClient.tryCompressChat,
mockLlmClient.tryCompressChat,
sendMessageStream,
1,
);
@@ -15424,14 +15420,14 @@ describe('Session', () => {
});
expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(3);
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockGeminiClient.tryCompressChat).toHaveBeenNthCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockLlmClient.tryCompressChat).toHaveBeenNthCalledWith(
2,
'test-session-id########1_stop_hook_1',
false,
expect.any(AbortSignal),
);
expect(mockGeminiClient.tryCompressChat).not.toHaveBeenCalledWith(
expect(mockLlmClient.tryCompressChat).not.toHaveBeenCalledWith(
'test-session-id########1_stop_hook_2',
false,
expect.any(AbortSignal),
@@ -15470,7 +15466,7 @@ describe('Session', () => {
.fn()
.mockImplementation((eventName: string) => eventName === 'Stop');
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce({
originalTokenCount: 50,
newTokenCount: 50,
@@ -15512,8 +15508,8 @@ describe('Session', () => {
}),
).resolves.toEqual({ stopReason: 'max_tokens' });
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockGeminiClient.tryCompressChat).toHaveBeenNthCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockLlmClient.tryCompressChat).toHaveBeenNthCalledWith(
2,
'test-session-id########1_stop_hook_1',
false,
@@ -15564,13 +15560,13 @@ describe('Session', () => {
});
expect(scheduler.start).toHaveBeenCalledTimes(1);
expect(mockGeminiClient.tryCompressChat).toHaveBeenNthCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenNthCalledWith(
1,
'test-session-id########1',
false,
expect.any(AbortSignal),
);
expect(mockGeminiClient.tryCompressChat).toHaveBeenNthCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenNthCalledWith(
2,
expect.stringMatching(/^test-session-id########cron\d+$/),
false,
@@ -15581,7 +15577,7 @@ describe('Session', () => {
typeof vi.fn
>;
expectCompressBeforeSend(
mockGeminiClient.tryCompressChat,
mockLlmClient.tryCompressChat,
sendMessageStream,
1,
);
@@ -16332,7 +16328,7 @@ describe('Session', () => {
it('does not submit delivery when the prompt hits the token limit', async () => {
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat.mockResolvedValueOnce({
mockLlmClient.tryCompressChat.mockResolvedValueOnce({
originalTokenCount: 101,
newTokenCount: 101,
compressionStatus: core.CompressionStatus.NOOP,
@@ -18514,7 +18510,7 @@ describe('Session', () => {
// Compress on the SECOND cron tick only — keyed on the cron promptId so
// the user 'hello' prompt's compression check stays a no-op.
let cronCompressions = 0;
mockGeminiClient.tryCompressChat = vi
mockLlmClient.tryCompressChat = vi
.fn()
.mockImplementation(async (promptId: string) => {
const isCron = String(promptId).includes('cron');
@@ -18595,7 +18591,7 @@ describe('Session', () => {
mockConfig.isCronEnabled = vi.fn().mockReturnValue(true);
mockConfig.getCronScheduler = vi.fn().mockReturnValue(scheduler);
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce({
originalTokenCount: 50,
newTokenCount: 50,
@@ -18616,11 +18612,11 @@ describe('Session', () => {
});
await vi.waitFor(() => {
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledTimes(2);
});
expect(scheduler.start).toHaveBeenCalledTimes(1);
expect(mockGeminiClient.tryCompressChat).toHaveBeenNthCalledWith(
expect(mockLlmClient.tryCompressChat).toHaveBeenNthCalledWith(
2,
expect.stringMatching(/^test-session-id########cron\d+$/),
false,
@@ -18679,7 +18675,7 @@ describe('Session', () => {
cronCallback?.({ prompt: 'scheduled prompt again' });
await Promise.resolve();
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledTimes(2);
expect(tokenLimitDiagnosticCount()).toBe(diagnosticCountBefore);
});
@@ -18698,7 +18694,7 @@ describe('Session', () => {
prompt: [{ type: 'text', text: '/compress' }],
});
expect(mockGeminiClient.tryCompressChat).not.toHaveBeenCalled();
expect(mockLlmClient.tryCompressChat).not.toHaveBeenCalled();
expect(mockChat.sendMessageStream).not.toHaveBeenCalled();
expect(mockConfig.startActiveTodoWorkChain).not.toHaveBeenCalled();
expect(mockClient.sessionUpdate).toHaveBeenCalledWith({
@@ -20818,7 +20814,7 @@ describe('Session', () => {
it('hands off a preempted Goal turn whose stream throws on abort', async () => {
// Same user action as the test above, but the preempted stream
// rejects out of the model network await instead of ending cleanly --
// which is what geminiChat actually does. Where the abort lands is
// which is what llmChat actually does. Where the abort lands is
// pure timing, so both spellings have to settle the same way: a
// handoff via finishTurn, never a pause. Pausing here would persist
// the goal as paused and silently stop the autonomous loop.
@@ -20894,7 +20890,7 @@ describe('Session', () => {
);
}
// The one difference from the sibling test: the abort lands
// inside the model network await, so geminiChat rejects instead
// inside the model network await, so llmChat rejects instead
// of handing back a stream that ends cleanly.
throw Object.assign(new Error('The operation was aborted'), {
name: 'AbortError',
@@ -23477,9 +23473,9 @@ describe('Session', () => {
denialState = next;
});
mockConfig.getBaseLlmClient = vi.fn().mockReturnValue(baseLlmClient);
mockConfig.getGeminiClient = vi
mockConfig.getLlmClient = vi
.fn()
.mockReturnValue({ ...mockGeminiClient, getHistoryTail });
.mockReturnValue({ ...mockLlmClient, getHistoryTail });
mockConfig.getAutoModeSettings = vi.fn().mockReturnValue({});
mockConfig.getModel = vi.fn().mockReturnValue('test-model');
mockConfig.getDisableAllHooks = vi.fn().mockReturnValue(true);
@@ -23579,9 +23575,9 @@ describe('Session', () => {
denialState = next;
});
mockConfig.getBaseLlmClient = vi.fn().mockReturnValue(baseLlmClient);
mockConfig.getGeminiClient = vi
mockConfig.getLlmClient = vi
.fn()
.mockReturnValue({ ...mockGeminiClient, getHistoryTail });
.mockReturnValue({ ...mockLlmClient, getHistoryTail });
mockConfig.getAutoModeSettings = vi.fn().mockReturnValue({});
mockConfig.getModel = vi.fn().mockReturnValue('test-model');
mockConfig.getDisableAllHooks = vi.fn().mockReturnValue(true);
@@ -23684,9 +23680,9 @@ describe('Session', () => {
denialState = next;
});
mockConfig.getBaseLlmClient = vi.fn().mockReturnValue(baseLlmClient);
mockConfig.getGeminiClient = vi
mockConfig.getLlmClient = vi
.fn()
.mockReturnValue({ ...mockGeminiClient, getHistoryTail });
.mockReturnValue({ ...mockLlmClient, getHistoryTail });
mockConfig.getAutoModeSettings = vi.fn().mockReturnValue({});
mockConfig.getModel = vi.fn().mockReturnValue('test-model');
mockConfig.getDisableAllHooks = vi.fn().mockReturnValue(true);
@@ -23802,9 +23798,9 @@ describe('Session', () => {
denialState = next;
});
mockConfig.getBaseLlmClient = vi.fn().mockReturnValue(baseLlmClient);
mockConfig.getGeminiClient = vi
mockConfig.getLlmClient = vi
.fn()
.mockReturnValue({ ...mockGeminiClient, getHistoryTail });
.mockReturnValue({ ...mockLlmClient, getHistoryTail });
mockConfig.getAutoModeSettings = vi.fn().mockReturnValue({});
mockConfig.getModel = vi.fn().mockReturnValue('test-model');
mockConfig.getDisableAllHooks = vi.fn().mockReturnValue(true);
@@ -23926,7 +23922,7 @@ describe('Session', () => {
});
mockConfig.setAutoModeDenialState = setAutoModeDenialState;
(
mockGeminiClient as unknown as {
mockLlmClient as unknown as {
getHistoryTail: ReturnType<typeof vi.fn>;
}
).getHistoryTail = vi.fn().mockReturnValue([]);
@@ -24320,7 +24316,7 @@ describe('Session', () => {
expect(mockChat.sendMessageStream).not.toHaveBeenCalled();
expect(result.stopReason).toBe('end_turn');
expect(
mockGeminiClient.beginManagedAutoMemoryRecall,
mockLlmClient.beginManagedAutoMemoryRecall,
).not.toHaveBeenCalled();
expect(mockMemoryManager.scheduleExtract).not.toHaveBeenCalled();
});
@@ -24364,7 +24360,7 @@ describe('Session', () => {
).toBe(true);
expect(textParts(sent).at(-1)).toContain('extra hook context');
expect(
mockGeminiClient.beginManagedAutoMemoryRecall,
mockLlmClient.beginManagedAutoMemoryRecall,
).toHaveBeenCalledWith('hello', expect.any(AbortSignal));
});
});
@@ -31847,7 +31843,7 @@ describe('Session', () => {
it('preserves feature-off Stop hook loop reporting before token rejection', async () => {
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockConfig.getStopHookBlockingCap = vi.fn().mockReturnValue(3);
mockGeminiClient.tryCompressChat.mockResolvedValue({
mockLlmClient.tryCompressChat.mockResolvedValue({
originalTokenCount: 50,
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
@@ -32639,7 +32635,7 @@ describe('Session', () => {
},
);
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce({
originalTokenCount: 50,
newTokenCount: 50,
@@ -33106,7 +33102,7 @@ describe('Session', () => {
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
};
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce(noCompression)
.mockResolvedValueOnce(noCompression)
.mockImplementationOnce(async () => {
@@ -33156,7 +33152,7 @@ describe('Session', () => {
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
};
mockGeminiClient.tryCompressChat.mockImplementation(async () => {
mockLlmClient.tryCompressChat.mockImplementation(async () => {
events.push('compression');
return noCompression;
});
@@ -33201,7 +33197,7 @@ describe('Session', () => {
expect(visionAfterDrainIndex).toBeLessThan(events.indexOf('claim'));
expect(events.filter((event) => event === 'compression')).toHaveLength(1);
expect(events.indexOf('compression')).toBeLessThan(imageDrainIndex);
expect(mockGeminiClient.tryCompressChat).toHaveBeenCalledOnce();
expect(mockLlmClient.tryCompressChat).toHaveBeenCalledOnce();
for (const call of vi
.mocked(mockChat.sendMessageStream)
.mock.calls.slice(1)) {
@@ -33226,7 +33222,7 @@ describe('Session', () => {
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
};
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce(noCompression)
.mockResolvedValueOnce(noCompression)
.mockImplementationOnce(async () => {
@@ -33260,7 +33256,7 @@ describe('Session', () => {
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
};
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce(noCompression)
.mockResolvedValueOnce(noCompression)
.mockResolvedValueOnce({
@@ -33320,7 +33316,7 @@ describe('Session', () => {
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
};
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce(noCompression)
.mockResolvedValueOnce(noCompression)
.mockRejectedValueOnce(new Error('compression unavailable'))
@@ -33397,7 +33393,7 @@ describe('Session', () => {
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
};
mockGeminiClient.tryCompressChat
mockLlmClient.tryCompressChat
.mockResolvedValueOnce(noCompression)
.mockResolvedValueOnce(noCompression)
.mockResolvedValueOnce({
@@ -33610,7 +33606,7 @@ describe('Session', () => {
getUserContentPushCount: () => number;
}
).getUserContentPushCount = vi.fn(() => userContentPushCount);
const replacementChat = Object.create(mockChat) as GeminiChat;
const replacementChat = Object.create(mockChat) as LlmChat;
const replacementAddHistory = vi.fn();
replacementChat.addHistory = replacementAddHistory;
replacementChat.getUserContentPushCount = vi.fn(
@@ -33664,9 +33660,9 @@ describe('Session', () => {
},
);
if (replaceChatAfterCompression) {
mockGeminiClient.tryCompressChat.mockImplementation(async () => {
mockLlmClient.tryCompressChat.mockImplementation(async () => {
if (vi.mocked(mockChat.sendMessageStream).mock.calls.length === 3) {
mockGeminiClient.getChat.mockReturnValue(replacementChat);
mockLlmClient.getChat.mockReturnValue(replacementChat);
}
return {
originalTokenCount: 0,
@@ -36923,7 +36919,7 @@ describe('Session', () => {
installPendingTodoTool();
mockConfig.getSessionTokenLimit = vi.fn().mockReturnValue(100);
mockConfig.getStopHookBlockingCap = vi.fn().mockReturnValue(3);
mockGeminiClient.tryCompressChat.mockResolvedValue({
mockLlmClient.tryCompressChat.mockResolvedValue({
originalTokenCount: 50,
newTokenCount: 50,
compressionStatus: core.CompressionStatus.NOOP,
@@ -17,7 +17,7 @@ import type {
} from '@google/genai';
import type {
Config,
GeminiChat,
LlmChat,
ToolCallConfirmationDetails,
ToolConfirmationPayload,
ToolResult,
@@ -1695,7 +1695,7 @@ export async function registerCreateSubSessionTool(
ToolNames.CREATE_SUB_SESSION,
async () => new CreateSubSessionTool(config),
);
await config.getGeminiClient().setTools();
await config.getLlmClient().setTools();
return;
}
toolRegistry.registerTool(new CreateSubSessionTool(config));
@@ -1711,7 +1711,7 @@ export async function registerCreateSubSessionTool(
// those configurations.
toolRegistry.revealDeferredTool(ToolNames.CREATE_SUB_SESSION);
toolRegistry.pinDeferredToolReveal(ToolNames.CREATE_SUB_SESSION);
await config.getGeminiClient().setTools();
await config.getLlmClient().setTools();
}
export interface AvailableCommandsSnapshot {
@@ -1937,7 +1937,7 @@ export class Session implements SessionContext {
private cronCompletion: Promise<void> | null = null;
private cronDisabledByTokenLimit = false;
private lastPromptTokenCount = 0;
private lastPromptTokenCountChat: GeminiChat | null = null;
private lastPromptTokenCountChat: LlmChat | null = null;
private midTurnDrainUnavailable = false;
private midTurnDrainTimeoutStrikes = 0;
// ACP can continue one logical conversation through prompt, cron, and
@@ -3270,11 +3270,11 @@ export class Session implements SessionContext {
}
async #syncLiveToolDeclarations(): Promise<void> {
const geminiClient = this.config.getGeminiClient();
if (!geminiClient) {
const llmClient = this.config.getLlmClient();
if (!llmClient) {
throw new Error('The Live backend model client is unavailable.');
}
await geminiClient.setTools();
await llmClient.setTools();
}
async setLiveConversationActive(active: boolean): Promise<void> {
@@ -3292,7 +3292,7 @@ export class Session implements SessionContext {
this.liveEndInstructionPending = true;
this.config.setLiveAppendSystemPrompt(LIVE_BACKEND_END_INSTRUCTIONS);
}
await this.config.getGeminiClient()?.refreshSystemInstruction();
await this.config.getLlmClient()?.refreshSystemInstruction();
}
async appendLiveConversationTranscript(
@@ -3341,7 +3341,7 @@ export class Session implements SessionContext {
if (this.liveConversationActive || !this.liveEndInstructionPending) return;
this.liveEndInstructionPending = false;
this.config.setLiveAppendSystemPrompt(undefined);
await this.config.getGeminiClient()?.refreshSystemInstruction();
await this.config.getLlmClient()?.refreshSystemInstruction();
}
getId(): string {
@@ -3924,7 +3924,7 @@ export class Session implements SessionContext {
);
}
const chat = this.config.getGeminiClient()!.getChat();
const chat = this.config.getLlmClient()!.getChat();
const apiHistory = chat.getHistoryShallow();
const apiTruncateIndex = this.#computeApiTruncationIndexForUserTurn(
apiHistory,
@@ -3975,7 +3975,7 @@ export class Session implements SessionContext {
}
captureHistorySnapshot(): Content[] {
return this.config.getGeminiClient()!.getChat().getHistoryShallow();
return this.config.getLlmClient()!.getChat().getHistoryShallow();
}
getRewindableUserTurnCount(): number {
@@ -4002,10 +4002,7 @@ export class Session implements SessionContext {
);
}
this.config
.getGeminiClient()!
.getChat()
.setHistory(structuredClone(history));
this.config.getLlmClient()!.getChat().setHistory(structuredClone(history));
this.activeTodoPlanRevision = undefined;
this.#clearTodoStopGuardTrustAndDrainAutomaticQueues();
}
@@ -4595,8 +4592,8 @@ export class Session implements SessionContext {
accepted: boolean;
interruption: 'none' | 'interrupted_prompt' | 'interrupted_turn';
}> {
const geminiClient = this.config.getGeminiClient();
if (!geminiClient || !geminiClient.isInitialized()) {
const llmClient = this.config.getLlmClient();
if (!llmClient || !llmClient.isInitialized()) {
return { accepted: false, interruption: 'none' };
}
@@ -4679,7 +4676,7 @@ export class Session implements SessionContext {
if (this.settings.merged.ui?.enableFollowupSuggestions === false) return;
if (this.config.getApprovalMode() === ApprovalMode.PLAN) return;
const chat = this.config.getGeminiClient()?.getChat();
const chat = this.config.getLlmClient()?.getChat();
if (!chat) return;
const ac = new AbortController();
@@ -4826,7 +4823,7 @@ export class Session implements SessionContext {
// it, `qwen review fetch-pr` cannot record its worktree lease and an
// interrupted /review leaves the review worktree behind. TUI and
// headless enter this context at their prompt entry points
// (useGeminiStream.ts / nonInteractiveCli.ts); ACP had no equivalent.
// (use-llm-stream.ts / nonInteractiveCli.ts); ACP had no equivalent.
// enterWith (not run) so the 500-line turn body below stays unnested;
// the binding dies with this async scope.
promptIdContext.enterWith(promptId);
@@ -5217,7 +5214,7 @@ export class Session implements SessionContext {
if (isFreshUserTurn) {
managedMemoryRecallStarted = true;
this.config
.getGeminiClient()
.getLlmClient()
.beginManagedAutoMemoryRecall(promptText, pendingSend.signal);
}
@@ -5233,7 +5230,7 @@ export class Session implements SessionContext {
this.activeTodoWorkChainPromptId = promptId;
// Snapshot file state before this turn (mirrors the makeSnapshot
// block in GeminiClient.sendMessageStream). Placed after
// block in LlmClient.sendMessageStream). Placed after
// slash-command and hook early-returns so locally handled commands
// don't create phantom snapshots that desync the snapshot index.
// Restore continuations record no user message; rewindToTurn()
@@ -5261,13 +5258,13 @@ export class Session implements SessionContext {
// Prepend session-level system reminders (plan mode / subagent /
// arena) so the model sees them, matching the behaviour of
// `GeminiClient.sendMessageStream` in the CLI/TUI path. Without this,
// `LlmClient.sendMessageStream` in the CLI/TUI path. Without this,
// plan mode in ACP has no effect because the model never learns it
// should avoid edits.
const systemReminders = await this.#buildInitialSystemReminders();
if (isFreshUserTurn) {
const memory = await this.config
.getGeminiClient()
.getLlmClient()
.consumeManagedAutoMemoryRecall('initial');
if (memory?.prompt) {
systemReminders.unshift({ text: memory.prompt });
@@ -5359,7 +5356,7 @@ export class Session implements SessionContext {
// not just the stop-hook loop. Daemon turns run autonomously in
// all approval modes (approvals are mediated by the ACP client
// rather than by gating this loop), so unlike the CLI reference
// (useGeminiStream.ts, which only emits in YOLO) this is
// (use-llm-stream.ts, which only emits in YOLO) this is
// intentionally emitted for every mode.
try {
if (isRestoreAskUserQuestion) {
@@ -5686,7 +5683,7 @@ export class Session implements SessionContext {
this.todoStopGuard.pauseForTrustedRetry();
// Fire StopFailure hook (fire-and-forget, replaces Stop event for API errors)
// Aligned with useGeminiStream.ts handleFinishedWithErrorEvent
// Aligned with use-llm-stream.ts handleFinishedWithErrorEvent
const errorStatus = getErrorStatus(error);
const errorMessage =
error instanceof Error ? error.message : String(error);
@@ -5898,7 +5895,7 @@ export class Session implements SessionContext {
},
).finally(() => {
if (managedMemoryRecallStarted) {
this.config.getGeminiClient().finishManagedAutoMemoryRecall();
this.config.getLlmClient().finishManagedAutoMemoryRecall();
}
});
}
@@ -6315,7 +6312,7 @@ export class Session implements SessionContext {
| ChannelDeliveryResponseBlock
| undefined;
let channelDeliveryCheckpoint = 0;
let providerSendChat: GeminiChat | undefined;
let providerSendChat: LlmChat | undefined;
let userContentPushCountBeforeSend = 0;
try {
@@ -7089,8 +7086,8 @@ export class Session implements SessionContext {
}
}
#getCurrentChat(): GeminiChat {
return this.config.getGeminiClient()!.getChat();
#getCurrentChat(): LlmChat {
return this.config.getLlmClient()!.getChat();
}
async #runWithFullTurnModel<T>(
@@ -7109,9 +7106,9 @@ export class Session implements SessionContext {
/**
* Create the MessageDisplay hook dispatcher for one model call's streamed
* reply, or null when the hook isn't registered (the common case — keeps
* the streaming loops zero-cost). The ACP surface consumes GeminiChat's
* the streaming loops zero-cost). The ACP surface consumes LlmChat's
* raw stream directly rather than going through
* GeminiClient.sendMessageStream, so it has to fire this hook itself —
* LlmClient.sendMessageStream, so it has to fire this hook itself —
* with the same contract as the terminal UI path in client.ts: debounced
* cumulative text, one message_id per model call, and an is_final firing
* on every non-aborted exit (delivered by awaiting `finish()` in a
@@ -7157,7 +7154,7 @@ export class Session implements SessionContext {
) => Promise<BeforeModelSendDecision>;
} = {},
): Promise<AutoCompressionSendResult> {
const geminiClient = this.config.getGeminiClient()!;
const llmClient = this.config.getLlmClient()!;
if (options.prepareBeforeCompression) {
const decision = await options.prepareBeforeCompression();
if (decision.kind === 'stop') {
@@ -7181,7 +7178,7 @@ export class Session implements SessionContext {
!(options.getModelOverride?.() ?? options.modelOverride)
) {
try {
const compressed = await geminiClient.tryCompressChat(
const compressed = await llmClient.tryCompressChat(
promptId,
false,
abortSignal,
@@ -7293,7 +7290,7 @@ export class Session implements SessionContext {
if (message[0]?.functionResponse) {
const memory =
await geminiClient.consumeManagedAutoMemoryRecall('tool_result');
await llmClient.consumeManagedAutoMemoryRecall('tool_result');
if (memory?.prompt) {
message = insertAfterFunctionResponses(message, [
{ text: memory.prompt },
@@ -10360,9 +10357,14 @@ export class Session implements SessionContext {
/**
* Assemble the per-turn system reminders the model needs to see at the
* start of a user query or cron fire. Mirrors the subagent/plan/arena
* branches in `GeminiClient.sendMessageStream` (`client.ts:848-878`) —
* branches in `LlmClient.sendMessageStream` (`client.ts:848-878`) —
* the ACP path bypasses that code, so without this helper plan mode is
* silently inert and subagent/arena sessions lose context.
*
* Scope note: the `relevantAutoMemory` reminder is intentionally NOT
* included here. Managed auto-memory requires a prefetch pipeline that
* lives in `LlmClient`, and porting it into the ACP path is tracked
* separately as part of the broader middleware-alignment work.
*/
async #buildInitialSystemReminders(): Promise<Part[]> {
const reminders: Part[] = [];
@@ -11011,7 +11013,7 @@ export class Session implements SessionContext {
// Parallels coreToolScheduler.ts.
const messages =
this.config
.getGeminiClient?.()
.getLlmClient?.()
?.getHistoryTail(MAX_TRANSCRIPT_MESSAGES, false) ?? [];
const decision = await evaluateAutoMode({
ctx: pmCtx,
@@ -12447,7 +12449,7 @@ export class Session implements SessionContext {
});
} finally {
if (terminalStatus && terminalStatus !== 'cancelled') {
this.config.getGeminiClient().recordCompletedToolCall(toolName, args);
this.config.getLlmClient().recordCompletedToolCall(toolName, args);
}
if (terminalStatus === 'cancelled') {
endToolSpan(toolSpan, { success: false, cancelled: true });
@@ -18,7 +18,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Session } from './Session.js';
import type { Config, GeminiChat } from '@qwen-code/qwen-code-core';
import type { Config, LlmChat } from '@qwen-code/qwen-code-core';
import {
ApprovalMode,
AuthType,
@@ -66,7 +66,7 @@ describe('Session.pendingWorktreeNotice', () => {
/** Parts arrays captured on each sendMessageStream call. */
let capturedMessages: unknown[][];
let mockChat: GeminiChat;
let mockChat: LlmChat;
let mockConfig: Config;
let mockClient: AgentSideConnection;
let mockSettings: LoadedSettings;
@@ -93,9 +93,9 @@ describe('Session.pendingWorktreeNotice', () => {
setHistory: vi.fn(),
truncateHistory: vi.fn(),
stripThoughtsFromHistory: vi.fn(),
} as unknown as GeminiChat;
} as unknown as LlmChat;
const mockGeminiClient = {
const mockLlmClient = {
getChat: vi.fn().mockReturnValue(mockChat),
tryCompressChat: vi.fn().mockResolvedValue({
originalTokenCount: 0,
@@ -163,7 +163,7 @@ describe('Session.pendingWorktreeNotice', () => {
getAuthType: vi.fn().mockReturnValue(AuthType.USE_OPENAI),
isCronEnabled: vi.fn().mockReturnValue(false),
getSessionTokenLimit: vi.fn().mockReturnValue(0),
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
getLlmClient: vi.fn().mockReturnValue(mockLlmClient),
getManagedAutoMemoryEnabled: vi.fn().mockReturnValue(false),
getDisableAllHooks: vi.fn().mockReturnValue(true),
hasHooksForEvent: vi.fn().mockReturnValue(false),
@@ -211,7 +211,7 @@ describe('history replay page', () => {
// getChat() THROWS there. The skip probe must guard on isInitialized().
const config = {
getRestoreAskUserQuestion: () => true,
getGeminiClient: () => ({
getLlmClient: () => ({
isInitialized: () => false,
getChat: () => {
throw new Error('Chat not initialized');
@@ -262,7 +262,7 @@ describe('history replay page', () => {
};
const config = {
getRestoreAskUserQuestion: () => true,
getGeminiClient: () => ({
getLlmClient: () => ({
isInitialized: () => true,
getChat: () => ({ peekLastHistoryEntry: () => lastEntry }),
}),
@@ -292,7 +292,7 @@ export async function collectHistoryReplayUpdates({
suppressRestoreAskUserQuestion !== true &&
config?.getRestoreAskUserQuestion?.() === true
) {
const replayClient = config.getGeminiClient?.();
const replayClient = config.getLlmClient?.();
const lastHistoryContent =
replayClient?.isInitialized?.() === true
? (replayClient.getChat?.()?.peekLastHistoryEntry?.() ??
@@ -1606,7 +1606,7 @@ describe('collectHistoryReplayUpdates restore skip', () => {
it('skips finalize from the transcript tail when chat is not initialized', async () => {
const config = {
getRestoreAskUserQuestion: () => true,
getGeminiClient: () => ({ isInitialized: () => false }),
getLlmClient: () => ({ isInitialized: () => false }),
} as unknown as Config;
const replay = await collectHistoryReplayUpdates({
@@ -1624,7 +1624,7 @@ describe('collectHistoryReplayUpdates restore skip', () => {
it('finalizes when restore skip is suppressed', async () => {
const config = {
getRestoreAskUserQuestion: () => true,
getGeminiClient: () => ({ isInitialized: () => false }),
getLlmClient: () => ({ isInitialized: () => false }),
} as unknown as Config;
const replay = await collectHistoryReplayUpdates({
@@ -45,7 +45,7 @@ const { LlmRewriter } = await import('./LlmRewriter.js');
function makeConfig(): Config {
return {
getModel: () => 'test-model',
getGeminiClient: () => ({}),
getLlmClient: () => ({}),
} as unknown as Config;
}
+3 -3
View File
@@ -51,7 +51,7 @@ const mocks = vi.hoisted(() => ({
installManagedNpmUpdate: vi.fn(),
}));
vi.mock('./gemini.js', () => ({
vi.mock('./llm.js', () => ({
main: mocks.main,
}));
@@ -513,8 +513,8 @@ describe('bootstrap import boundaries', () => {
expect(source).not.toContain("import yargs from 'yargs'");
expect(source).not.toContain("from '@qwen-code/qwen-code-core'");
expect(source).not.toContain("import './gemini.js'");
expect(source).not.toContain("import { main } from './gemini.js'");
expect(source).not.toContain("import './llm.js'");
expect(source).not.toContain("import { main } from './llm.js'");
expect(source).not.toContain("from './utils/acp-startup-profiler.js'");
});
+2 -2
View File
@@ -398,7 +398,7 @@ export async function runCliEntry(
: undefined;
acpStartupProfiler?.initializeAcpStartupProfiler();
acpStartupProfiler?.markAcpStartup('geminiImportStart');
const { main } = await import('./gemini.js');
const { main } = await import('./llm.js');
acpStartupProfiler?.markAcpStartup('geminiImportEnd');
await main();
}
@@ -512,7 +512,7 @@ export function stampCliEntryEnv(entryPath?: string): void {
// handleUncaughtException and isExpectedPtyRaceError live in
// ./utils/uncaught-exception-handler.js and are re-exported here for existing
// importers (cli.test.ts). gemini.tsx must import them from that leaf module
// importers (cli.test.ts). llm.tsx must import them from that leaf module
// directly: a static import of this entry file from a module the bundle loads
// lazily makes esbuild hoist this entry into a shared chunk, which silently
// disables the bootstrap guard at the bottom.
+1 -1
View File
@@ -46,7 +46,7 @@ import { HEADLESS_YOLO_NO_SANDBOX_WARNING } from '../utils/headlessSafetyWarning
* Pause the current async function indefinitely. Used after the daemon
* listener is up so yargs `parse()` never resolves — if it did, the
* top-level CLI would fall through to the interactive (TUI) entry point
* in `gemini.tsx`. SIGINT / SIGTERM / SIGHUP in `runQwenServe` is the sole
* in `llm.tsx`. SIGINT / SIGTERM / SIGHUP in `runQwenServe` is the sole
* exit route.
*/
function blockForever(): Promise<never> {
+1 -1
View File
@@ -521,7 +521,7 @@ describe('parseArguments', () => {
it('rejects --json-schema combined with --input-format stream-json', async () => {
// The "first valid structured_output call ends the session"
// contract is incompatible with the long-lived stream-json input
// protocol. Also load-bearing: gemini.tsx's
// protocol. Also load-bearing: llm.tsx's
// `process.exit(process.exitCode ?? 0)` plumbing in the stream-json
// branch explicitly relies on this rejection holding. Pair with
// --output-format stream-json because input/output formats must
+2 -2
View File
@@ -1535,7 +1535,7 @@ export async function loadCliConfig(
*/
sessionMcpServers?: Record<string, MCPServerConfig>,
/**
* Lifecycle handle for the settings file watcher started in `gemini.tsx`
* Lifecycle handle for the settings file watcher started in `llm.tsx`
* before `Config.initialize()`. Passed through to `Config` so it can be
* stopped during shutdown — only `stopWatching()` is exposed here to keep
* core decoupled from the CLI-owned `SettingsWatcher` implementation.
@@ -2005,7 +2005,7 @@ export async function loadCliConfig(
if (argv.resume) {
// By the time we get here, argv.resume has been resolved to a valid
// session UUID by gemini.tsx (which handles custom title lookup and
// session UUID by llm.tsx (which handles custom title lookup and
// the interactive picker for ambiguous matches).
sessionId = argv.resume;
deferProjectionUntilWriterLease =
+1 -1
View File
@@ -2222,7 +2222,7 @@ describe('Settings Loading and Merging', () => {
const warnings = getSettingsWarnings(result);
// Corruption warning no longer goes through migrationWarnings —
// it is emitted via settings.corruptedPath check in gemini.tsx
// it is emitted via settings.corruptedPath check in llm.tsx
// early stderr path instead. Verify corruptedPath is set.
expect(result.corruptedPath).toBeDefined();
expect(warnings.some((w) => w.includes('invalid JSON'))).toBe(false);
@@ -13,7 +13,7 @@ import {
} from 'node:fs';
import type {
Config,
ServerGeminiStreamEvent,
ServerLlmStreamEvent,
ToolCallRequestInfo,
ToolCallResponseInfo,
} from '@qwen-code/qwen-code-core';
@@ -205,7 +205,7 @@ export class DualOutputBridge {
}
}
processEvent(event: ServerGeminiStreamEvent): void {
processEvent(event: ServerLlmStreamEvent): void {
if (!this.active) return;
this.disableIfBufferOverflowed();
if (!this.active) return;
@@ -28,7 +28,7 @@ import {
registerLspHotReload,
setupUnhandledRejectionHandler,
validateDnsResolutionOrder,
} from './gemini.js';
} from './llm.js';
import { startInteractiveUI } from './ui/startInteractiveUI.js';
import { clearCiEnv } from './test-utils/ci-env.js';
import type { CliArgs } from './config/config.js';
@@ -74,7 +74,7 @@ const sessionRegistryConfigStub = {
describe('gemini import boundary', () => {
it('does not statically import ACP or noninteractive auth branches', () => {
const source = readFileSync('src/gemini.tsx', 'utf8');
const source = readFileSync('src/llm.tsx', 'utf8');
expect(source).not.toContain(
"import { runAcpAgent } from './acp-integration/acpAgent.js'",
@@ -292,7 +292,7 @@ function withLspDisabledConfig<T extends object>(
};
}
describe('gemini.tsx main function', () => {
describe('llm.tsx main function', () => {
let originalEnvGeminiSandbox: string | undefined;
let originalEnvSandbox: string | undefined;
let originalEnvQwenSandboxImage: string | undefined;
@@ -1633,7 +1633,7 @@ describe('gemini.tsx main function', () => {
});
});
describe('gemini.tsx main function kitty protocol', () => {
describe('llm.tsx main function kitty protocol', () => {
let originalEnvNoRelaunch: string | undefined;
let setRawModeSpy: MockInstance<
(mode: boolean) => NodeJS.ReadStream & { fd: 0 }
@@ -2514,7 +2514,7 @@ describe('gemini.tsx main function kitty protocol', () => {
// The synthetic structured_output tool only terminates the run inside
// runNonInteractive. In TUI mode it's an inert tool that prints
// "accepted" and leaves the chat alive — silently stranding the run.
// gemini.tsx must reject this combination at runtime (parse-time
// llm.tsx must reject this combination at runtime (parse-time
// gating can't catch the no-prompt-on-TTY case because stdin
// availability isn't probed yet at parse time).
const { loadCliConfig, parseArguments } = await import(
@@ -354,7 +354,7 @@ export async function main() {
profileCheckpoint('main_entry');
const acpStartupProfilerEnabled = isAcpStartupProfilerEnabled();
// Bridge core-package startup events (Config.initialize, MCP discovery,
// GeminiClient.setTools) into the cli's startup profiler. Gated on
// LlmClient.setTools) into the cli's startup profiler. Gated on
// `isStartupProfilerEnabled()` so that when QWEN_CODE_PROFILE_STARTUP is
// unset (the common case) every core-side `recordStartupEvent()` call
// sees a null sink and short-circuits at the first comparison, instead
@@ -7,9 +7,9 @@
import { Buffer } from 'node:buffer';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
GeminiEventType,
LlmEventType,
type Config,
type ServerGeminiStreamEvent,
type ServerLlmStreamEvent,
type ToolCallRequestInfo,
type AgentResultDisplay,
} from '@qwen-code/qwen-code-core';
@@ -293,7 +293,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should build message with text blocks', () => {
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Hello world',
});
@@ -316,7 +316,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should set stop_reason to tool_use when message contains only tool_use blocks', () => {
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-1',
name: 'test_tool',
@@ -443,7 +443,7 @@ describe('BaseJsonOutputAdapter', () => {
adapter.startAssistantMessage();
const state = adapter['mainAgentMessageState'];
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'text',
});
@@ -483,7 +483,7 @@ describe('BaseJsonOutputAdapter', () => {
adapter.startAssistantMessage();
const state = adapter['mainAgentMessageState'];
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'test',
});
@@ -504,7 +504,7 @@ describe('BaseJsonOutputAdapter', () => {
adapter.startAssistantMessage();
const state = adapter['mainAgentMessageState'];
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'test',
});
@@ -522,7 +522,7 @@ describe('BaseJsonOutputAdapter', () => {
adapter.startAssistantMessage();
const state = adapter['mainAgentMessageState'];
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'test',
});
state.openBlocks.add(0);
@@ -768,7 +768,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should reset main agent message state', () => {
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'test',
});
@@ -787,7 +787,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should process Content events', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Hello',
});
@@ -801,7 +801,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should process Citation events', () => {
adapter.processEvent({
type: GeminiEventType.Citation,
type: LlmEventType.Citation,
value: 'Citation text',
});
@@ -813,9 +813,9 @@ describe('BaseJsonOutputAdapter', () => {
it('should ignore non-string Citation values', () => {
adapter.processEvent({
type: GeminiEventType.Citation,
type: LlmEventType.Citation,
value: 123,
} as unknown as ServerGeminiStreamEvent);
} as unknown as ServerLlmStreamEvent);
const state = adapter['mainAgentMessageState'];
expect(state.blocks).toHaveLength(0);
@@ -823,7 +823,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should process Thought events', () => {
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: 'Planning',
description: 'Thinking',
@@ -841,7 +841,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should process ToolCallRequest events', () => {
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-1',
name: 'test_tool',
@@ -863,7 +863,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should process Finished events with usage metadata', () => {
adapter.processEvent({
type: GeminiEventType.Finished,
type: LlmEventType.Finished,
value: {
reason: undefined,
usageMetadata: {
@@ -882,13 +882,13 @@ describe('BaseJsonOutputAdapter', () => {
it('should ignore events after finalization', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'First',
});
adapter.finalizeAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Second',
});
@@ -907,7 +907,7 @@ describe('BaseJsonOutputAdapter', () => {
it('should build and return assistant message', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Test response',
});
@@ -1273,7 +1273,7 @@ describe('BaseJsonOutputAdapter', () => {
beforeEach(() => {
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Response text',
});
const message = adapter.finalizeAssistantMessage();
@@ -10,14 +10,14 @@ import type {
ToolCallRequestInfo,
ToolCallResponseInfo,
SessionMetrics,
ServerGeminiStreamEvent,
ServerLlmStreamEvent,
AgentResultDisplay,
McpToolProgressData,
ShellProgressData,
} from '@qwen-code/qwen-code-core';
import {
formatVisionBridgeNoticeDisplay,
GeminiEventType,
LlmEventType,
isVisionBridgeNoticeDisplay,
ToolErrorType,
parseAndFormatApiError,
@@ -118,7 +118,7 @@ export interface MessageEmitter {
*/
export interface JsonOutputAdapterInterface extends MessageEmitter {
startAssistantMessage(): void;
processEvent(event: ServerGeminiStreamEvent): void;
processEvent(event: ServerLlmStreamEvent): void;
finalizeAssistantMessage(): CLIAssistantMessage;
emitResult(options: ResultOptions): void;
@@ -201,7 +201,7 @@ export abstract class BaseJsonOutputAdapter {
/**
* Creates a Usage object from metadata.
*
* @param metadata - Optional usage metadata from Gemini API
* @param metadata - Optional LLM usage metadata
* @returns Usage object
*/
protected createUsage(
@@ -605,27 +605,27 @@ export abstract class BaseJsonOutputAdapter {
}
/**
* Processes a stream event from the Gemini API.
* Processes an LLM stream event.
* This is a shared implementation used by both streaming and non-streaming adapters.
*
* @param event - Stream event from Gemini API
* @param event - LLM stream event
*/
processEvent(event: ServerGeminiStreamEvent): void {
processEvent(event: ServerLlmStreamEvent): void {
const state = this.mainAgentMessageState;
if (state.finalized) {
return;
}
switch (event.type) {
case GeminiEventType.Content:
case LlmEventType.Content:
this.appendText(state, event.value, null);
break;
case GeminiEventType.Citation:
case LlmEventType.Citation:
if (typeof event.value === 'string') {
this.appendText(state, `\n${event.value}`, null);
}
break;
case GeminiEventType.Thought:
case LlmEventType.Thought:
this.appendThinking(
state,
event.value.subject,
@@ -633,16 +633,16 @@ export abstract class BaseJsonOutputAdapter {
null,
);
break;
case GeminiEventType.ToolCallRequest:
case LlmEventType.ToolCallRequest:
this.appendToolUse(state, event.value, null);
break;
case GeminiEventType.Finished:
case LlmEventType.Finished:
if (event.value?.usageMetadata) {
state.usage = this.createUsage(event.value.usageMetadata);
}
this.finalizePendingBlocks(state, null);
break;
case GeminiEventType.Error: {
case LlmEventType.Error: {
// Format the error message using parseAndFormatApiError for consistency
// with interactive mode error display
const errorText = parseAndFormatApiError(
@@ -652,7 +652,7 @@ export abstract class BaseJsonOutputAdapter {
this.appendText(state, errorText, null);
break;
}
case GeminiEventType.ModelFallback:
case LlmEventType.ModelFallback:
// Surface model fallback transitions so non-interactive consumers
// (CI pipelines, SDK clients) can observe capacity-driven model
// switches without parsing assistant content.
@@ -6,11 +6,8 @@
import { Buffer } from 'node:buffer';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import type {
Config,
ServerGeminiStreamEvent,
} from '@qwen-code/qwen-code-core';
import { GeminiEventType, OutputFormat } from '@qwen-code/qwen-code-core';
import type { Config, ServerLlmStreamEvent } from '@qwen-code/qwen-code-core';
import { LlmEventType, OutputFormat } from '@qwen-code/qwen-code-core';
import type { Part } from '@google/genai';
import { JsonOutputAdapter } from './JsonOutputAdapter.js';
import {
@@ -59,14 +56,14 @@ describe('JsonOutputAdapter', () => {
});
it('should append text content from Content events', () => {
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.Content,
const event: ServerLlmStreamEvent = {
type: LlmEventType.Content,
value: 'Hello',
};
adapter.processEvent(event);
const event2: ServerGeminiStreamEvent = {
type: GeminiEventType.Content,
const event2: ServerLlmStreamEvent = {
type: LlmEventType.Content,
value: ' World',
};
adapter.processEvent(event2);
@@ -80,8 +77,8 @@ describe('JsonOutputAdapter', () => {
});
it('should append citation content from Citation events', () => {
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.Citation,
const event: ServerLlmStreamEvent = {
type: LlmEventType.Citation,
value: 'Citation text',
};
adapter.processEvent(event);
@@ -94,10 +91,10 @@ describe('JsonOutputAdapter', () => {
});
it('should ignore non-string citation values', () => {
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.Citation,
const event: ServerLlmStreamEvent = {
type: LlmEventType.Citation,
value: 123,
} as unknown as ServerGeminiStreamEvent;
} as unknown as ServerLlmStreamEvent;
adapter.processEvent(event);
const message = adapter.finalizeAssistantMessage();
@@ -105,8 +102,8 @@ describe('JsonOutputAdapter', () => {
});
it('should append thinking from Thought events', () => {
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.Thought,
const event: ServerLlmStreamEvent = {
type: LlmEventType.Thought,
value: {
subject: 'Planning',
description: 'Thinking about the task',
@@ -124,8 +121,8 @@ describe('JsonOutputAdapter', () => {
});
it('should handle thinking with only subject', () => {
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.Thought,
const event: ServerLlmStreamEvent = {
type: LlmEventType.Thought,
value: {
subject: 'Planning',
description: '',
@@ -141,8 +138,8 @@ describe('JsonOutputAdapter', () => {
});
it('should append tool use from ToolCallRequest events', () => {
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.ToolCallRequest,
const event: ServerLlmStreamEvent = {
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-call-1',
name: 'test_tool',
@@ -165,7 +162,7 @@ describe('JsonOutputAdapter', () => {
it('should set stop_reason to tool_use when message contains only tool_use blocks', () => {
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-call-1',
name: 'test_tool',
@@ -181,7 +178,7 @@ describe('JsonOutputAdapter', () => {
it('should set stop_reason to null when message contains text blocks', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Some text',
});
@@ -191,7 +188,7 @@ describe('JsonOutputAdapter', () => {
it('should set stop_reason to null when message contains thinking blocks', () => {
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: 'Planning',
description: 'Thinking about the task',
@@ -204,7 +201,7 @@ describe('JsonOutputAdapter', () => {
it('should set stop_reason to tool_use when message contains multiple tool_use blocks', () => {
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-call-1',
name: 'test_tool_1',
@@ -214,7 +211,7 @@ describe('JsonOutputAdapter', () => {
},
});
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-call-2',
name: 'test_tool_2',
@@ -239,8 +236,8 @@ describe('JsonOutputAdapter', () => {
cachedContentTokenCount: 10,
totalTokenCount: 160,
};
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.Finished,
const event: ServerLlmStreamEvent = {
type: LlmEventType.Finished,
value: {
reason: undefined,
usageMetadata,
@@ -260,12 +257,12 @@ describe('JsonOutputAdapter', () => {
it('should finalize pending blocks on Finished event', () => {
// Add some text first
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Some text',
});
const event: ServerGeminiStreamEvent = {
type: GeminiEventType.Finished,
const event: ServerLlmStreamEvent = {
type: LlmEventType.Finished,
value: { reason: undefined, usageMetadata: undefined },
};
adapter.processEvent(event);
@@ -280,7 +277,7 @@ describe('JsonOutputAdapter', () => {
adapter.finalizeAssistantMessage().message.content;
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Should be ignored',
});
@@ -296,7 +293,7 @@ describe('JsonOutputAdapter', () => {
it('should build and emit a complete assistant message', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Test response',
});
@@ -313,7 +310,7 @@ describe('JsonOutputAdapter', () => {
it('should return same message on subsequent calls', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Test',
});
@@ -325,11 +322,11 @@ describe('JsonOutputAdapter', () => {
it('should split different block types into separate assistant messages', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text',
});
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: { subject: 'Thinking', description: 'Thought' },
});
@@ -386,7 +383,7 @@ describe('JsonOutputAdapter', () => {
beforeEach(() => {
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Response text',
});
adapter.finalizeAssistantMessage();
@@ -843,7 +840,7 @@ describe('JsonOutputAdapter', () => {
},
);
adapter.startAssistantMessage();
adapter.processEvent({ type: GeminiEventType.Content, value: 'done' });
adapter.processEvent({ type: LlmEventType.Content, value: 'done' });
adapter.finalizeAssistantMessage();
const storedMessages = (
@@ -918,7 +915,7 @@ describe('JsonOutputAdapter', () => {
adapter.emitUserMessage([{ text: 'User input' }]);
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Assistant response',
});
adapter.finalizeAssistantMessage();
@@ -6,7 +6,7 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { Config } from '@qwen-code/qwen-code-core';
import { GeminiEventType } from '@qwen-code/qwen-code-core';
import { LlmEventType } from '@qwen-code/qwen-code-core';
import { StreamJsonOutputAdapter } from './StreamJsonOutputAdapter.js';
/**
@@ -56,7 +56,7 @@ describe('StreamJsonOutputAdapter — dual-output extensions', () => {
);
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'sidecar',
});
adapter.finalizeAssistantMessage();
@@ -9,9 +9,9 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import type {
Config,
GoalSnapshotV2,
ServerGeminiStreamEvent,
ServerLlmStreamEvent,
} from '@qwen-code/qwen-code-core';
import { GeminiEventType } from '@qwen-code/qwen-code-core';
import { LlmEventType } from '@qwen-code/qwen-code-core';
import type { Part } from '@google/genai';
import { StreamJsonOutputAdapter } from './StreamJsonOutputAdapter.js';
import {
@@ -70,14 +70,14 @@ describe('StreamJsonOutputAdapter', () => {
it('should reset state for new message', () => {
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'First',
});
adapter.finalizeAssistantMessage();
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Second',
});
@@ -96,7 +96,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should emit stream events for text deltas', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Hello',
});
@@ -126,7 +126,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should emit active goal stream events', () => {
adapter.processEvent({
type: GeminiEventType.ActiveGoal,
type: LlmEventType.ActiveGoal,
value: {
condition: 'finish the refactor',
iterations: 2,
@@ -138,7 +138,7 @@ describe('StreamJsonOutputAdapter', () => {
});
adapter.processEvent({
type: GeminiEventType.ActiveGoal,
type: LlmEventType.ActiveGoal,
value: null,
});
@@ -179,12 +179,12 @@ describe('StreamJsonOutputAdapter', () => {
it('emits v2 goal_state before the gated legacy projection', () => {
adapter.processEvent({
type: GeminiEventType.GoalState,
type: LlmEventType.GoalState,
value: goalSnapshot,
cause: 'edit',
});
adapter.processEvent({
type: GeminiEventType.ActiveGoal,
type: LlmEventType.ActiveGoal,
value: {
condition: 'finish the refactor',
iterations: 3,
@@ -221,11 +221,11 @@ describe('StreamJsonOutputAdapter', () => {
it('does not emit duplicate v2 goal snapshots from overlapping sources', () => {
adapter.processEvent({
type: GeminiEventType.GoalState,
type: LlmEventType.GoalState,
value: goalSnapshot,
});
adapter.processEvent({
type: GeminiEventType.GoalState,
type: LlmEventType.GoalState,
value: structuredClone(goalSnapshot),
});
@@ -240,7 +240,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should emit message_start event on first content', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'First',
});
@@ -262,7 +262,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should emit content_block_start for new blocks', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text',
});
@@ -284,7 +284,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should emit thinking delta events', () => {
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: 'Planning',
description: 'Thinking',
@@ -310,7 +310,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should emit message_stop on finalization', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text',
});
adapter.finalizeAssistantMessage();
@@ -341,7 +341,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should not emit stream events', () => {
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text',
});
@@ -360,7 +360,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should not emit active goal stream events', () => {
adapter.processEvent({
type: GeminiEventType.ActiveGoal,
type: LlmEventType.ActiveGoal,
value: {
condition: 'finish the refactor',
iterations: 0,
@@ -389,12 +389,12 @@ describe('StreamJsonOutputAdapter', () => {
it('still emits v2 goal_state without the partial-message gate', () => {
adapter.processEvent({
type: GeminiEventType.GoalState,
type: LlmEventType.GoalState,
value: goalSnapshot,
cause: 'edit',
});
adapter.processEvent({
type: GeminiEventType.ActiveGoal,
type: LlmEventType.ActiveGoal,
value: {
condition: 'finish the refactor',
iterations: 3,
@@ -420,7 +420,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should still emit final assistant message', () => {
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text',
});
adapter.finalizeAssistantMessage();
@@ -447,11 +447,11 @@ describe('StreamJsonOutputAdapter', () => {
it('should append text content from Content events', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Hello',
});
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: ' World',
});
@@ -465,7 +465,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should append citation content from Citation events', () => {
adapter.processEvent({
type: GeminiEventType.Citation,
type: LlmEventType.Citation,
value: 'Citation text',
});
@@ -478,9 +478,9 @@ describe('StreamJsonOutputAdapter', () => {
it('should ignore non-string citation values', () => {
adapter.processEvent({
type: GeminiEventType.Citation,
type: LlmEventType.Citation,
value: 123,
} as unknown as ServerGeminiStreamEvent);
} as unknown as ServerLlmStreamEvent);
const message = adapter.finalizeAssistantMessage();
expect(message.message.content).toHaveLength(0);
@@ -488,7 +488,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should append thinking from Thought events', () => {
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: 'Planning',
description: 'Thinking about the task',
@@ -506,7 +506,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should handle thinking with only subject', () => {
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: 'Planning',
description: '',
@@ -522,7 +522,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should preserve whitespace in thinking content (issue #1356)', () => {
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: '',
description: 'The user just said "Hello"',
@@ -545,21 +545,21 @@ describe('StreamJsonOutputAdapter', () => {
it('should preserve whitespace when streaming multiple thinking fragments (issue #1356)', () => {
// Simulate streaming thinking content in multiple events
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: '',
description: 'The user just',
},
});
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: '',
description: ' said "Hello"',
},
});
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: '',
description: '. This is a simple greeting',
@@ -584,7 +584,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should append tool use from ToolCallRequest events', () => {
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-call-1',
name: 'test_tool',
@@ -606,7 +606,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should set stop_reason to tool_use when message contains only tool_use blocks', () => {
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-call-1',
name: 'test_tool',
@@ -622,7 +622,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should set stop_reason to null when message contains text blocks', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Some text',
});
@@ -632,7 +632,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should set stop_reason to null when message contains thinking blocks', () => {
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: {
subject: 'Planning',
description: 'Thinking about the task',
@@ -645,7 +645,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should set stop_reason to tool_use when message contains multiple tool_use blocks', () => {
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-call-1',
name: 'test_tool_1',
@@ -655,7 +655,7 @@ describe('StreamJsonOutputAdapter', () => {
},
});
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,
type: LlmEventType.ToolCallRequest,
value: {
callId: 'tool-call-2',
name: 'test_tool_2',
@@ -681,7 +681,7 @@ describe('StreamJsonOutputAdapter', () => {
totalTokenCount: 160,
};
adapter.processEvent({
type: GeminiEventType.Finished,
type: LlmEventType.Finished,
value: {
reason: undefined,
usageMetadata,
@@ -703,7 +703,7 @@ describe('StreamJsonOutputAdapter', () => {
adapter.finalizeAssistantMessage().message.content;
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Should be ignored',
});
@@ -720,7 +720,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should build and emit a complete assistant message', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Test response',
});
@@ -737,7 +737,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should emit message to stdout immediately', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Test',
});
@@ -752,7 +752,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should store message in lastAssistantMessage', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Test',
});
@@ -764,7 +764,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should return same message on subsequent calls', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Test',
});
@@ -777,11 +777,11 @@ describe('StreamJsonOutputAdapter', () => {
it('should split different block types into separate assistant messages', () => {
stdoutWriteSpy.mockClear();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text',
});
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: { subject: 'Thinking', description: 'Thought' },
});
@@ -876,7 +876,7 @@ describe('StreamJsonOutputAdapter', () => {
adapter = new StreamJsonOutputAdapter(mockConfig, false);
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Response text',
});
adapter.finalizeAssistantMessage();
@@ -1297,11 +1297,11 @@ describe('StreamJsonOutputAdapter', () => {
it('should not include message_id in content_block events', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text',
});
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'More',
});
@@ -1329,7 +1329,7 @@ describe('StreamJsonOutputAdapter', () => {
it('should identify content_block events by session_id and index', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text',
});
@@ -1362,15 +1362,15 @@ describe('StreamJsonOutputAdapter', () => {
it('should split assistant messages when block types change repeatedly', () => {
stdoutWriteSpy.mockClear();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Text content',
});
adapter.processEvent({
type: GeminiEventType.Thought,
type: LlmEventType.Thought,
value: { subject: 'Thinking', description: 'Thought' },
});
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'More text',
});
@@ -1439,15 +1439,15 @@ describe('StreamJsonOutputAdapter', () => {
it('should merge consecutive text fragments', () => {
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'Hello',
});
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: ' ',
});
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: 'World',
});
@@ -7,12 +7,12 @@
import { randomUUID } from 'node:crypto';
import type {
Config,
ServerGeminiStreamEvent,
ServerLlmStreamEvent,
ToolCallRequestInfo,
McpToolProgressData,
ShellProgressData,
} from '@qwen-code/qwen-code-core';
import { GeminiEventType } from '@qwen-code/qwen-code-core';
import { LlmEventType } from '@qwen-code/qwen-code-core';
import type {
CLIAssistantMessage,
CLIMessage,
@@ -131,8 +131,8 @@ export class StreamJsonOutputAdapter
this.emitMessage(message);
}
override processEvent(event: ServerGeminiStreamEvent): void {
if (event.type === GeminiEventType.GoalState) {
override processEvent(event: ServerLlmStreamEvent): void {
if (event.type === LlmEventType.GoalState) {
const signature = JSON.stringify(event.value);
if (signature === this.lastGoalStateSignature) return;
this.lastGoalStateSignature = signature;
@@ -153,7 +153,7 @@ export class StreamJsonOutputAdapter
// Active goal updates are session-level metadata, not message content.
// They intentionally bypass the base finalized guard so late goal state
// changes can still reach stream consumers.
if (event.type === GeminiEventType.ActiveGoal) {
if (event.type === LlmEventType.ActiveGoal) {
this.emitStreamEventIfEnabled(
{
type: 'active_goal',
+14 -14
View File
@@ -317,16 +317,16 @@ describe('runNonInteractiveStreamJson', () => {
return { continueResults, getControlContext: () => controlContext };
}
function createInitializedGeminiClient(historyTail: Content[]) {
function createInitializedLlmClient(historyTail: Content[]) {
const getHistoryTail = vi.fn().mockReturnValue(historyTail);
const geminiClient = {
const llmClient = {
isInitialized: vi.fn().mockReturnValue(true),
getChat: vi.fn().mockReturnValue({ getHistoryTail }),
};
config = createConfig({
getGeminiClient: vi.fn().mockReturnValue(geminiClient),
getLlmClient: vi.fn().mockReturnValue(llmClient),
});
return { geminiClient, getHistoryTail };
return { llmClient, getHistoryTail };
}
it('initializes session and processes initialize control request', async () => {
@@ -406,7 +406,7 @@ describe('runNonInteractiveStreamJson', () => {
it('rejects continue_last_turn when the Gemini client is not initialized', async () => {
const { continueResults } = installContinueDispatch();
config = createConfig({
getGeminiClient: vi.fn().mockReturnValue(undefined),
getLlmClient: vi.fn().mockReturnValue(undefined),
});
const initRequest = createControlRequest('initialize');
const continueRequest = createContinueRequest();
@@ -426,7 +426,7 @@ describe('runNonInteractiveStreamJson', () => {
it('rejects continue_last_turn when the last turn ended cleanly', async () => {
const { continueResults } = installContinueDispatch();
const { getHistoryTail } = createInitializedGeminiClient([
const { getHistoryTail } = createInitializedLlmClient([
{ role: 'model', parts: [{ text: 'done' }] },
]);
const initRequest = createControlRequest('initialize');
@@ -448,7 +448,7 @@ describe('runNonInteractiveStreamJson', () => {
it('deduplicates continue_last_turn while a continuation is pending or running', async () => {
const { continueResults } = installContinueDispatch();
createInitializedGeminiClient([
createInitializedLlmClient([
{ role: 'user', parts: [{ text: 'resume me' }] },
]);
const initRequest = createControlRequest('initialize');
@@ -497,7 +497,7 @@ describe('runNonInteractiveStreamJson', () => {
it('keeps continue_last_turn available after an interrupt with no active turn', async () => {
const { continueResults, getControlContext } = installContinueDispatch();
createInitializedGeminiClient([
createInitializedLlmClient([
{ role: 'user', parts: [{ text: 'resume me' }] },
]);
const initRequest = createControlRequest('initialize');
@@ -577,7 +577,7 @@ describe('runNonInteractiveStreamJson', () => {
it('emits a terminal error result when an accepted continuation is abandoned by shutdown', async () => {
const { continueResults, getControlContext } = installContinueDispatch();
createInitializedGeminiClient([
createInitializedLlmClient([
{ role: 'user', parts: [{ text: 'resume me' }] },
]);
const initRequest = createControlRequest('initialize');
@@ -628,7 +628,7 @@ describe('runNonInteractiveStreamJson', () => {
it('emits an error result when a scheduled continue turn fails', async () => {
const { continueResults } = installContinueDispatch();
createInitializedGeminiClient([
createInitializedLlmClient([
{ role: 'user', parts: [{ text: 'resume me' }] },
]);
const initRequest = createControlRequest('initialize');
@@ -658,7 +658,7 @@ describe('runNonInteractiveStreamJson', () => {
it('flushes recording failures before a session-level error result', async () => {
const { continueResults } = installContinueDispatch();
createInitializedGeminiClient([
createInitializedLlmClient([
{ role: 'user', parts: [{ text: 'resume me' }] },
]);
const order: string[] = [];
@@ -667,7 +667,7 @@ describe('runNonInteractiveStreamJson', () => {
| undefined;
let flushCount = 0;
config = createConfig({
getGeminiClient: vi.fn().mockReturnValue(config.getGeminiClient()),
getLlmClient: vi.fn().mockReturnValue(config.getLlmClient()),
onChatRecordingFailure: (
listener: (event: { sessionId: string; error: Error }) => void,
) => {
@@ -717,7 +717,7 @@ describe('runNonInteractiveStreamJson', () => {
it('does not emit a second result when a failed continue turn already reported one', async () => {
const { continueResults } = installContinueDispatch();
createInitializedGeminiClient([
createInitializedLlmClient([
{ role: 'user', parts: [{ text: 'resume me' }] },
]);
const initRequest = createControlRequest('initialize');
@@ -762,7 +762,7 @@ describe('runNonInteractiveStreamJson', () => {
it('emits a continue_turn_failed diagnostic when a continue turn fails after a result', async () => {
const { continueResults } = installContinueDispatch();
createInitializedGeminiClient([
createInitializedLlmClient([
{ role: 'user', parts: [{ text: 'resume me' }] },
]);
const initRequest = createControlRequest('initialize');
+7 -7
View File
@@ -159,12 +159,12 @@ class Session {
debugLogger.debug('[Session] Initializing config');
try {
// gemini.tsx has already emitted warnings known before stream-json
// llm.tsx has already emitted warnings known before stream-json
// initialization starts. Keep that snapshot so only warnings produced
// by the deferred initialize() call are written here.
const emittedWarnings = new Set(this.config.getWarnings());
// Bracket `config.initialize()` with the same profiler checkpoints
// the non-stream-json branch in `gemini.tsx` uses so the
// the non-stream-json branch in `llm.tsx` uses so the
// `config_initialize_dur` derived phase shows up in stream-json
// startup profiles. `profileCheckpoint` is a no-op when
// `QWEN_CODE_PROFILE_STARTUP` is unset, so this adds zero overhead
@@ -184,7 +184,7 @@ class Session {
// MCP servers settle, so we must explicitly await discovery here —
// otherwise the first prompt would see only built-in tools.
await this.config.waitForMcpReady();
// Surface MCP failures on stderr — same rationale as gemini.tsx's
// Surface MCP failures on stderr — same rationale as llm.tsx's
// non-interactive branch: per-server errors are caught inside
// `discoverAllMcpToolsIncremental` and never reach a TTY otherwise,
// so a script using stream-json with broken MCP config would
@@ -203,7 +203,7 @@ class Session {
}
// Finalize the startup profile here so `config_initialize_*` and the
// MCP discovery events captured during init/discovery make it into
// the on-disk profile. gemini.tsx's stream-json branch deliberately
// the on-disk profile. llm.tsx's stream-json branch deliberately
// skips finalize because the profiler's `finalized` guard would
// otherwise suppress every event emitted during the
// `Session.ensureConfigInitialized` flow above.
@@ -512,15 +512,15 @@ class Session {
return { accepted: false, interruption: 'none' };
}
const geminiClient = this.config.getGeminiClient();
if (!geminiClient || !geminiClient.isInitialized()) {
const llmClient = this.config.getLlmClient();
if (!llmClient || !llmClient.isInitialized()) {
debugLogger.debug(
'[Session] continue_last_turn rejected: gemini client is not ready',
);
return { accepted: false, interruption: 'none' };
}
const chat = geminiClient.getChat();
const chat = llmClient.getChat();
const historyTail =
chat.getHistoryTailShallow?.(TURN_INTERRUPTION_HISTORY_TAIL_COUNT) ??
chat.getHistoryTail(TURN_INTERRUPTION_HISTORY_TAIL_COUNT);
File diff suppressed because it is too large. Load diff
+22 -22
View File
@@ -26,7 +26,7 @@ import {
executeToolCall,
shutdownTelemetry,
isTelemetrySdkInitialized,
GeminiEventType,
LlmEventType,
FatalInputError,
promptIdContext,
OutputFormat,
@@ -389,7 +389,7 @@ async function emitNonInteractiveFinalMessage(params: {
// (systemMessage should already be emitted by caller)
adapter.startAssistantMessage();
adapter.processEvent({
type: GeminiEventType.Content,
type: LlmEventType.Content,
value: message,
} as unknown as Parameters<JsonOutputAdapterInterface['processEvent']>[0]);
adapter.finalizeAssistantMessage();
@@ -585,18 +585,18 @@ export async function runNonInteractive(
});
};
const geminiClient = config.getGeminiClient();
const llmClient = config.getLlmClient();
const abortController = options.abortController ?? new AbortController();
const queuedGoalTurns: HeadlessGoalTurn[] = [];
let activeGoalTurn: HeadlessGoalTurn | undefined;
let goalRuntimeUnsubscribe: (() => void) | undefined;
const emitGoalSnapshot = (snapshot: GoalSnapshotV2) => {
adapter.processEvent({
type: GeminiEventType.GoalState,
type: LlmEventType.GoalState,
value: snapshot,
});
adapter.processEvent({
type: GeminiEventType.ActiveGoal,
type: LlmEventType.ActiveGoal,
value: projectLegacyActiveGoal(snapshot),
});
};
@@ -1086,7 +1086,7 @@ export async function runNonInteractive(
// runs once per (rare) continue request, so the full clone is fine.
const recoveryPlan = buildSessionRecoveryPlanFromApiHistory({
sessionId,
apiHistory: geminiClient.getChat().getHistory(),
apiHistory: llmClient.getChat().getHistory(),
});
debugLogger.info('[runNonInteractive] continueInterrupted recovery', {
kind: recoveryPlan.kind,
@@ -1283,7 +1283,7 @@ export async function runNonInteractive(
}
// Inject a worktree context notice into the model's first prompt.
// Two sources: the `--worktree` startup flag (set by gemini.tsx
// Two sources: the `--worktree` startup flag (set by llm.tsx
// before loadCliConfig) takes precedence over the Phase C resume
// restore. TUI does this via historyManager.addItem(INFO); here in
// headless we prepend a `<system-reminder>` block since there is
@@ -1546,7 +1546,7 @@ export async function runNonInteractive(
// An explicit inline `/model <id> <prompt>` override wins for the whole
// turn: while active, skill-tool `modelOverride` writes (including the
// undefined-clears case) are skipped so they cannot silently revert the
// submitted prompt to the session model mid-turn. Unlike useGeminiStream's
// submitted prompt to the session model mid-turn. Unlike useLlmStream's
// ref-based `applyModelOverride`/`clearModelOverride` helpers, this is a
// run-scoped const — non-interactive mode is single-turn, so there is no
// retry-clearing or skill-tool takeover to guard against, just the
@@ -1701,7 +1701,7 @@ export async function runNonInteractive(
// Fresh map per call today; copy so a future cached accessor cannot
// turn this run's cross-turn recording into shared-state mutation.
const handledToolCallFingerprints = new Map(
geminiClient.getHistoryToolCallFingerprints(),
llmClient.getHistoryToolCallFingerprints(),
);
// Tracks duplicate-error responses emitted during this headless run.
// Once a provider id reaches this set, seeing it again is terminal for
@@ -2002,7 +2002,7 @@ export async function runNonInteractive(
responseByRequest.set(requestInfo, toolResponse);
terminateTurn ||= toolResponse.terminateTurn === true;
config
.getGeminiClient()
.getLlmClient()
.recordCompletedToolCall(
requestInfo.name,
requestInfo.args as Record<string, unknown>,
@@ -2352,7 +2352,7 @@ export async function runNonInteractive(
const toolCallRequests: ToolCallRequestInfo[] = [];
const apiStartTime = Date.now();
const responseStream = geminiClient.sendMessageStream(
const responseStream = llmClient.sendMessageStream(
currentMessages[0]?.parts || [],
abortController.signal,
currentPromptId,
@@ -2395,21 +2395,21 @@ export async function runNonInteractive(
}
// Use adapter for all event processing
adapter.processEvent(event);
if (event.type === GeminiEventType.ToolCallRequest) {
if (event.type === LlmEventType.ToolCallRequest) {
toolCallRequests.push(event.value);
}
if (event.type === GeminiEventType.ModelFallback) {
if (event.type === LlmEventType.ModelFallback) {
toolCallRequests.length = 0;
}
if (
event.type === GeminiEventType.Content &&
event.type === LlmEventType.Content &&
plainTextPreview.length < PLAIN_TEXT_PREVIEW_LIMIT
) {
const remaining =
PLAIN_TEXT_PREVIEW_LIMIT - plainTextPreview.length;
plainTextPreview += String(event.value).slice(0, remaining);
}
if (event.type === GeminiEventType.LoopDetected) {
if (event.type === LlmEventType.LoopDetected) {
if (!loopDetected) {
loopDetectedMessage = emitLoopDetectedMessage(
config,
@@ -2420,7 +2420,7 @@ export async function runNonInteractive(
}
if (
outputFormat === OutputFormat.TEXT &&
event.type === GeminiEventType.Error
event.type === LlmEventType.Error
) {
const errorText = parseAndFormatApiError(
event.value.error,
@@ -2496,7 +2496,7 @@ export async function runNonInteractive(
return emitLoopDetectedResult();
}
if (terminateTurn && activeGoalTurn) {
geminiClient.addHistory({
llmClient.addHistory({
role: 'user',
parts: toolResponseParts,
});
@@ -2680,7 +2680,7 @@ export async function runNonInteractive(
const itemToolCallRequests: ToolCallRequestInfo[] = [];
const itemApiStartTime = Date.now();
selectActiveInteraction(itemPromptId, itemIsFirstTurn);
const itemStream = geminiClient.sendMessageStream(
const itemStream = llmClient.sendMessageStream(
itemMessages[0]?.parts || [],
abortController.signal,
itemPromptId,
@@ -2722,10 +2722,10 @@ export async function runNonInteractive(
await routeAbort();
}
adapter.processEvent(event);
if (event.type === GeminiEventType.ToolCallRequest) {
if (event.type === LlmEventType.ToolCallRequest) {
itemToolCallRequests.push(event.value);
}
if (event.type === GeminiEventType.LoopDetected) {
if (event.type === LlmEventType.LoopDetected) {
if (!loopDetected) {
loopDetectedMessage = emitLoopDetectedMessage(
config,
@@ -2736,7 +2736,7 @@ export async function runNonInteractive(
}
if (
outputFormat === OutputFormat.TEXT &&
event.type === GeminiEventType.Error
event.type === LlmEventType.Error
) {
const errorText = parseAndFormatApiError(
event.value.error,
@@ -2976,7 +2976,7 @@ export async function runNonInteractive(
}
const memoryTaskPromises = config
.getGeminiClient()
.getLlmClient()
.consumePendingMemoryTaskPromises();
if (memoryTaskPromises.length > 0) {
await Promise.allSettled(memoryTaskPromises);
@@ -74,7 +74,7 @@ export class RemoteInputWatcher {
/**
* Register the TUI's submit function. Called from AppContainer
* once useGeminiStream's submitQuery is available.
* once useLlmStream's submitQuery is available.
*/
setSubmitFn(fn: SubmitFn): void {
this.submitFn = fn;
+2 -2
View File
@@ -396,8 +396,8 @@ describe('CLI entry import boundary', () => {
it('does not statically import the full gemini entry before the serve fast path can run', () => {
const cliSource = readFileSync('src/cli.ts', 'utf8');
expect(cliSource).not.toContain("import './gemini.js'");
expect(cliSource).not.toContain("import { main } from './gemini.js'");
expect(cliSource).not.toContain("import './llm.js'");
expect(cliSource).not.toContain("import { main } from './llm.js'");
expect(cliSource).not.toContain("process.argv[2] === 'serve'");
expect(cliSource).toContain("await import('./serve/fast-path.js')");
});
+1 -1
View File
@@ -441,7 +441,7 @@ export async function persistStartupWorktreeSidecar(
* the first user prompt (TUI: INFO history item + reminder prefix; headless:
* `<system-reminder>` prefix + JSON event; ACP currently exits before
* reaching this code path — see the `--worktree` × `--acp` mutex check
* in `gemini.tsx`).
* in `llm.tsx`).
*
* Mirrors `restoreWorktreeContext`'s contextMessage shape so resumed-with-
* worktree and started-with-worktree sessions read identically to the model.
@@ -7,7 +7,7 @@
import { vi } from 'vitest';
/**
* Stateful fake of the real GeminiClient's single swap-slot contract (see
* Stateful fake of the real LlmClient's single swap-slot contract (see
* beginTelemetrySwap's JSDoc in core client.ts): one open transaction at a
* time; commit/abort release the slot. Shared by the /resume and /branch
* hook tests so the contract has exactly one home — when it evolves,
+64 -64
View File
@@ -68,7 +68,7 @@ import {
type Config,
makeFakeConfig,
SendMessageType,
type GeminiClient,
type LlmClient,
type GoalTurnHost,
type HeldMessage,
type SubagentManager,
@@ -163,7 +163,7 @@ vi.mock('./hooks/slashCommandProcessor.js');
vi.mock('./hooks/useTerminalSize.js', () => ({
useTerminalSize: vi.fn(() => ({ columns: 80, rows: 24 })),
}));
vi.mock('./hooks/useGeminiStream.js');
vi.mock('./hooks/use-llm-stream.js');
vi.mock('./hooks/vim.js');
vi.mock('./hooks/useFocus.js');
vi.mock('./hooks/useBracketedPaste.js');
@@ -223,7 +223,7 @@ import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useLlmStream } from './hooks/use-llm-stream.js';
import { useVim } from './hooks/vim.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
@@ -259,7 +259,7 @@ describe('AppContainer State Management', () => {
const mockedUseSettingsCommand = useSettingsCommand as Mock;
const mockedUseModelCommand = useModelCommand as Mock;
const mockedUseSlashCommandProcessor = useSlashCommandProcessor as Mock;
const mockedUseGeminiStream = useGeminiStream as Mock;
const mockedUseLlmStream = useLlmStream as Mock;
const mockedUseVim = useVim as Mock;
const mockedUseFolderTrust = useFolderTrust as Mock;
const mockedUseIdeTrustListener = useIdeTrustListener as Mock;
@@ -392,7 +392,7 @@ describe('AppContainer State Management', () => {
shellConfirmationRequest: null,
confirmationRequest: null,
});
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -466,14 +466,14 @@ describe('AppContainer State Management', () => {
// Mock config's getTargetDir to return consistent workspace directory
vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
// Mock GeminiClient to prevent unhandled errors from AgentTool.refreshSubagents
const mockGeminiClient: Partial<GeminiClient> = {
// Mock LlmClient to prevent unhandled errors from AgentTool.refreshSubagents
const mockLlmClient: Partial<LlmClient> = {
initialize: vi.fn().mockResolvedValue(undefined),
setTools: vi.fn().mockResolvedValue(undefined),
isInitialized: vi.fn().mockReturnValue(false), // Return false to prevent setTools from being called
};
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue(
mockGeminiClient as GeminiClient,
vi.spyOn(mockConfig, 'getLlmClient').mockReturnValue(
mockLlmClient as LlmClient,
);
// Mock SubagentManager to prevent errors during AgentTool initialization
@@ -591,7 +591,7 @@ describe('AppContainer State Management', () => {
filesFailed: string[];
};
fileRewindError?: Error;
noGeminiClient?: boolean;
noLlmClient?: boolean;
history?: HistoryItem[];
contextFilePaths?: string[];
};
@@ -641,15 +641,15 @@ describe('AppContainer State Management', () => {
];
const getHistoryShallow = vi.fn(() => apiHistory);
const truncateHistory = vi.fn();
const geminiClient = {
const llmClient = {
initialize: vi.fn().mockResolvedValue(undefined),
setTools: vi.fn().mockResolvedValue(undefined),
isInitialized: vi.fn().mockReturnValue(false),
getHistoryShallow,
truncateHistory,
} as unknown as GeminiClient;
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue(
options.noGeminiClient ? (null as unknown as GeminiClient) : geminiClient,
} as unknown as LlmClient;
vi.spyOn(mockConfig, 'getLlmClient').mockReturnValue(
options.noLlmClient ? (null as unknown as LlmClient) : llmClient,
);
const rewind = vi.fn();
@@ -765,7 +765,7 @@ describe('AppContainer State Management', () => {
throw new Error('cancel failed');
});
const requestShutdown = vi.fn();
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: StreamingState.Responding,
submitQuery: vi.fn(),
initError: null,
@@ -776,12 +776,12 @@ describe('AppContainer State Management', () => {
streamingResponseLengthRef: { current: 0 },
isReceivingContent: false,
});
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue({
vi.spyOn(mockConfig, 'getLlmClient').mockReturnValue({
initialize: vi.fn().mockResolvedValue(undefined),
setTools: vi.fn().mockResolvedValue(undefined),
isInitialized: vi.fn().mockReturnValue(false),
requestShutdown,
} as unknown as GeminiClient);
} as unknown as LlmClient);
render(
<AppContainer
@@ -933,7 +933,7 @@ describe('AppContainer State Management', () => {
shellConfirmationRequest: null,
confirmationRequest: null,
});
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery,
initError: null,
@@ -1551,7 +1551,7 @@ describe('AppContainer State Management', () => {
restoreMessages: vi.fn(),
drainQueue: vi.fn().mockReturnValue([]),
});
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery,
initError: null,
@@ -1756,7 +1756,7 @@ describe('AppContainer State Management', () => {
| undefined;
metadata?.onAdmissionFailed?.();
throw new Error('persistent prepare failure');
}) as unknown as ReturnType<typeof useGeminiStream>['submitQuery'];
}) as unknown as ReturnType<typeof useLlmStream>['submitQuery'];
const { rerender } = renderHook(
({ pendingSubmissionCount, submissionSettledRevision }) =>
useQueuedSubmissionDrain({
@@ -1911,7 +1911,7 @@ describe('AppContainer State Management', () => {
| { onAdmissionFailed?: () => void }
| undefined;
metadata?.onAdmissionFailed?.();
}) as unknown as ReturnType<typeof useGeminiStream>['submitQuery'];
}) as unknown as ReturnType<typeof useLlmStream>['submitQuery'];
renderHook(() =>
useQueuedSubmissionDrain({
@@ -1962,7 +1962,7 @@ describe('AppContainer State Management', () => {
| { onDeliveryFailed?: () => void }
| undefined;
metadata?.onDeliveryFailed?.();
}) as unknown as ReturnType<typeof useGeminiStream>['submitQuery'];
}) as unknown as ReturnType<typeof useLlmStream>['submitQuery'];
const { rerender } = renderHook(
({ pendingSubmissionCount, submissionSettledRevision }) =>
@@ -2039,7 +2039,7 @@ describe('AppContainer State Management', () => {
| { onAdmissionFailed?: () => void }
| undefined;
metadata?.onAdmissionFailed?.();
}) as unknown as ReturnType<typeof useGeminiStream>['submitQuery'];
}) as unknown as ReturnType<typeof useLlmStream>['submitQuery'];
const { rerender } = renderHook(
({ pendingSubmissionCount, submissionSettledRevision }) =>
@@ -2143,7 +2143,7 @@ describe('AppContainer State Management', () => {
const mockQueueMessage = vi.fn();
const mockSubmitQuery = vi.fn();
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: mockSubmitQuery,
initError: null,
@@ -2191,7 +2191,7 @@ describe('AppContainer State Management', () => {
const mockSubmitQuery = vi.fn();
const mockQueueMessage = vi.fn();
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: mockSubmitQuery,
initError: null,
@@ -2242,7 +2242,7 @@ describe('AppContainer State Management', () => {
const mockSubmitQuery = vi.fn();
const mockQueueMessage = vi.fn();
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: mockSubmitQuery,
initError: null,
@@ -2352,7 +2352,7 @@ describe('AppContainer State Management', () => {
const mockSubmitQuery = vi.fn();
const mockQueueMessage = vi.fn();
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: mockSubmitQuery,
initError: null,
@@ -2401,7 +2401,7 @@ describe('AppContainer State Management', () => {
vi.spyOn(mockConfig, 'consumePendingRecoveredAgentsNotice')
.mockReturnValueOnce('Use list_agents to inspect restored agents.')
.mockReturnValue(null);
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -3020,7 +3020,7 @@ describe('AppContainer State Management', () => {
shellConfirmationRequest: null,
confirmationRequest: null,
});
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: StreamingState.Responding,
submitQuery: vi.fn(),
initError: null,
@@ -3060,8 +3060,8 @@ describe('AppContainer State Management', () => {
});
describe('Cancel Handler (issue #3204)', () => {
// The cancel handler is wired through useGeminiStream's onCancelSubmit
// arg (positional index 15 — see the useGeminiStream call site in
// The cancel handler is wired through useLlmStream's onCancelSubmit
// arg (positional index 15 — see the useLlmStream call site in
// AppContainer.tsx). We capture it via mockImplementation so a future
// signature change surfaces as a clear test failure rather than silently
// grabbing the wrong callback.
@@ -3110,7 +3110,7 @@ describe('AppContainer State Management', () => {
streamReturnValue: Record<string, unknown>,
) => {
capturedOnCancelSubmit = null;
mockedUseGeminiStream.mockImplementation((...args: unknown[]) => {
mockedUseLlmStream.mockImplementation((...args: unknown[]) => {
const candidate = args[ON_CANCEL_SUBMIT_ARG_INDEX];
if (typeof candidate === 'function') {
capturedOnCancelSubmit = candidate as CapturedCancelSubmit;
@@ -3126,7 +3126,7 @@ describe('AppContainer State Management', () => {
const triggerCancel = (info?: Parameters<CapturedCancelSubmit>[0]) => {
if (!capturedOnCancelSubmit) {
throw new Error(
`onCancelSubmit was not captured at arg index ${ON_CANCEL_SUBMIT_ARG_INDEX} — useGeminiStream signature may have changed`,
`onCancelSubmit was not captured at arg index ${ON_CANCEL_SUBMIT_ARG_INDEX} — useLlmStream signature may have changed`,
);
}
capturedOnCancelSubmit(info);
@@ -3442,15 +3442,15 @@ describe('AppContainer State Management', () => {
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
removeLastUserMessage: mockRemoveLastUserMessage,
});
// Extend the default GeminiClient mock with the orphan-strip
// Extend the default LlmClient mock with the orphan-strip
// entry-point so the auto-restore branch's third cleanup leg can
// be observed.
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue({
vi.spyOn(mockConfig, 'getLlmClient').mockReturnValue({
initialize: vi.fn().mockResolvedValue(undefined),
setTools: vi.fn().mockResolvedValue(undefined),
isInitialized: vi.fn().mockReturnValue(false),
stripOrphanedUserEntriesFromHistory: mockStripOrphans,
} as unknown as GeminiClient);
} as unknown as LlmClient);
installCancelCapture({
streamingState: 'responding',
submitQuery: vi.fn(),
@@ -3527,12 +3527,12 @@ describe('AppContainer State Management', () => {
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
removeLastUserMessage: mockRemoveLastUserMessage,
});
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue({
vi.spyOn(mockConfig, 'getLlmClient').mockReturnValue({
initialize: vi.fn().mockResolvedValue(undefined),
setTools: vi.fn().mockResolvedValue(undefined),
isInitialized: vi.fn().mockReturnValue(false),
stripOrphanedUserEntriesFromHistory: mockStripOrphans,
} as unknown as GeminiClient);
} as unknown as LlmClient);
installCancelCapture({
streamingState: 'responding',
submitQuery: vi.fn(),
@@ -3595,12 +3595,12 @@ describe('AppContainer State Management', () => {
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
removeLastUserMessage: vi.fn().mockResolvedValue(true),
});
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue({
vi.spyOn(mockConfig, 'getLlmClient').mockReturnValue({
initialize: vi.fn().mockResolvedValue(undefined),
setTools: vi.fn().mockResolvedValue(undefined),
isInitialized: vi.fn().mockReturnValue(false),
stripOrphanedUserEntriesFromHistory: mockStripOrphans,
} as unknown as GeminiClient);
} as unknown as LlmClient);
installCancelCapture({
streamingState: 'responding',
submitQuery: vi.fn(),
@@ -3721,7 +3721,7 @@ describe('AppContainer State Management', () => {
it('does not auto-restore when the cancelled turn did not add a user item (e.g. Cron / slash submit_prompt)', async () => {
// Some submit paths (SendMessageType.Cron, slash submit_prompt) run
// through useGeminiStream without pushing a `user` history item.
// through useLlmStream without pushing a `user` history item.
// If history happens to end with an older user prompt followed only
// by synthetic items (e.g. info), the auto-restore guard must NOT
// wrongly truncate/restore that older prompt on behalf of the
@@ -3925,7 +3925,7 @@ describe('AppContainer State Management', () => {
it('does not auto-restore when the sync pendingItem snapshot has meaningful content (closes stale-state race)', async () => {
// Race scenario from PR review: stream chunk arrives → cancelOngoingRequest
// commits via addItem → fires onCancelSubmit before React re-renders, so
// the consumer's pendingGeminiHistoryItems prop reads as [] even though
// the consumer's pendingLlmHistoryItems prop reads as [] even though
// pendingHistoryItemRef.current was non-null. The synchronous snapshot
// passed via info.pendingItem must override the stale React-state copy.
const mockSetText = vi.fn();
@@ -4783,7 +4783,7 @@ describe('AppContainer State Management', () => {
// Mock the streaming state and thought
const thoughtSubject = 'Processing request';
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
@@ -4831,7 +4831,7 @@ describe('AppContainer State Management', () => {
} as unknown as LoadedSettings;
// Mock the streaming state as Idle with no thought
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -4878,7 +4878,7 @@ describe('AppContainer State Management', () => {
// Mock the streaming state and thought
const thoughtSubject = 'Confirm tool execution';
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: StreamingState.WaitingForConfirmation,
submitQuery: vi.fn(),
initError: null,
@@ -4927,7 +4927,7 @@ describe('AppContainer State Management', () => {
// Mock the streaming state and thought with a short subject
const shortTitle = 'Short';
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
@@ -4981,7 +4981,7 @@ describe('AppContainer State Management', () => {
// Mock the streaming state and thought
const title = 'Test Title';
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
@@ -5032,7 +5032,7 @@ describe('AppContainer State Management', () => {
vi.stubEnv('CLI_TITLE', 'Custom Title');
// Mock the streaming state as Idle with no thought
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -5100,7 +5100,7 @@ describe('AppContainer State Management', () => {
ReturnType<Config['getChatRecordingService']>
>);
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -5200,7 +5200,7 @@ describe('AppContainer State Management', () => {
ReturnType<Config['getChatRecordingService']>
>);
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -5328,7 +5328,7 @@ describe('AppContainer State Management', () => {
mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 5 });
mockedMeasureElement.mockReturnValue({ width: 80, height: 10 }); // Footer is taller than the screen
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -5659,7 +5659,7 @@ describe('AppContainer State Management', () => {
it('should cancel ongoing request on first Ctrl+C', () => {
const mockCancelOngoingRequest = vi.fn();
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
@@ -5737,7 +5737,7 @@ describe('AppContainer State Management', () => {
request: { callId: 'call-shell-1', name: 'run_shell_command' },
promoteAbortController: promoteAc,
};
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
@@ -5794,7 +5794,7 @@ describe('AppContainer State Management', () => {
const promoteAc2 = new AbortController();
const abortSpy1 = vi.spyOn(promoteAc1, 'abort');
const abortSpy2 = vi.spyOn(promoteAc2, 'abort');
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
@@ -5854,7 +5854,7 @@ describe('AppContainer State Management', () => {
// Pin the safety contract: pressing Ctrl+B mid-prompt with no
// pending tool calls must NOT throw — falls through to the input
// layer's own Ctrl+B (cursor-left).
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
@@ -5915,7 +5915,7 @@ describe('AppContainer State Management', () => {
// be filtered out by the tool-name guard.
promoteAbortController: fakeNonShellAc,
};
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'responding',
submitQuery: vi.fn(),
initError: null,
@@ -5978,7 +5978,7 @@ describe('AppContainer State Management', () => {
const ctrlO = makeKey({ name: 'o', ctrl: true, sequence: '\x0f' });
it('Ctrl+O flips the full-detail state that expands thoughts and tool output', () => {
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -6539,7 +6539,7 @@ describe('AppContainer State Management', () => {
});
it('shows an error and returns for conversation-only rewind with no client', async () => {
const harness = renderRewindHarness({ noGeminiClient: true });
const harness = renderRewindHarness({ noLlmClient: true });
await runRewind(harness.target, 'conversation');
@@ -6556,7 +6556,7 @@ describe('AppContainer State Management', () => {
});
it('falls back to code restore for both-mode rewind with no client', async () => {
const harness = renderRewindHarness({ noGeminiClient: true });
const harness = renderRewindHarness({ noLlmClient: true });
await runRewind(harness.target, 'both');
@@ -6574,7 +6574,7 @@ describe('AppContainer State Management', () => {
it('surfaces unexpected outer errors through history', async () => {
const harness = renderRewindHarness();
vi.spyOn(mockConfig, 'getGeminiClient').mockImplementation(() => {
vi.spyOn(mockConfig, 'getLlmClient').mockImplementation(() => {
throw new Error('client exploded');
});
@@ -6623,7 +6623,7 @@ describe('AppContainer State Management', () => {
loadHistory: vi.fn(),
truncateToItem: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -6667,7 +6667,7 @@ describe('AppContainer State Management', () => {
loadHistory: vi.fn(),
truncateToItem: vi.fn(),
});
mockedUseGeminiStream.mockReturnValue({
mockedUseLlmStream.mockReturnValue({
streamingState: 'idle',
submitQuery: vi.fn(),
initError: null,
@@ -7075,8 +7075,8 @@ describe('AppContainer State Management', () => {
);
// performMemoryRefresh is the 12th arg (index 11) passed to
// useGeminiStream by AppContainer.
const calls = mockedUseGeminiStream.mock.calls;
// useLlmStream by AppContainer.
const calls = mockedUseLlmStream.mock.calls;
const performMemoryRefresh = calls[
calls.length - 1
]![11] as () => Promise<void>;
+49 -54
View File
@@ -165,10 +165,7 @@ import {
import { clearScreen } from '../utils/stdioHelpers.js';
import { useTextBuffer } from './components/shared/text-buffer.js';
import { useLogger } from './hooks/useLogger.js';
import {
useGeminiStream,
type CancelSubmitInfo,
} from './hooks/useGeminiStream.js';
import { useLlmStream, type CancelSubmitInfo } from './hooks/use-llm-stream.js';
import type { TrackedExecutingToolCall } from './hooks/useReactToolScheduler.js';
import { useVim } from './hooks/vim.js';
import {
@@ -386,7 +383,7 @@ export function useQueuedSubmissionDrain({
restoreMessages: UseMessageQueueReturn['restoreMessages'];
restorePeerMessage: UseMessageQueueReturn['restorePeerMessage'];
addHistoryItem: (item: HistoryItemWithoutId, timestamp: number) => number;
submitQuery: ReturnType<typeof useGeminiStream>['submitQuery'];
submitQuery: ReturnType<typeof useLlmStream>['submitQuery'];
submissionInFlightRef: RefObject<boolean>;
submissionSettledRevision: number;
}) {
@@ -606,11 +603,11 @@ export function getSpeculativeToolResult(response: unknown): {
}
function getResponseCandidateTokens(
pendingGeminiHistoryItems: HistoryItemWithoutId[],
pendingLlmHistoryItems: HistoryItemWithoutId[],
): number {
let tokens = 0;
for (const item of pendingGeminiHistoryItems) {
for (const item of pendingLlmHistoryItems) {
if (item.type !== 'tool_group') {
continue;
}
@@ -1053,7 +1050,7 @@ export const AppContainer = (props: AppContainerProps) => {
// the profile captures the full MCP timeline without holding back
// the user-facing TTI.
// Phase D-1: when launched with --worktree, gemini.tsx stashes a
// Phase D-1: when launched with --worktree, llm.tsx stashes a
// one-shot notice on Config. Consume it here so it surfaces in the
// transcript AND gets injected into the next user prompt. This
// wins over the Phase C resume-restore path below — startup beats
@@ -1184,7 +1181,7 @@ export const AppContainer = (props: AppContainerProps) => {
*
* 1. **16ms batch-flush of `setTools()`**: as each MCP server completes
* discover, `McpClientManager` emits `mcp-client-update`. We coalesce
* these into at most one `GeminiClient.setTools()` call per ~16ms
* these into at most one `LlmClient.setTools()` call per ~16ms
* window. With three MCP servers settling within a few ms of each
* other, the model sees one consolidated tool refresh instead of
* three back-to-back; with a server stream over 1s, the model sees
@@ -1205,8 +1202,8 @@ export const AppContainer = (props: AppContainerProps) => {
*/
useEffect(() => {
if (!isConfigInitialized) return undefined;
const geminiClient = config.getGeminiClient();
if (!geminiClient) return undefined;
const llmClient = config.getLlmClient();
if (!llmClient) return undefined;
const manager = config.getToolRegistry().getMcpClientManager();
let flushTimer: NodeJS.Timeout | null = null;
@@ -1227,11 +1224,11 @@ export const AppContainer = (props: AppContainerProps) => {
clearTimeout(flushTimer);
flushTimer = null;
}
// GeminiClient.setTools() has no try/catch around warmAll() /
// LlmClient.setTools() has no try/catch around warmAll() /
// getFunctionDeclarations() / getChat().setTools(). A silent
// discard here would make production tool-registration regressions
// invisible, so route the error through debugLogger.
return geminiClient.setTools().catch((err) => {
return llmClient.setTools().catch((err) => {
debugLogger.error(
`setTools() batch-flush failed: ${err instanceof Error ? err.message : String(err)}`,
);
@@ -1246,7 +1243,7 @@ export const AppContainer = (props: AppContainerProps) => {
}, MCP_BATCH_FLUSH_MS);
};
// Match the non-interactive entry points (`gemini.tsx`, `session.ts`,
// Match the non-interactive entry points (`llm.tsx`, `session.ts`,
// `acpAgent.ts`) which warn to stderr when MCP discovery completes with
// failed servers. The interactive path can't use stderr (it would
// collide with Ink's rendered output), so we route through
@@ -1312,9 +1309,9 @@ export const AppContainer = (props: AppContainerProps) => {
// Track idle state via ref so the update handler can defer notifications
// while the model is streaming, without triggering re-renders.
// Note: isIdleRef.current is assigned after streamingState becomes available
// (see the assignment below useGeminiStream).
// (see the assignment below useLlmStream).
const isIdleRef = useRef(true);
// Live content-area height, kept in a ref so useGeminiStream (called above the
// Live content-area height, kept in a ref so useLlmStream (called above the
// point where availableTerminalHeight is computed) can read the current value
// when bounding the pending item's rendered height. terminalWidthRef pairs
// with it so the commit loop reads width and height consistently (both live).
@@ -1936,7 +1933,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Signal the client to skip background memory tasks (extract, dream,
// skill review) so the process can exit without spawning new agent
// work during the exit window.
config.getGeminiClient()?.requestShutdown();
config.getLlmClient()?.requestShutdown();
setTimeout(async () => {
await runExitCleanup();
process.exit(0);
@@ -2275,7 +2272,7 @@ export const AppContainer = (props: AppContainerProps) => {
streamingState,
submitQuery,
initError,
pendingHistoryItems: pendingGeminiHistoryItems,
pendingHistoryItems: pendingLlmHistoryItems,
clearPendingState,
thought,
cancelOngoingRequest,
@@ -2287,8 +2284,8 @@ export const AppContainer = (props: AppContainerProps) => {
pendingToolCalls,
streamingResponseLengthRef,
isReceivingContent,
} = useGeminiStream(
config.getGeminiClient(),
} = useLlmStream(
config.getLlmClient(),
historyManager.history,
historyManager.addItem,
config,
@@ -2410,7 +2407,7 @@ export const AppContainer = (props: AppContainerProps) => {
}, []);
// Auto-accept indicator — disabled on agent tabs (agents handle their own)
const geminiClient = config.getGeminiClient();
const llmClient = config.getLlmClient();
const showAutoAcceptIndicator = useAutoAcceptIndicator({
config,
@@ -2884,7 +2881,7 @@ export const AppContainer = (props: AppContainerProps) => {
spec.status === 'completed'
) {
// Accept completed speculation: inject messages and apply files
acceptSpeculation(spec, geminiClient)
acceptSpeculation(spec, llmClient)
.then((result) => {
logSpeculation(
config,
@@ -3019,7 +3016,7 @@ export const AppContainer = (props: AppContainerProps) => {
handleSlashCommand,
slashCommands,
config,
geminiClient,
llmClient,
historyManager,
settings.merged.ui?.disableWorkflowKeywordTrigger,
setBufferText,
@@ -3047,8 +3044,8 @@ export const AppContainer = (props: AppContainerProps) => {
} = useWelcomeBack(config, handleFinalSubmit, buffer, settings.merged);
const pendingHistoryItems = useMemo(
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
() => [...pendingSlashCommandHistoryItems, ...pendingLlmHistoryItems],
[pendingSlashCommandHistoryItems, pendingLlmHistoryItems],
);
const rawStickyTodos = useMemo(
() => getStickyTodos(historyManager.history, pendingHistoryItems),
@@ -3064,14 +3061,14 @@ export const AppContainer = (props: AppContainerProps) => {
cancelHandlerRef.current = useCallback(
(info?: CancelSubmitInfo) => {
// Combine the React-state pending items (slash command, retry countdown,
// tool group, etc.) with the synchronous snapshot of the Gemini pending
// item from `useGeminiStream`. The snapshot closes the race where a
// tool group, etc.) with the synchronous snapshot of the LLM pending
// item from `useLlmStream`. The snapshot closes the race where a
// stream chunk just set `pendingHistoryItem` but the consumer's React
// state still reads as empty — without it, auto-restore could wrongly
// truncate just-committed meaningful content.
const pendingHistoryItems: HistoryItemWithoutId[] = [
...pendingSlashCommandHistoryItems,
...pendingGeminiHistoryItems,
...pendingLlmHistoryItems,
];
if (info?.pendingItem) {
pendingHistoryItems.push(info.pendingItem);
@@ -3106,7 +3103,7 @@ export const AppContainer = (props: AppContainerProps) => {
// the strip only pops trailing user entries, and a responded prompt is
// not trailing.
if (info?.wasGoalTurn) {
geminiClient?.stripOrphanedUserEntriesFromHistory?.();
llmClient?.stripOrphanedUserEntriesFromHistory?.();
}
// Restore-on-cancel: pull the just-submitted prompt back into the input
@@ -3172,7 +3169,7 @@ export const AppContainer = (props: AppContainerProps) => {
}
// Synchronous "did the turn produce any content event" flag from
// useGeminiStream. Catches the race where the pre-cancel flush
// useLlmStream. Catches the race where the pre-cancel flush
// committed gemini_content via addItem and a later thought event
// overwrote pendingHistoryItem with a synthetic value — the
// committed text isn't in historyRef.current yet (React hasn't
@@ -3244,17 +3241,17 @@ export const AppContainer = (props: AppContainerProps) => {
// in the input buffer.
refreshStatic();
restoreCancelledPrompt();
// Third cleanup leg: the in-memory chat history. `GeminiChat`
// Third cleanup leg: the in-memory chat history. `LlmChat`
// appends the user content before the stream generator runs, and
// the abort path doesn't pop it. Without this strip, the NEXT
// request's wire payload would carry the cancelled prompt as an
// orphan user turn alongside the new one — model context would
// contradict what the UI told the user was rewound. Mirrors the
// existing strip in the Retry submit path
// (GeminiClient.sendMessageStream).
geminiClient?.stripOrphanedUserEntriesFromHistory?.();
// (LlmClient.sendMessageStream).
llmClient?.stripOrphanedUserEntriesFromHistory?.();
// Also undo the cross-session ↑-history disk entry written by
// useGeminiStream's `logger.logMessage` — otherwise
// useLlmStream's `logger.logMessage` — otherwise
// getPreviousUserMessages would resurrect the cancelled prompt next
// session. Fire-and-forget; the UI restore must not block on disk
// I/O. Logger.removeLastUserMessage already swallows internal
@@ -3274,10 +3271,10 @@ export const AppContainer = (props: AppContainerProps) => {
removeGoalTurns,
historyManager,
logger,
geminiClient,
llmClient,
refreshStatic,
pendingSlashCommandHistoryItems,
pendingGeminiHistoryItems,
pendingLlmHistoryItems,
],
);
@@ -3346,7 +3343,7 @@ export const AppContainer = (props: AppContainerProps) => {
!isEditorDialogOpen &&
!showWelcomeBackDialog &&
welcomeBackChoice !== 'restart' &&
geminiClient?.isInitialized?.()
llmClient?.isInitialized?.()
) {
handleFinalSubmit(initialPrompt);
initialPromptSubmitted.current = true;
@@ -3361,7 +3358,7 @@ export const AppContainer = (props: AppContainerProps) => {
isEditorDialogOpen,
showWelcomeBackDialog,
welcomeBackChoice,
geminiClient,
llmClient,
]);
// Generate prompt suggestions when streaming completes. Enabled by default:
@@ -3403,10 +3400,10 @@ export const AppContainer = (props: AppContainerProps) => {
prevStreamingStateRef.current === StreamingState.Responding &&
streamingState === StreamingState.Idle &&
// Check both committed history and pending items for errors
// (API errors go to pendingGeminiHistoryItems, not historyManager.history)
// (API errors go to pendingLlmHistoryItems, not historyManager.history)
historyManager.history[historyManager.history.length - 1]?.type !==
'error' &&
!pendingGeminiHistoryItems.some((item) => item.type === 'error') &&
!pendingLlmHistoryItems.some((item) => item.type === 'error') &&
!shellConfirmationRequest &&
!confirmationRequest &&
!loopDetectionConfirmationRequest &&
@@ -3419,7 +3416,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Only clone the tail — full structuredClone of a large resumed session
// causes transient heap peaks that trigger OOM (#4624).
const conversationHistory = geminiClient.getHistoryTail(40, true);
const conversationHistory = llmClient.getHistoryTail(40, true);
generatePromptSuggestion(config, conversationHistory, ac.signal, {
// On by default: the schema declares `default: true`, but
// `mergeSettings` doesn't apply schema defaults, so an unset value is
@@ -3718,7 +3715,7 @@ export const AppContainer = (props: AppContainerProps) => {
mainContentHeightReservation -
tabBarHeight,
);
// Expose to useGeminiStream (called earlier) for rendered-height-aware commit.
// Expose to useLlmStream (called earlier) for rendered-height-aware commit.
availableTerminalHeightRef.current = availableTerminalHeight;
terminalWidthRef.current = terminalWidth;
@@ -3815,13 +3812,11 @@ export const AppContainer = (props: AppContainerProps) => {
// the conversation stays at the newer state.
const needsConversation =
option === 'conversation' || option === 'both';
const geminiClient = needsConversation
? config.getGeminiClient()
: null;
const llmClient = needsConversation ? config.getLlmClient() : null;
let apiTruncateIndex = -1;
let conversationSkippedNoClient = false;
if (needsConversation) {
if (!geminiClient) {
if (!llmClient) {
if (option === 'conversation') {
historyManager.addItem(
{
@@ -3841,7 +3836,7 @@ export const AppContainer = (props: AppContainerProps) => {
apiTruncateIndex = computeApiTruncationIndex(
historyManager.history,
userItem.id,
geminiClient.getHistoryShallow(),
llmClient.getHistoryShallow(),
);
if (apiTruncateIndex < 0) {
historyManager.addItem(
@@ -3873,7 +3868,7 @@ export const AppContainer = (props: AppContainerProps) => {
if (promptId) {
try {
const truncateHistory =
option === 'both' && !!geminiClient && apiTruncateIndex >= 0;
option === 'both' && !!llmClient && apiTruncateIndex >= 0;
const result = await config
.getFileHistoryService()
.rewind(promptId, truncateHistory);
@@ -3914,7 +3909,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Skip if file restore had failures in "both" mode to avoid inconsistent state.
if (
needsConversation &&
geminiClient &&
llmClient &&
apiTruncateIndex >= 0 &&
!(option === 'both' && hasRestoreFailure)
) {
@@ -3932,7 +3927,7 @@ export const AppContainer = (props: AppContainerProps) => {
if (isRealUserTurn(h)) targetTurnIndex++;
}
geminiClient.truncateHistory(apiTruncateIndex);
llmClient.truncateHistory(apiTruncateIndex);
// Strip suppressOnRestore flags and filter out collapse-summary items
// so rewound items remain visible without stale summary text
@@ -4131,7 +4126,7 @@ export const AppContainer = (props: AppContainerProps) => {
);
const responseCandidateTokens = getResponseCandidateTokens(
pendingGeminiHistoryItems,
pendingLlmHistoryItems,
);
const {
@@ -4608,7 +4603,7 @@ export const AppContainer = (props: AppContainerProps) => {
// line ~448 — Ink v6.2.3 proxies can mangle binary escape sequences).
writeTerminalTitle((value) => process.stdout.write(value), title);
}
// Exit cleanup is handled by setWindowTitle() in gemini.tsx → process.on('exit')
// Exit cleanup is handled by setWindowTitle() in llm.tsx → process.on('exit')
}, [
sessionName,
streamingState,
@@ -4687,7 +4682,7 @@ export const AppContainer = (props: AppContainerProps) => {
memoryFileCount,
streamingState,
initError,
pendingGeminiHistoryItems,
pendingLlmHistoryItems,
thought,
shellModeActive,
userMessages,
@@ -4833,7 +4828,7 @@ export const AppContainer = (props: AppContainerProps) => {
memoryFileCount,
streamingState,
initError,
pendingGeminiHistoryItems,
pendingLlmHistoryItems,
thought,
shellModeActive,
userMessages,
@@ -68,7 +68,7 @@ describe('advisorCommand', () => {
let mockContext: CommandContext;
const createConfig = (overrides: Record<string, unknown> = {}) => ({
getGeminiClient: () => ({
getLlmClient: () => ({
getHistoryForForkWindow: () => [
{ role: 'user', parts: [{ text: 'hello' }] },
],
@@ -335,7 +335,7 @@ describe('advisorCommand', () => {
mockContext = createMockCommandContext({
services: {
config: createConfig({
getGeminiClient: () => ({
getLlmClient: () => ({
getHistoryForForkWindow: () => [],
}),
}),
@@ -106,7 +106,7 @@ async function askAdvisor(
const cacheSafeParams = buildBtwCacheSafeParams(config);
if (
!cacheSafeParams ||
config.getGeminiClient().getHistoryForForkWindow().length === 0
config.getLlmClient().getHistoryForForkWindow().length === 0
) {
throw new Error(t('No conversation context available for /advisor'));
}
@@ -65,7 +65,7 @@ describe('arenaCommand agent completion history', () => {
getAvailableModelsForAuthType: vi.fn(() => []),
})),
getApprovalMode: vi.fn(() => 'default'),
getGeminiClient: vi.fn(() => ({
getLlmClient: vi.fn(() => ({
getChat: vi.fn(() => ({
getHistoryShallow: vi.fn(() => []),
})),
+1 -1
View File
@@ -205,7 +205,7 @@ function executeArenaCommand(
// its worktree directory — keeping the parent's would duplicate it.
let chatHistory;
try {
const fullHistory = config.getGeminiClient().getChat().getHistoryShallow();
const fullHistory = config.getLlmClient().getChat().getHistoryShallow();
chatHistory = stripStartupContext(fullHistory);
} catch {
debugLogger.debug('Could not retrieve chat history for arena agents');
@@ -57,7 +57,7 @@ describe('btwCommand', () => {
let mockContext: CommandContext;
const createConfig = (overrides: Record<string, unknown> = {}) => ({
getGeminiClient: () => ({}),
getLlmClient: () => ({}),
getModel: () => 'test-model',
getSessionId: () => 'test-session-id',
getApprovalMode: () => 'default',
@@ -55,7 +55,7 @@ describe('cdCommand', () => {
getWorkingDir: () => currentDir,
isRestrictiveSandbox: () => false,
relocateWorkingDirectory,
getGeminiClient: () => ({
getLlmClient: () => ({
addWorkingDirectoryChangedContext,
}),
} as unknown as Config,
@@ -164,7 +164,7 @@ describe('cdCommand', () => {
getWorkingDir: () => currentDir,
isRestrictiveSandbox: () => true,
relocateWorkingDirectory,
getGeminiClient: () => ({
getLlmClient: () => ({
addWorkingDirectoryChangedContext,
}),
} as unknown as Config,
+1 -1
View File
@@ -212,7 +212,7 @@ export const cdCommand: SlashCommand = {
try {
await config
.getGeminiClient()
.getLlmClient()
?.addWorkingDirectoryChangedContext(realOldDir, realTargetPath);
} catch (error) {
warnings.push(
@@ -24,7 +24,7 @@ vi.mock('@qwen-code/qwen-code-core', async () => {
};
});
import type { GeminiClient } from '@qwen-code/qwen-code-core';
import type { LlmClient } from '@qwen-code/qwen-code-core';
describe('clearCommand', () => {
let mockContext: CommandContext;
@@ -60,10 +60,10 @@ describe('clearCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
resetChat: mockResetChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
getBackgroundTaskRegistry: vi.fn().mockReturnValue({
hasRunningTasks: vi.fn().mockReturnValue(false),
reset: mockResetBackgroundTasks,
@@ -335,9 +335,9 @@ describe('clearCommand', () => {
abortAll: mockAbortBackgroundShells,
}),
startNewSession: mockStartNewSession,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
resetChat: mockResetChat,
} as unknown as GeminiClient),
} as unknown as LlmClient),
getModel: vi.fn().mockReturnValue('test-model'),
getApprovalMode: vi.fn().mockReturnValue('default'),
getToolRegistry: vi.fn().mockReturnValue({
@@ -433,9 +433,9 @@ describe('clearCommand', () => {
}),
getHookSystem: mockGetHookSystem,
startNewSession: mockStartNewSession,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
resetChat: mockResetChat,
} as unknown as GeminiClient),
} as unknown as LlmClient),
getModel: vi.fn().mockReturnValue('test-model'),
getApprovalMode: vi.fn().mockReturnValue('default'),
getToolRegistry: vi.fn().mockReturnValue({
@@ -506,9 +506,9 @@ describe('clearCommand', () => {
}),
getHookSystem: mockGetHookSystem,
startNewSession: mockStartNewSession,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
resetChat: mockResetChat,
} as unknown as GeminiClient),
} as unknown as LlmClient),
getModel: vi.fn().mockReturnValue('test-model'),
getApprovalMode: vi.fn().mockReturnValue('default'),
getToolRegistry: vi.fn().mockReturnValue({
@@ -575,9 +575,9 @@ describe('clearCommand', () => {
}),
getHookSystem: mockGetHookSystem,
startNewSession: mockStartNewSession,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
resetChat: mockResetChat,
} as unknown as GeminiClient),
} as unknown as LlmClient),
getModel: vi.fn().mockReturnValue('test-model'),
getApprovalMode: vi.fn().mockReturnValue('default'),
getToolRegistry: vi.fn().mockReturnValue({
@@ -645,9 +645,9 @@ describe('clearCommand', () => {
}),
getHookSystem: mockGetHookSystem,
startNewSession: mockStartNewSession,
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
resetChat: mockResetChat,
} as unknown as GeminiClient),
} as unknown as LlmClient),
getModel: vi.fn().mockReturnValue('test-model'),
getApprovalMode: vi.fn().mockReturnValue('default'),
getToolRegistry: vi.fn().mockReturnValue({
+3 -3
View File
@@ -116,14 +116,14 @@ export const clearCommand: SlashCommand = {
// Clear UI first for immediate responsiveness
context.ui.clear();
const geminiClient = config.getGeminiClient();
if (geminiClient) {
const llmClient = config.getLlmClient();
if (llmClient) {
context.ui.setDebugMessage(
t('Starting a new session, resetting chat, and clearing terminal.'),
);
// If resetChat fails, the exception will propagate and halt the command,
// which is the correct behavior to signal a failure to the user.
await geminiClient.resetChat();
await llmClient.resetChat();
} else {
context.ui.setDebugMessage(t('Starting a new session and clearing.'));
}
@@ -7,7 +7,7 @@
import {
CompressionStatus,
type ChatCompressionInfo,
type GeminiClient,
type LlmClient,
} from '@qwen-code/qwen-code-core';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { compressCommand } from './compressCommand.js';
@@ -23,10 +23,10 @@ describe('compressCommand', () => {
context = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -181,10 +181,10 @@ describe('compressCommand', () => {
executionMode: 'non_interactive',
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -213,10 +213,10 @@ describe('compressCommand', () => {
executionMode: 'acp',
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -271,10 +271,10 @@ describe('compressCommand', () => {
const ctx = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
invocation: {
@@ -296,10 +296,10 @@ describe('compressCommand', () => {
const ctx = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
invocation: { raw: '/compress ', name: 'compress', args: ' ' },
@@ -318,10 +318,10 @@ describe('compressCommand', () => {
const ctx = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
invocation: {
@@ -341,10 +341,10 @@ describe('compressCommand', () => {
const ctx = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
invocation: { raw: `/compress ${long}`, name: 'compress', args: long },
@@ -363,10 +363,10 @@ describe('compressCommand', () => {
const ctx = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
invocation: {
@@ -57,8 +57,8 @@ export const compressCommand: SlashCommand = {
};
const config = context.services.config;
const geminiClient = config?.getGeminiClient();
if (!config || !geminiClient) {
const llmClient = config?.getLlmClient();
if (!config || !llmClient) {
return {
type: 'message',
messageType: 'error',
@@ -82,7 +82,7 @@ export const compressCommand: SlashCommand = {
const doCompress = async () => {
const promptId = `compress-${Date.now()}`;
return await geminiClient.tryCompressChat(
return await llmClient.tryCompressChat(
promptId,
true,
abortSignal,
@@ -7,7 +7,7 @@
import {
CompressionStatus,
type ChatCompressionInfo,
type GeminiClient,
type LlmClient,
} from '@qwen-code/qwen-code-core';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { compressFastCommand } from './compressFastCommand.js';
@@ -23,10 +23,10 @@ describe('compressFastCommand', () => {
context = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -75,10 +75,10 @@ describe('compressFastCommand', () => {
},
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -108,10 +108,10 @@ describe('compressFastCommand', () => {
},
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -225,10 +225,10 @@ describe('compressFastCommand', () => {
executionMode: 'non_interactive',
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -266,10 +266,10 @@ describe('compressFastCommand', () => {
executionMode: 'non_interactive',
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -294,10 +294,10 @@ describe('compressFastCommand', () => {
executionMode: 'non_interactive',
services: {
config: {
getGeminiClient: () =>
getLlmClient: () =>
({
tryCompressChatFast: mockTryCompressChatFast,
}) as unknown as GeminiClient,
}) as unknown as LlmClient,
},
},
});
@@ -59,8 +59,8 @@ export const compressFastCommand: SlashCommand = {
};
const config = context.services.config;
const geminiClient = config?.getGeminiClient();
if (!config || !geminiClient) {
const llmClient = config?.getLlmClient();
if (!config || !llmClient) {
return {
type: 'message',
messageType: 'error',
@@ -68,7 +68,7 @@ export const compressFastCommand: SlashCommand = {
};
}
const doCompress = async () => await geminiClient.tryCompressChatFast();
const doCompress = async () => await llmClient.tryCompressChatFast();
if (executionMode === 'acp') {
const messages = async function* () {
@@ -128,7 +128,7 @@ describe('collectContextData (contextCommand)', () => {
const isLastPromptTokenCountEstimated = vi.fn().mockReturnValue(false);
const config = {
...makeMockConfig(200_000),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
getChat: vi.fn().mockReturnValue({
getLastPromptTokenCount,
@@ -148,7 +148,7 @@ describe('collectContextData (contextCommand)', () => {
it('reports a nonzero compression-derived count as estimated', async () => {
const config = {
...makeMockConfig(200_000),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
getChat: vi.fn().mockReturnValue({
getLastPromptTokenCount: vi.fn().mockReturnValue(50_000),
@@ -173,7 +173,7 @@ describe('collectContextData (contextCommand)', () => {
mockGetLastPromptTokenCount.mockReturnValue(60_000);
const config = {
...makeMockConfig(200_000),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(false),
getChat: vi.fn(() => {
throw new Error('Chat not initialized');
@@ -35,7 +35,7 @@ import * as path from 'node:path';
/**
* Classify a token count against the three-tier compaction ladder. Mirrors
* the gating logic in `chatCompressionService` / `geminiChat` so the
* the gating logic in `chatCompressionService` / `llmChat` so the
* `/context` output's "current tier" label reflects exactly which tier the
* runtime would treat the session as sitting in.
*/
@@ -107,15 +107,15 @@ export async function collectContextData(
// (#5763). The active chat carries the correct per-session value; fall back
// to the global singleton only when no chat exists yet (first /context,
// --continue resume before any send).
const geminiClient = config.getGeminiClient?.();
const activeChat = geminiClient?.isInitialized?.()
? geminiClient.getChat()
const llmClient = config.getLlmClient?.();
const activeChat = llmClient?.isInitialized?.()
? llmClient.getChat()
: undefined;
const apiTotalTokens = activeChat
? activeChat.getLastPromptTokenCount()
: uiTelemetryService.getLastPromptTokenCount();
// Cached-content tokens have no per-chat mirror today (only the global
// singleton is written, geminiChat.ts), so this read stays global. It only
// singleton is written, llm-chat.ts), so this read stays global. It only
// refines the messages-vs-cache split, not the headline total or tier.
const apiCachedTokens = uiTelemetryService.getLastCachedContentTokenCount();
@@ -33,7 +33,7 @@ describe('copyCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () => ({
getLlmClient: () => ({
getChat: mockGetChat,
}),
getDebugLogger: () => ({
+1 -1
View File
@@ -365,7 +365,7 @@ export const copyCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
supportedModes: ['interactive'] as const,
action: async (context, args): Promise<SlashCommandActionReturn | void> => {
const chat = await context.services.config?.getGeminiClient()?.getChat();
const chat = await context.services.config?.getLlmClient()?.getChat();
const history = chat?.getHistoryShallow();
const aiMessages = history?.filter((item) => item.role === 'model') ?? [];
@@ -76,7 +76,7 @@ describe('directoryCommand', () => {
mockConfig = {
getWorkspaceContext: () => mockWorkspaceContext,
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
addDirectoryContext: vi.fn(),
}),
getWorkingDir: () => '/test/dir',
@@ -450,11 +450,11 @@ describe('directoryCommand', () => {
});
it('should warn when gemini.addDirectoryContext throws', async () => {
vi.mocked(mockConfig.getGeminiClient).mockReturnValue({
vi.mocked(mockConfig.getLlmClient).mockReturnValue({
addDirectoryContext: vi
.fn()
.mockRejectedValue(new Error('gemini unavailable')),
} as unknown as ReturnType<typeof mockConfig.getGeminiClient>);
} as unknown as ReturnType<typeof mockConfig.getLlmClient>);
const newPath = path.normalize('/home/user/new-project');
if (!addCommand?.action) throw new Error('No action');
const result = await addCommand.action(mockContext, newPath);
@@ -281,7 +281,7 @@ export const directoryCommand: SlashCommand = {
}
if (added.length > 0) {
const gemini = config.getGeminiClient();
const gemini = config.getLlmClient();
if (gemini) {
try {
await gemini.addDirectoryContext();
@@ -38,7 +38,7 @@ describe('runDoctorChecks', () => {
services: {
config: {
getAuthType: vi.fn().mockReturnValue('openai'),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
}),
getModel: vi.fn().mockReturnValue('gpt-4'),
@@ -110,7 +110,7 @@ describe('runDoctorChecks', () => {
services: {
config: {
getAuthType: vi.fn().mockReturnValue(undefined),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(false),
}),
getModel: vi.fn().mockReturnValue('gpt-4'),
@@ -227,7 +227,7 @@ describe('runDoctorChecks', () => {
services: {
config: {
getAuthType: vi.fn().mockReturnValue('openai'),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
}),
getModel: vi.fn().mockReturnValue('gpt-4'),
@@ -253,7 +253,7 @@ describe('runDoctorChecks', () => {
services: {
config: {
getAuthType: vi.fn().mockReturnValue('openai'),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
}),
getModel: vi.fn().mockReturnValue('gpt-4'),
@@ -282,7 +282,7 @@ describe('runDoctorChecks', () => {
services: {
config: {
getAuthType: vi.fn().mockReturnValue('openai'),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
}),
getModel: vi.fn().mockReturnValue('gpt-4'),
@@ -313,7 +313,7 @@ describe('runDoctorChecks', () => {
services: {
config: {
getAuthType: vi.fn().mockReturnValue('openai'),
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
}),
getModel: vi.fn().mockReturnValue('gpt-4'),
+1 -1
View File
@@ -150,7 +150,7 @@ async function checkApiClient(
}
try {
const client = config.getGeminiClient();
const client = config.getLlmClient();
if (client.isInitialized()) {
return {
category: t('Authentication'),
@@ -821,7 +821,7 @@ describe('doctorCommand', () => {
getSessionId: () => 'test-session',
getCliVersion: () => '0.0.0',
getTruncateToolOutputThreshold: () => 25_000,
getGeminiClient: () => ({
getLlmClient: () => ({
getHistoryShallow: () => {
if (options.historyThrows) {
throw new Error('history unavailable');
@@ -1014,7 +1014,7 @@ describe('doctorCommand', () => {
getSessionId: () => 'test-session',
getCliVersion: () => '0.0.0',
getTruncateToolOutputThreshold: () => Number.POSITIVE_INFINITY,
getGeminiClient: () => ({
getLlmClient: () => ({
getHistoryShallow: () => [],
}),
getToolRegistry: () => ({
@@ -1108,7 +1108,7 @@ describe('doctorCommand', () => {
config: {
getSessionId: () => 'test-session',
getCliVersion: () => '0.0.0',
getGeminiClient: () => ({
getLlmClient: () => ({
getHistoryShallow: () => history,
}),
},
@@ -475,7 +475,7 @@ function collectToolResultRetention(
): ToolResultRetentionReport | null {
try {
const config = context.services.config;
const history = config?.getGeminiClient()?.getHistoryShallow();
const history = config?.getLlmClient()?.getHistoryShallow();
if (!history) {
return null;
}
@@ -41,7 +41,7 @@ describe('forkCommand', () => {
];
const createConfig = (overrides: Record<string, unknown> = {}) => ({
getGeminiClient: () => ({
getLlmClient: () => ({
addHistory: mockAddHistory,
getHistoryShallow: () => historyWithTurn,
}),
@@ -121,7 +121,7 @@ describe('forkCommand', () => {
const fresh = createMockCommandContext({
services: {
config: createConfig({
getGeminiClient: () => ({ getHistoryShallow: () => [] }),
getLlmClient: () => ({ getHistoryShallow: () => [] }),
}),
},
});
@@ -137,7 +137,7 @@ describe('forkCommand', () => {
const unreadableHistory = createMockCommandContext({
services: {
config: createConfig({
getGeminiClient: () => ({
getLlmClient: () => ({
getHistoryShallow: () => {
throw new Error('history unavailable');
},
+2 -3
View File
@@ -92,8 +92,7 @@ export const forkCommand: SlashCommand = {
// Guard: a fork inherits the conversation history; there must be one.
let hasHistory = false;
try {
hasHistory =
(config.getGeminiClient().getHistoryShallow() ?? []).length > 0;
hasHistory = (config.getLlmClient().getHistoryShallow() ?? []).length > 0;
} catch (error) {
debugLogger.debug('Failed to read history before /fork:', error);
hasHistory = false;
@@ -167,7 +166,7 @@ export const forkCommand: SlashCommand = {
}
try {
config.getGeminiClient().addHistory({
config.getLlmClient().addHistory({
role: 'user',
parts: [
{
@@ -562,7 +562,7 @@ describe('languageCommand', () => {
const refreshHierarchicalMemory = vi.fn().mockResolvedValue(undefined);
const refreshSystemInstruction = vi.fn().mockResolvedValue(undefined);
const getGeminiClient = vi
const getLlmClient = vi
.fn()
.mockReturnValue({ refreshSystemInstruction });
(
@@ -574,7 +574,7 @@ describe('languageCommand', () => {
getOutputLanguageFilePath: vi.fn().mockReturnValue(undefined),
setOutputLanguageFilePath: vi.fn(),
refreshHierarchicalMemory,
getGeminiClient,
getLlmClient,
};
const result = await languageCommand.action(mockContext, 'output auto');
@@ -606,7 +606,7 @@ describe('languageCommand', () => {
const refreshHierarchicalMemory = vi.fn().mockResolvedValue(undefined);
const refreshSystemInstruction = vi.fn().mockResolvedValue(undefined);
const getGeminiClient = vi
const getLlmClient = vi
.fn()
.mockReturnValue({ refreshSystemInstruction });
(
@@ -618,7 +618,7 @@ describe('languageCommand', () => {
getOutputLanguageFilePath: vi.fn().mockReturnValue(undefined),
setOutputLanguageFilePath: vi.fn(),
refreshHierarchicalMemory,
getGeminiClient,
getLlmClient,
};
const result = await languageCommand.action(
@@ -627,7 +627,7 @@ describe('languageCommand', () => {
);
expect(refreshHierarchicalMemory).toHaveBeenCalledTimes(1);
expect(getGeminiClient).toHaveBeenCalledTimes(1);
expect(getLlmClient).toHaveBeenCalledTimes(1);
expect(refreshSystemInstruction).toHaveBeenCalledTimes(1);
// Memory MUST be refreshed before the system instruction is rebuilt;
// otherwise the new instruction would be built from stale userMemory
@@ -661,7 +661,7 @@ describe('languageCommand', () => {
getOutputLanguageFilePath: vi.fn().mockReturnValue(undefined),
setOutputLanguageFilePath: vi.fn(),
refreshHierarchicalMemory,
// No getGeminiClient — refreshSystemInstruction must not be reached.
// No getLlmClient — refreshSystemInstruction must not be reached.
};
const result = await languageCommand.action(mockContext, 'output Korean');
@@ -233,7 +233,7 @@ async function setOutputLanguage(
if (config) {
try {
await config.refreshHierarchicalMemory();
await config.getGeminiClient().refreshSystemInstruction();
await config.getLlmClient().refreshSystemInstruction();
} catch (error) {
debugLogger.warn(
'Failed to apply output language to running session:',
@@ -39,7 +39,7 @@ describe('mcpCommand', () => {
getMcpServers: ReturnType<typeof vi.fn>;
getBlockedMcpServers: ReturnType<typeof vi.fn>;
getPromptRegistry: ReturnType<typeof vi.fn>;
getGeminiClient: ReturnType<typeof vi.fn>;
getLlmClient: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
@@ -65,7 +65,7 @@ describe('mcpCommand', () => {
getAllPrompts: vi.fn().mockReturnValue([]),
getPromptsByServer: vi.fn().mockReturnValue([]),
}),
getGeminiClient: vi.fn(),
getLlmClient: vi.fn(),
};
mockContext = createMockCommandContext({
@@ -44,7 +44,7 @@ describe('restoreCommand', () => {
getProjectTempCheckpointsDir: vi.fn().mockReturnValue(checkpointsDir),
getProjectTempDir: vi.fn().mockReturnValue(geminiTempDir),
},
getGeminiClient: vi.fn().mockReturnValue({
getLlmClient: vi.fn().mockReturnValue({
setHistory: mockSetHistory,
}),
} as unknown as Config;
@@ -149,7 +149,7 @@ async function restoreAction(
}
if (toolCallData.clientHistory) {
await config?.getGeminiClient()?.setHistory(toolCallData.clientHistory);
await config?.getLlmClient()?.setHistory(toolCallData.clientHistory);
}
return {
@@ -34,7 +34,7 @@ const makeContext = (projectRoot: string): CommandContext => {
};
const config = {
getProjectRoot: () => projectRoot,
getGeminiClient: () => ({ getChat: () => chat }),
getLlmClient: () => ({ getChat: () => chat }),
getModel: () => 'test-model',
};
return createMockCommandContext({
@@ -578,7 +578,7 @@ describe('summaryCommand custom export path', () => {
};
const config = {
getProjectRoot: () => projectRoot,
getGeminiClient: () => ({ getChat: () => chat }),
getLlmClient: () => ({ getChat: () => chat }),
getModel: () => 'test-model',
};
const context = createMockCommandContext({
@@ -128,8 +128,8 @@ export const summaryCommand: SlashCommand = {
};
}
const geminiClient = config.getGeminiClient();
if (!geminiClient) {
const llmClient = config.getLlmClient();
if (!llmClient) {
return {
type: 'message',
messageType: 'error',
@@ -158,7 +158,7 @@ export const summaryCommand: SlashCommand = {
}
const getChatHistory = () => {
const chat = geminiClient.getChat();
const chat = llmClient.getChat();
return chat.getHistoryShallow();
};
@@ -182,8 +182,8 @@ export const summaryCommand: SlashCommand = {
// Carry over the main session's system instruction. Without this the
// model sees only chat history + the summary prompt, losing the coding-
// assistant role, project context, and user memory. The chat sets it
// as a string (see GeminiClient.getMainSessionSystemInstruction).
const rawSystemInstruction = geminiClient
// as a string (see LlmClient.getMainSessionSystemInstruction).
const rawSystemInstruction = llmClient
.getChat()
.getGenerationConfig().systemInstruction;
const chatSystemInstruction =
+2 -2
View File
@@ -42,11 +42,11 @@ export const toolsCommand: SlashCommand = {
const tools = toolRegistry.getAllTools();
// Filter out MCP tools by checking for the absence of a serverName property
const geminiTools = tools.filter((tool) => !('serverName' in tool));
const llmTools = tools.filter((tool) => !('serverName' in tool));
const toolsListItem: HistoryItemToolsList = {
type: MessageType.TOOLS_LIST,
tools: geminiTools.map((tool) => ({
tools: llmTools.map((tool) => ({
name: tool.name,
displayName: tool.displayName,
description: tool.description,
+1 -1
View File
@@ -254,7 +254,7 @@ export interface LoadHistoryActionReturn {
/**
* The return type for a command action that should immediately submit
* content as a prompt to the Gemini model.
* content as a prompt to the model.
*/
export interface SubmitPromptActionReturn {
type: 'submit_prompt';
@@ -149,7 +149,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
streamingResponseLengthRef: { current: 0 },
voiceMicWarnedStatusRef: { current: null },
isReceivingContent: false,
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
terminalWidth: 80,
...overrides,
}) as UIState;
@@ -389,7 +389,7 @@ describe('<HistoryItemDisplay />', () => {
expect(lastFrame()).toMatchSnapshot();
});
it('should render a full gemini item when using availableTerminalHeightGemini', () => {
it('should render a full gemini item when using availableTerminalHeightLlm', () => {
const item: HistoryItem = {
id: 1,
type: 'gemini',
@@ -401,7 +401,7 @@ describe('<HistoryItemDisplay />', () => {
isPending={false}
terminalWidth={80}
availableTerminalHeight={10}
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
availableTerminalHeightLlm={Number.MAX_SAFE_INTEGER}
/>,
);
@@ -426,7 +426,7 @@ describe('<HistoryItemDisplay />', () => {
expect(lastFrame()).toMatchSnapshot();
});
it('should render a full gemini_content item when using availableTerminalHeightGemini', () => {
it('should render a full gemini_content item when using availableTerminalHeightLlm', () => {
const item: HistoryItem = {
id: 1,
type: 'gemini_content',
@@ -438,7 +438,7 @@ describe('<HistoryItemDisplay />', () => {
isPending={false}
terminalWidth={80}
availableTerminalHeight={10}
availableTerminalHeightGemini={Number.MAX_SAFE_INTEGER}
availableTerminalHeightLlm={Number.MAX_SAFE_INTEGER}
/>,
);
@@ -83,7 +83,7 @@ interface HistoryItemDisplayProps {
commands?: readonly SlashCommand[];
activeShellPtyId?: number | null;
embeddedShellFocused?: boolean;
availableTerminalHeightGemini?: number;
availableTerminalHeightLlm?: number;
sourceCopyIndexOffsets?: MarkdownSourceCopyIndexOffsets;
/** Force thinking blocks expanded (e.g. in SessionPreview). */
thoughtExpanded?: boolean;
@@ -237,7 +237,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
isFocused = true,
activeShellPtyId,
embeddedShellFocused,
availableTerminalHeightGemini,
availableTerminalHeightLlm,
sourceCopyIndexOffsets,
thoughtExpanded,
fullDetail = false,
@@ -306,7 +306,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
omittedImageCount={itemForDisplay.omittedImageCount}
isPending={isPending}
availableTerminalHeight={
availableTerminalHeightGemini ?? availableTerminalHeight
availableTerminalHeightLlm ?? availableTerminalHeight
}
contentWidth={contentWidth}
sourceCopyIndexOffsets={sourceCopyIndexOffsets}
@@ -320,7 +320,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
omittedImageCount={itemForDisplay.omittedImageCount}
isPending={isPending}
availableTerminalHeight={
availableTerminalHeightGemini ?? availableTerminalHeight
availableTerminalHeightLlm ?? availableTerminalHeight
}
contentWidth={contentWidth}
sourceCopyIndexOffsets={sourceCopyIndexOffsets}
@@ -332,7 +332,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
isPending={isPending}
expanded={resolvedThoughtExpanded}
availableTerminalHeight={
availableTerminalHeightGemini ?? availableTerminalHeight
availableTerminalHeightLlm ?? availableTerminalHeight
}
contentWidth={contentWidth}
durationMs={itemForDisplay.durationMs}
@@ -345,7 +345,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
isPending={isPending}
expanded={resolvedThoughtExpanded}
availableTerminalHeight={
availableTerminalHeightGemini ?? availableTerminalHeight
availableTerminalHeightLlm ?? availableTerminalHeight
}
contentWidth={contentWidth}
/>
@@ -23,7 +23,8 @@ describe('IdeTrustChangeDialog', () => {
expect(frameText).toContain(
'Workspace trust has changed due to a change in the IDE connection.',
);
expect(frameText).toContain("Press 'r' to restart Gemini");
expect(frameText).toContain("Press 'r' to restart Qwen");
expect(frameText).toContain('Code and apply the changes.');
});
it('renders the correct message for TRUST_CHANGE', () => {
@@ -35,7 +36,7 @@ describe('IdeTrustChangeDialog', () => {
expect(frameText).toContain(
'Workspace trust has changed due to a change in the IDE trust.',
);
expect(frameText).toContain("Press 'r' to restart Gemini");
expect(frameText).toContain("Press 'r' to restart Qwen Code");
});
it('renders a generic message for NONE reason', () => {
@@ -43,7 +43,8 @@ export const IdeTrustChangeDialog = ({ reason }: IdeTrustChangeDialogProps) => {
return (
<Box borderStyle="round" borderColor={theme.status.warning} paddingX={1}>
<Text color={theme.status.warning}>
{message} Press &apos;r&apos; to restart Gemini to apply the changes.
{message} Press &apos;r&apos; to restart Qwen Code and apply the
changes.
</Text>
</Box>
);
@@ -221,7 +221,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: [],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
} as unknown as ReturnType<typeof useUIState>);
mockedUseUIActions.mockReturnValue({
handleRetryLastPrompt: vi.fn(),
@@ -640,7 +640,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: true,
messageQueue: [],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
} as unknown as ReturnType<typeof useUIState>);
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
@@ -667,7 +667,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: [],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
historyManager: { addItem },
} as unknown as ReturnType<typeof useUIState>);
vi.mocked(createVoiceRecorder).mockReturnValue({
@@ -1756,7 +1756,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: [],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
historyManager: { addItem },
} as unknown as ReturnType<typeof useUIState>);
vi.mocked(clipboardUtils.clipboardHasImage).mockImplementation(
@@ -5485,7 +5485,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: [],
pendingGeminiHistoryItems: [
pendingLlmHistoryItems: [
{
type: 'tool_group',
tools: [
@@ -5988,7 +5988,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: ['queued message'],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
streamingState: StreamingState.Responding,
} as unknown as ReturnType<typeof useUIState>);
const mockPopAllQueued = vi.fn(() => null);
@@ -6019,7 +6019,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: [],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
streamingState: StreamingState.Responding,
} as unknown as ReturnType<typeof useUIState>);
@@ -6046,7 +6046,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: [],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
streamingState: StreamingState.Responding,
} as unknown as ReturnType<typeof useUIState>);
const mockPopAllQueued = vi.fn(() => null);
@@ -6081,7 +6081,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: ['queued follow-up'],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
streamingState: StreamingState.Responding,
} as unknown as ReturnType<typeof useUIState>);
const mockPopAllQueued = vi.fn(() => null);
@@ -6115,7 +6115,7 @@ describe('InputPrompt', () => {
mockedUseUIState.mockReturnValue({
isFeedbackDialogOpen: false,
messageQueue: [],
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
streamingState: StreamingState.Responding,
} as unknown as ReturnType<typeof useUIState>);
props.buffer.setText('draft to clear');
@@ -304,12 +304,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const hasActiveToolConfirmation = useMemo(
() =>
Boolean(uiState.confirmationRequest) ||
(uiState.pendingGeminiHistoryItems ?? []).some(
(uiState.pendingLlmHistoryItems ?? []).some(
(item) =>
item.type === 'tool_group' &&
item.tools.some((tool) => tool.confirmationDetails),
),
[uiState.confirmationRequest, uiState.pendingGeminiHistoryItems],
[uiState.confirmationRequest, uiState.pendingLlmHistoryItems],
);
const [historyRestoredText, setHistoryRestoredText] = useState<string | null>(
null,
@@ -175,7 +175,7 @@ const createUIState = (overrides: Partial<UIState> = {}): UIState =>
memoryFileCount: 0,
streamingState: {} as UIState['streamingState'],
initError: null,
pendingGeminiHistoryItems: [],
pendingLlmHistoryItems: [],
thought: null,
shellModeActive: false,
userMessages: [],
@@ -1364,7 +1364,7 @@ describe('<MainContent />', () => {
expect(historyItemDisplayPropsSpy.mock.calls.at(-1)?.[0]).toEqual(
expect.objectContaining({
availableTerminalHeight: 100,
availableTerminalHeightGemini: 65536,
availableTerminalHeightLlm: 65536,
}),
);
@@ -1394,7 +1394,7 @@ describe('<MainContent />', () => {
expect(historyItemDisplayPropsSpy.mock.calls.at(-1)?.[0]).toEqual(
expect.objectContaining({
availableTerminalHeight: undefined,
availableTerminalHeightGemini: undefined,
availableTerminalHeightLlm: undefined,
}),
);
});
@@ -42,8 +42,8 @@ import {
import { TextSelectionController } from '../selection/use-text-selection.js';
import { measureElementPosition } from '../utils/measure-element-position.js';
// Limit Gemini messages to a very high number of lines to mitigate performance
// issues in the worst case if we somehow get an enormous response from Gemini.
// Limit LLM messages to a very high number of lines to mitigate performance
// issues in the worst case if we somehow get an enormous model response.
// This threshold is arbitrary but should be high enough to never impact normal
// usage.
const MAX_GEMINI_MESSAGE_LINES = 65536;
@@ -463,7 +463,7 @@ export const MainContent = ({ footerRef }: MainContentProps) => {
availableTerminalHeight={
uiState.constrainHeight ? staticAreaMaxItemHeight : undefined
}
availableTerminalHeightGemini={
availableTerminalHeightLlm={
uiState.constrainHeight ? MAX_GEMINI_MESSAGE_LINES : undefined
}
item={item}
@@ -554,7 +554,7 @@ export const MainContent = ({ footerRef }: MainContentProps) => {
terminalWidth={terminalWidth}
mainAreaWidth={mainAreaWidth}
availableTerminalHeight={staticAreaMaxItemHeight}
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
availableTerminalHeightLlm={MAX_GEMINI_MESSAGE_LINES}
key={h.id}
item={h}
isPending={false}
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<HistoryItemDisplay /> > should render a full gemini item when using availableTerminalHeightGemini 1`] = `
exports[`<HistoryItemDisplay /> > should render a full gemini item when using availableTerminalHeightLlm 1`] = `
"
◆︎ Example code block:
1 Line 1
@@ -55,7 +55,7 @@ exports[`<HistoryItemDisplay /> > should render a full gemini item when using av
50 Line 50"
`;
exports[`<HistoryItemDisplay /> > should render a full gemini_content item when using availableTerminalHeightGemini 1`] = `
exports[`<HistoryItemDisplay /> > should render a full gemini_content item when using availableTerminalHeightLlm 1`] = `
" Example code block:
1 Line 1
2 Line 2
@@ -1473,7 +1473,7 @@ describe('BackgroundTasksDialog', () => {
it('renders the Error block on failed status with a "+ Stopped because" verb', () => {
// Dream failures need to surface — they are the user's only signal
// that consolidation didn't happen as expected (success path
// already produces a memory_saved toast in useGeminiStream).
// already produces a memory_saved toast in useLlmStream).
const h = setup([
dreamEntry({
status: 'failed',
@@ -139,9 +139,9 @@ export const AuthenticateStep: React.FC<AuthenticateStepProps> = ({
}
// Update the client with the new tools
const geminiClient = config.getGeminiClient();
if (geminiClient) {
await geminiClient.setTools();
const llmClient = config.getLlmClient();
if (llmClient) {
await llmClient.setTools();
}
setMessages((prev) => [
@@ -100,7 +100,7 @@ export interface UIState {
memoryFileCount: number;
streamingState: StreamingState;
initError: string | null;
pendingGeminiHistoryItems: HistoryItemWithoutId[];
pendingLlmHistoryItems: HistoryItemWithoutId[];
thought: ThoughtSummary | null;
shellModeActive: boolean;
userMessages: string[];
+1 -1
View File
@@ -116,7 +116,7 @@ describe('handleAutoUpdate', () => {
});
it('should show manual update message when enableAutoUpdate is false', () => {
// When enableAutoUpdate is false, gemini.tsx won't call checkForUpdates(),
// When enableAutoUpdate is false, llm.tsx won't call checkForUpdates(),
// but if handleAutoUpdate is still called, it should show a manual update message.
mockSettings.merged.general!.enableAutoUpdate = false;
mockGetInstallationInfo.mockReturnValue({
+1 -1
View File
@@ -40,7 +40,7 @@ export async function handleAutoUpdate(
return;
}
// enableAutoUpdate is checked in gemini.tsx before calling this function,
// enableAutoUpdate is checked in llm.tsx before calling this function,
// so if we get here, auto-update is enabled (or undefined, which defaults to enabled).
const isAutoUpdateEnabled =
settings.merged.general?.enableAutoUpdate !== false;
@@ -10,7 +10,7 @@
* must not leave that replay in the process-wide usage aggregate (and must
* not let the rollback's own re-initialize add a second copy on top).
*
* These tests drive the REAL hooks against the REAL GeminiClient and the
* These tests drive the REAL hooks against the REAL LlmClient and the
* REAL UiTelemetryService singleton — only the startChat side of the client
* and the session-service I/O are stubbed — so the assertions observe the
* actual aggregate the /stats display and persistSessionUsage read.
@@ -23,7 +23,7 @@ import { useBranchCommand } from './useBranchCommand.js';
import { useResumeCommand } from './useResumeCommand.js';
import type { LoadedSettings } from '../../config/settings.js';
// Replace only SessionService (file I/O); keep GeminiClient and the real
// Replace only SessionService (file I/O); keep LlmClient and the real
// uiTelemetryService singleton intact.
const sessionServiceMocks = vi.hoisted(() => ({
sessions: new Map<string, { conversation: unknown }>(),
@@ -72,7 +72,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
});
import {
GeminiClient,
LlmClient,
uiTelemetryService,
EVENT_API_RESPONSE,
type Config,
@@ -158,7 +158,7 @@ function promptTokens(): number {
}
/**
* A stateful fake Config wired to a REAL GeminiClient. Tracks the live
* A stateful fake Config wired to a REAL LlmClient. Tracks the live
* session id + resumed data exactly like Config.startNewSession does, so
* client.initialize() sees the same facts the hooks set.
*/
@@ -257,12 +257,12 @@ function makeFakeEnv() {
}),
};
const client = new GeminiClient(config as Config);
config.getGeminiClient = () => client;
const client = new LlmClient(config as Config);
config.getLlmClient = () => client;
// initialize() rebuilds the chat through startChat; stub only that side
// effect so the replay path (the unit under test) stays real.
vi.spyOn(client, 'startChat').mockImplementation(async function (
this: GeminiClient,
this: LlmClient,
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any).chat = fakeChat;
@@ -38,7 +38,7 @@ import {
import {
MAX_RETAINED_TOOL_RESULT_DISPLAY_CHARS,
type Config,
type GeminiClient,
type LlmClient,
type ShellExecutionResult,
type ShellOutputEvent,
} from '@qwen-code/qwen-code-core';
@@ -54,7 +54,7 @@ describe('useShellCommandProcessor', () => {
let onExecMock: Mock;
let onDebugMessageMock: Mock;
let mockConfig: Config;
let mockGeminiClient: GeminiClient;
let mockLlmClient: LlmClient;
let mockShellOutputCallback: (event: ShellOutputEvent) => void;
let resolveExecutionPromise: (result: ShellExecutionResult) => void;
@@ -77,7 +77,7 @@ describe('useShellCommandProcessor', () => {
terminalWidth: 80,
}),
} as Config;
mockGeminiClient = { addHistory: vi.fn() } as unknown as GeminiClient;
mockLlmClient = { addHistory: vi.fn() } as unknown as LlmClient;
vi.mocked(os.platform).mockReturnValue('linux');
vi.mocked(os.tmpdir).mockReturnValue('/tmp');
@@ -106,7 +106,7 @@ describe('useShellCommandProcessor', () => {
onExecMock,
onDebugMessageMock,
mockConfig,
mockGeminiClient,
mockLlmClient,
setShellInputFocusedMock,
),
);
@@ -187,7 +187,7 @@ describe('useShellCommandProcessor', () => {
],
}),
);
expect(mockGeminiClient.addHistory).toHaveBeenCalled();
expect(mockLlmClient.addHistory).toHaveBeenCalled();
expect(setShellInputFocusedMock).toHaveBeenCalledWith(false);
});
@@ -218,7 +218,7 @@ describe('useShellCommandProcessor', () => {
expect(finalDisplay).toContain('truncated from');
const modelHistoryText = (
vi.mocked(mockGeminiClient.addHistory).mock.calls[0]![0].parts![0]! as {
vi.mocked(mockLlmClient.addHistory).mock.calls[0]![0].parts![0]! as {
text: string;
}
).text;
@@ -247,7 +247,7 @@ describe('useShellCommandProcessor', () => {
await act(async () => await execPromise);
const modelHistoryText = (
vi.mocked(mockGeminiClient.addHistory).mock.calls[0]![0].parts![0]! as {
vi.mocked(mockLlmClient.addHistory).mock.calls[0]![0].parts![0]! as {
text: string;
}
).text;
@@ -13,7 +13,7 @@ import { useCallback, useState } from 'react';
import type {
AnsiOutput,
Config,
GeminiClient,
LlmClient,
ShellExecutionResult,
} from '@qwen-code/qwen-code-core';
import {
@@ -41,8 +41,8 @@ function copyString(value: string): string {
return value.split('').join('');
}
function addShellCommandToGeminiHistory(
geminiClient: GeminiClient,
function addShellCommandToLlmHistory(
llmClient: LlmClient,
rawQuery: string,
resultText: string,
) {
@@ -52,7 +52,7 @@ function addShellCommandToGeminiHistory(
'\n... (truncated)'
: resultText;
geminiClient.addHistory({
llmClient.addHistory({
role: 'user',
parts: [
{
@@ -82,7 +82,7 @@ export const useShellCommandProcessor = (
onExec: (command: Promise<void>) => void,
onDebugMessage: (message: string) => void,
config: Config,
geminiClient: GeminiClient,
llmClient: LlmClient,
setShellInputFocused: (value: boolean) => void,
terminalWidth?: number,
terminalHeight?: number,
@@ -322,11 +322,7 @@ export const useShellCommandProcessor = (
);
// Keep the existing LLM history behavior unchanged.
addShellCommandToGeminiHistory(
geminiClient,
rawQuery,
finalOutput,
);
addShellCommandToLlmHistory(llmClient, rawQuery, finalOutput);
})
.catch((err) => {
setPendingHistoryItem(null);
@@ -384,7 +380,7 @@ export const useShellCommandProcessor = (
addItemToHistory,
setPendingHistoryItem,
onExec,
geminiClient,
llmClient,
setShellInputFocused,
terminalHeight,
terminalWidth,
@@ -27,7 +27,7 @@ import { McpPromptLoader } from '../../services/McpPromptLoader.js';
import { ExtensionRefreshState } from '../../config/extension-refresh-state.js';
import { refreshExtensionContentRuntime } from '../../config/extension-runtime-reload.js';
import {
type GeminiClient,
type LlmClient,
SlashCommandStatus,
ToolConfirmationOutcome,
makeFakeConfig,
@@ -1406,8 +1406,8 @@ describe('useSlashCommandProcessor', () => {
it('should handle "load_history" action', async () => {
const mockClient = {
setHistory: vi.fn(),
} as unknown as GeminiClient;
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue(mockClient);
} as unknown as LlmClient;
vi.spyOn(mockConfig, 'getLlmClient').mockReturnValue(mockClient);
const command = createTestCommand({
name: 'load',
@@ -1434,8 +1434,8 @@ describe('useSlashCommandProcessor', () => {
it('should preserve thoughts when handling "load_history" action', async () => {
const mockClient = {
setHistory: vi.fn(),
} as unknown as GeminiClient;
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue(mockClient);
} as unknown as LlmClient;
vi.spyOn(mockConfig, 'getLlmClient').mockReturnValue(mockClient);
const historyWithThoughts = [
{
@@ -1321,7 +1321,7 @@ export const useSlashCommandProcessor = (
}
}
case 'load_history': {
config?.getGeminiClient()?.setHistory(result.clientHistory);
config?.getLlmClient()?.setHistory(result.clientHistory);
fullCommandContext.ui.clear();
result.history.forEach((item, index) => {
fullCommandContext.ui.addItem(item, index);
Loaded 100 of 238 files, more files were not shown because too many files have changed in this diff. Show more