mirror of
https://github.com/cline/cline.git
synced 2026-09-14 11:29:25 +08:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49c0d1b6a3 | ||
|
|
92dc5dfed3 | ||
|
|
ce85e49c7b | ||
|
|
dfecadbcbd | ||
|
|
519a22c5d5 | ||
|
|
fbdfa77bb9 | ||
|
|
5ad8d33977 | ||
|
|
5226b107ba | ||
|
|
26f015fbd1 | ||
|
|
bdce31deea | ||
|
|
406674d27f | ||
|
|
24303ab0cb | ||
|
|
f48ba92357 | ||
|
|
3693d2f867 | ||
|
|
86aca36d03 | ||
|
|
d5db7eb853 | ||
|
|
11d5ebe8bc | ||
|
|
6f7cc4907f | ||
|
|
27e3541569 | ||
|
|
ae9c5b4d9d | ||
|
|
f86ca6b36b | ||
|
|
0e0b11032e | ||
|
|
a93d850aee | ||
|
|
fe25258b0d | ||
|
|
53d1567731 | ||
|
|
9a93008463 | ||
|
|
678b0ae951 | ||
|
|
8b6f2cf0b7 | ||
|
|
25ef0939cc |
@@ -1,6 +1,9 @@
|
||||
name: ext-vscode-publish-nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Every day at 4:00 AM PST (12:00 UTC)
|
||||
- cron: "0 12 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
run-name: "Publish Nightly from ${{ github.ref_name }} @ ${{ github.sha }}"
|
||||
|
||||
@@ -149,7 +149,7 @@ Toggle between Plan mode and Act mode. In Plan mode, Cline explores your codebas
|
||||
|
||||
## Rules and Skills
|
||||
|
||||
Define project-specific rules in `.clinerules` files that guide how Cline works in your codebase: coding standards, architecture conventions, deployment procedures, testing requirements. Rules are picked up automatically by the CLI, VS Code extension, and JetBrains plugin. Use skills to let the model load specific rules when needed.
|
||||
Define project-specific rules in `.clinerules` files that guide how Cline works in your codebase: coding standards, architecture conventions, deployment procedures, testing requirements. Rules are picked up automatically by the CLI, VS Code extension, and JetBrains plugin. Use skills to let the model load specific rules when needed.
|
||||
|
||||
## Works With Every Model
|
||||
|
||||
@@ -158,10 +158,10 @@ Cline is not locked to a single AI provider. Use whichever model fits your workf
|
||||
| Provider | Models |
|
||||
|----------|--------|
|
||||
| Anthropic | Claude Opus, Sonnet, Haiku |
|
||||
| OpenAI | GPT series model |
|
||||
| Google | Gemini series model |
|
||||
| OpenAI | GPT series models |
|
||||
| Google | Gemini series models |
|
||||
| OpenRouter | 200+ models from any provider |
|
||||
| Vercel AI Gateway | Models through Vercel AI Gateway |
|
||||
| Vercel AI Gateway | Route to many providers through one gateway |
|
||||
| AWS Bedrock | Claude, Llama, and more |
|
||||
| Azure / GCP Vertex | All hosted models |
|
||||
| Cerebras / Groq | Fast inference models |
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Cline CLI Changelog
|
||||
|
||||
## 3.0.38
|
||||
|
||||
- New plan/act accent palette: act mode is now blue (`#79b8ff`) and plan mode amber, replacing the old cyan/yellow — applied across dialogs, the model selector, config, onboarding, markdown, and syntax highlighting, with light-theme variants tuned for contrast
|
||||
- Restyled chat input: a minimal frame with full-width horizontal rules and a bold accent prompt glyph instead of the tinted background, plus slimmer user-message bubbles
|
||||
- Assistant markdown accents are now tinted by the mode (plan/act) they were produced in
|
||||
- Polished the status bar usage display and ClinePass model name
|
||||
- Harmonized the success/diff green and dark syntax-highlighting colors with the new brand palette
|
||||
- The thinking-level picker now defaults its cursor to Medium instead of Off
|
||||
- `read_files` now tolerates malformed input from weaker models: line-range entries (`start_line`/`end_line`) sent as separate array items are coalesced back onto the preceding file path instead of being rejected (from SDK v0.0.58)
|
||||
- Models in the live catalog that don't report a context window now default to a 128K input-token limit, so under-specified models get a usable context budget (from SDK v0.0.57)
|
||||
|
||||
## 3.0.37
|
||||
|
||||
- Weaker models (e.g. DeepSeek) that emit malformed tool calls — wrong argument types or truncated JSON — are now handled gracefully and run instead of erroring out
|
||||
- Plan/act mode switches are now visible to the model, so it knows when you change modes mid-session
|
||||
- Fixed plan/act mode notices being dropped from prompts sent to the model
|
||||
- Fixed a race where switching modes in an empty session could trigger an unexpected restart
|
||||
|
||||
## 3.0.36
|
||||
|
||||
- Fixed plan mode's `switch_to_act_mode` tool not taking effect until the end of the turn: the model would keep running with plan-mode tools (no file editor) and fall back to editing files through shell commands. Switching to act mode now ends the plan-mode run and automatically continues with the approved plan using the full act-mode toolset. A Tab mode toggle racing a completing turn can no longer auto-start plan execution you didn't approve.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/cli",
|
||||
"displayName": "cline",
|
||||
"version": "3.0.36",
|
||||
"version": "3.0.38",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -126,7 +126,8 @@ describe("compactInteractiveMessages", () => {
|
||||
|
||||
expect(compact).toHaveBeenCalledTimes(1);
|
||||
expect(result.compacted).toBe(true);
|
||||
expect(result.messages).toEqual([messages[0]]);
|
||||
expect(result.canonicalMessages).toEqual(messages);
|
||||
expect(result.compactionState?.messages).toEqual([messages[0]]);
|
||||
});
|
||||
|
||||
it("falls back to legacy contextWindow for manual compaction", async () => {
|
||||
@@ -157,7 +158,8 @@ describe("compactInteractiveMessages", () => {
|
||||
|
||||
expect(compact).toHaveBeenCalledTimes(1);
|
||||
expect(result.compacted).toBe(true);
|
||||
expect(result.messages).toEqual([messages[0]]);
|
||||
expect(result.canonicalMessages).toEqual(messages);
|
||||
expect(result.compactionState?.messages).toEqual([messages[0]]);
|
||||
});
|
||||
|
||||
it("uses a useful target budget for manual compaction", async () => {
|
||||
@@ -174,7 +176,8 @@ describe("compactInteractiveMessages", () => {
|
||||
messages,
|
||||
});
|
||||
|
||||
const compactedTextLength = result.messages.reduce(
|
||||
const compactedMessages = result.compactionState?.messages ?? [];
|
||||
const compactedTextLength = compactedMessages.reduce(
|
||||
(total, message) =>
|
||||
total +
|
||||
(typeof message.content === "string" ? message.content.length : 0),
|
||||
@@ -182,8 +185,9 @@ describe("compactInteractiveMessages", () => {
|
||||
);
|
||||
|
||||
expect(result.compacted).toBe(true);
|
||||
expect(result.messages.length).toBeGreaterThan(1);
|
||||
expect(result.messages.length).toBeLessThan(messages.length);
|
||||
expect(result.canonicalMessages).toEqual(messages);
|
||||
expect(compactedMessages.length).toBeGreaterThan(1);
|
||||
expect(compactedMessages.length).toBeLessThan(messages.length);
|
||||
expect(compactedTextLength).toBeGreaterThan(1_000);
|
||||
});
|
||||
|
||||
@@ -214,8 +218,9 @@ describe("compactInteractiveMessages", () => {
|
||||
});
|
||||
|
||||
expect(result.compacted).toBe(true);
|
||||
expect(result.messages).toHaveLength(messages.length);
|
||||
expect(result.messages[0]?.content).toBe(
|
||||
expect(result.canonicalMessages).toEqual(messages);
|
||||
expect(result.compactionState?.messages).toHaveLength(messages.length);
|
||||
expect(result.compactionState?.messages[0]?.content).toBe(
|
||||
"same count but content should be trimmed",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
createContextCompactionPrepareTurn,
|
||||
createSessionCompactionState,
|
||||
type ProviderConfig,
|
||||
type ProviderSettings,
|
||||
type ProviderSettingsManager,
|
||||
type ReasoningSettings,
|
||||
type SessionCompactionState,
|
||||
toProviderConfig,
|
||||
} from "@cline/core";
|
||||
import type { Message } from "@cline/shared";
|
||||
@@ -52,7 +54,12 @@ export async function compactInteractiveMessages(input: {
|
||||
providerSettingsManager: ProviderSettingsManager;
|
||||
sessionId: string;
|
||||
messages: Message[];
|
||||
}): Promise<{ compacted: boolean; messages: Message[] }> {
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<{
|
||||
compacted: boolean;
|
||||
canonicalMessages: Message[];
|
||||
compactionState?: SessionCompactionState;
|
||||
}> {
|
||||
const modelInfo = input.config.knownModels?.[input.config.modelId];
|
||||
const maxInputTokens =
|
||||
input.config.compaction?.maxInputTokens ??
|
||||
@@ -81,8 +88,11 @@ export async function compactInteractiveMessages(input: {
|
||||
{ mode: "manual" },
|
||||
);
|
||||
if (!compact) {
|
||||
return { compacted: false, messages: input.messages };
|
||||
return { compacted: false, canonicalMessages: input.messages };
|
||||
}
|
||||
// Manual compaction intentionally summarizes the full canonical transcript
|
||||
// instead of reusing a prior sidecar summary, which avoids summary-of-summary
|
||||
// drift across repeated `/compact` calls.
|
||||
const result = await compact({
|
||||
agentId: "cli",
|
||||
conversationId: input.sessionId,
|
||||
@@ -90,7 +100,7 @@ export async function compactInteractiveMessages(input: {
|
||||
iteration: 0,
|
||||
messages: input.messages,
|
||||
apiMessages: input.messages,
|
||||
abortSignal: new AbortController().signal,
|
||||
abortSignal: input.abortSignal ?? new AbortController().signal,
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
model: {
|
||||
@@ -103,8 +113,17 @@ export async function compactInteractiveMessages(input: {
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
return { compacted: false, messages: input.messages };
|
||||
if (!result?.messages) {
|
||||
return { compacted: false, canonicalMessages: input.messages };
|
||||
}
|
||||
return { compacted: true, messages: result.messages };
|
||||
return {
|
||||
compacted: true,
|
||||
canonicalMessages: input.messages,
|
||||
compactionState: createSessionCompactionState({
|
||||
sourceMessages: input.messages,
|
||||
compactedMessages: result.messages,
|
||||
conversationId: input.sessionId,
|
||||
systemPrompt: result.systemPrompt,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,81 +1,89 @@
|
||||
import type {
|
||||
AgentEvent,
|
||||
ProviderSettingsManager,
|
||||
TeamEvent,
|
||||
ToolApprovalRequest,
|
||||
ToolApprovalResult,
|
||||
import {
|
||||
createSessionCompactionState,
|
||||
type ProviderSettingsManager,
|
||||
type SessionManifest,
|
||||
SessionNotFoundError,
|
||||
SessionSource,
|
||||
type ToolApprovalRequest,
|
||||
type ToolApprovalResult,
|
||||
} from "@cline/core";
|
||||
import { SessionNotFoundError } from "@cline/core";
|
||||
import type { AgentTool, Message } from "@cline/shared";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChatCommandState } from "../../utils/chat-commands";
|
||||
import type { Config } from "../../utils/types";
|
||||
|
||||
const {
|
||||
mockCreateCliCore,
|
||||
mockCreateRuntimeHooks,
|
||||
mockLoadInteractiveResumeMessages,
|
||||
mockSetActiveCliSession,
|
||||
} = vi.hoisted(() => ({
|
||||
mockCreateCliCore: vi.fn(),
|
||||
mockCreateRuntimeHooks: vi.fn(),
|
||||
mockLoadInteractiveResumeMessages: vi.fn(),
|
||||
mockSetActiveCliSession: vi.fn(),
|
||||
}));
|
||||
const createCliCoreMock = vi.hoisted(() => vi.fn());
|
||||
const compactInteractiveMessagesMock = vi.hoisted(() => vi.fn());
|
||||
const createRuntimeHooksMock = vi.hoisted(() => vi.fn());
|
||||
const setActiveCliSessionMock = vi.hoisted(() => vi.fn());
|
||||
const loadInteractiveResumeMessagesMock = vi.hoisted(() => vi.fn());
|
||||
const subscribeToAgentEventsMock = vi.hoisted(() => vi.fn());
|
||||
const subscribeToPendingPromptEventsMock = vi.hoisted(() => vi.fn());
|
||||
const markAbortInProgressMock = vi.hoisted(() => vi.fn());
|
||||
const submitAndExitInTerminalMock = vi.hoisted(() => vi.fn());
|
||||
const createInteractiveExitSummaryMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../session/session", () => ({
|
||||
createCliCore: mockCreateCliCore,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/hooks", () => ({
|
||||
createRuntimeHooks: mockCreateRuntimeHooks,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/output", () => ({
|
||||
setActiveCliSession: mockSetActiveCliSession,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/resume", () => ({
|
||||
loadInteractiveResumeMessages: mockLoadInteractiveResumeMessages,
|
||||
createCliCore: createCliCoreMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/approval", () => ({
|
||||
submitAndExitInTerminal: vi.fn(),
|
||||
submitAndExitInTerminal: submitAndExitInTerminalMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/hooks", () => ({
|
||||
createRuntimeHooks: createRuntimeHooksMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/output", () => ({
|
||||
setActiveCliSession: setActiveCliSessionMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/resume", () => ({
|
||||
loadInteractiveResumeMessages: loadInteractiveResumeMessagesMock,
|
||||
}));
|
||||
|
||||
vi.mock("../active-runtime", () => ({
|
||||
markAbortInProgress: vi.fn(),
|
||||
markAbortInProgress: markAbortInProgressMock,
|
||||
}));
|
||||
|
||||
vi.mock("../session-events", () => ({
|
||||
subscribeToAgentEvents: vi.fn(() => vi.fn()),
|
||||
subscribeToPendingPromptEvents: vi.fn(() => vi.fn()),
|
||||
subscribeToAgentEvents: subscribeToAgentEventsMock,
|
||||
subscribeToPendingPromptEvents: subscribeToPendingPromptEventsMock,
|
||||
}));
|
||||
|
||||
import { createInteractiveSessionRuntime } from "./session-runtime";
|
||||
vi.mock("./compaction", () => ({
|
||||
compactInteractiveMessages: compactInteractiveMessagesMock,
|
||||
}));
|
||||
|
||||
function makeConfig(): Config {
|
||||
vi.mock("./exit-summary", () => ({
|
||||
createInteractiveExitSummary: createInteractiveExitSummaryMock,
|
||||
}));
|
||||
|
||||
function createConfig(): Config {
|
||||
return {
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-test",
|
||||
apiKey: "",
|
||||
providerId: "cline",
|
||||
modelId: "openai/gpt-5.3-codex",
|
||||
verbose: false,
|
||||
sandbox: false,
|
||||
thinking: false,
|
||||
outputMode: "text",
|
||||
cwd: "/tmp/project",
|
||||
workspaceRoot: "/tmp/project",
|
||||
systemPrompt: "system",
|
||||
mode: "act",
|
||||
systemPrompt: "",
|
||||
enableTools: true,
|
||||
enableSpawnAgent: true,
|
||||
enableAgentTeams: false,
|
||||
defaultToolAutoApprove: false,
|
||||
toolPolicies: {},
|
||||
cwd: "/tmp/work",
|
||||
workspaceRoot: "/tmp/work",
|
||||
enableAgentTeams: true,
|
||||
verbose: false,
|
||||
thinking: false,
|
||||
outputMode: "text",
|
||||
sandbox: false,
|
||||
defaultToolAutoApprove: true,
|
||||
toolPolicies: {
|
||||
"*": { autoApprove: true },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeChatCommandState(config: Config): ChatCommandState {
|
||||
function createChatCommandState(config = createConfig()): ChatCommandState {
|
||||
return {
|
||||
enableTools: config.enableTools,
|
||||
autoApproveTools: config.defaultToolAutoApprove,
|
||||
@@ -84,6 +92,35 @@ function makeChatCommandState(config: Config): ChatCommandState {
|
||||
};
|
||||
}
|
||||
|
||||
function createProviderSettingsManager(): ProviderSettingsManager {
|
||||
return {
|
||||
getProviderSettings: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as ProviderSettingsManager;
|
||||
}
|
||||
|
||||
function createManifest(sessionId: string): SessionManifest {
|
||||
return {
|
||||
version: 1,
|
||||
session_id: sessionId,
|
||||
source: SessionSource.CLI,
|
||||
pid: 1,
|
||||
started_at: "2026-01-01T00:00:00.000Z",
|
||||
status: "running",
|
||||
interactive: true,
|
||||
provider: "anthropic",
|
||||
model: "claude-test",
|
||||
cwd: "/tmp/project",
|
||||
workspace_root: "/tmp/project",
|
||||
enable_tools: true,
|
||||
enable_spawn: true,
|
||||
enable_teams: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function importRuntime() {
|
||||
return await import("./session-runtime");
|
||||
}
|
||||
|
||||
function makeSwitchToActModeTool(): AgentTool {
|
||||
return {
|
||||
name: "switch_to_act_mode",
|
||||
@@ -100,9 +137,9 @@ function makeManager() {
|
||||
const sessionId = `session-${startCount}`;
|
||||
return {
|
||||
sessionId,
|
||||
manifest: {
|
||||
session_id: sessionId,
|
||||
},
|
||||
manifest: createManifest(sessionId),
|
||||
manifestPath: `/tmp/${sessionId}.json`,
|
||||
messagesPath: `/tmp/${sessionId}.messages.json`,
|
||||
};
|
||||
});
|
||||
return {
|
||||
@@ -114,6 +151,8 @@ function makeManager() {
|
||||
dispose: vi.fn(),
|
||||
get: vi.fn(),
|
||||
readMessages: vi.fn(async (): Promise<Message[]> => []),
|
||||
readSessionCompactionState: vi.fn().mockResolvedValue(undefined),
|
||||
updateSessionCompactionState: vi.fn(),
|
||||
readTranscript: vi.fn(),
|
||||
ingestHookEvent: vi.fn(),
|
||||
subscribe: vi.fn(),
|
||||
@@ -133,7 +172,7 @@ function makeTurnResult() {
|
||||
toolCalls: [],
|
||||
iterations: 1,
|
||||
finishReason: "completed" as const,
|
||||
model: { id: "openai/gpt-5.3-codex", provider: "cline" },
|
||||
model: { id: "claude-test", provider: "anthropic" },
|
||||
startedAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
endedAt: new Date("2026-01-01T00:00:00.100Z"),
|
||||
durationMs: 100,
|
||||
@@ -150,20 +189,22 @@ function deferred<T>() {
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
function makeRuntime(
|
||||
async function makeRuntime(
|
||||
manager: ReturnType<typeof makeManager>,
|
||||
options: {
|
||||
config?: Config;
|
||||
resumeSessionId?: string;
|
||||
resolveToolPolicy?: (toolName: string) => Config["toolPolicies"][string];
|
||||
} = {},
|
||||
) {
|
||||
mockCreateCliCore.mockResolvedValue(manager);
|
||||
const config = makeConfig();
|
||||
createCliCoreMock.mockResolvedValue(manager);
|
||||
const config = options.config ?? createConfig();
|
||||
const { createInteractiveSessionRuntime } = await importRuntime();
|
||||
return createInteractiveSessionRuntime({
|
||||
config,
|
||||
providerSettingsManager: {} as ProviderSettingsManager,
|
||||
providerSettingsManager: createProviderSettingsManager(),
|
||||
resumeSessionId: options.resumeSessionId,
|
||||
chatCommandState: makeChatCommandState(config),
|
||||
chatCommandState: createChatCommandState(config),
|
||||
requestToolApproval: async (
|
||||
_request: ToolApprovalRequest,
|
||||
): Promise<ToolApprovalResult> => ({ approved: true }),
|
||||
@@ -172,26 +213,325 @@ function makeRuntime(
|
||||
askQuestionRef: { current: null },
|
||||
resolveMistakeLimitDecision: undefined,
|
||||
switchToActModeTool: makeSwitchToActModeTool(),
|
||||
onAgentEvent: (_event: AgentEvent) => {},
|
||||
onTeamEvent: (_event: TeamEvent) => {},
|
||||
onPendingPrompts: () => {},
|
||||
onPendingPromptSubmitted: () => {},
|
||||
onAgentEvent: vi.fn(),
|
||||
onTeamEvent: vi.fn(),
|
||||
onPendingPrompts: vi.fn(),
|
||||
onPendingPromptSubmitted: vi.fn(),
|
||||
});
|
||||
}
|
||||
|
||||
describe("createInteractiveSessionRuntime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockCreateRuntimeHooks.mockReturnValue({
|
||||
createCliCoreMock.mockReset();
|
||||
compactInteractiveMessagesMock.mockReset();
|
||||
createRuntimeHooksMock.mockReset();
|
||||
setActiveCliSessionMock.mockReset();
|
||||
loadInteractiveResumeMessagesMock.mockReset();
|
||||
subscribeToAgentEventsMock.mockReset();
|
||||
subscribeToPendingPromptEventsMock.mockReset();
|
||||
markAbortInProgressMock.mockReset();
|
||||
submitAndExitInTerminalMock.mockReset();
|
||||
createInteractiveExitSummaryMock.mockReset();
|
||||
createRuntimeHooksMock.mockReturnValue({
|
||||
hooks: undefined,
|
||||
shutdown: vi.fn(async () => {}),
|
||||
shutdown: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
mockLoadInteractiveResumeMessages.mockResolvedValue([]);
|
||||
loadInteractiveResumeMessagesMock.mockResolvedValue([]);
|
||||
subscribeToAgentEventsMock.mockReturnValue(() => {});
|
||||
subscribeToPendingPromptEventsMock.mockReturnValue(() => {});
|
||||
});
|
||||
|
||||
it("manual compact updates the active session sidecar without restarting", async () => {
|
||||
const sessionId = "sess-active";
|
||||
const messages = [
|
||||
{ id: "u1", role: "user" as const, content: "hello" },
|
||||
{ id: "a1", role: "assistant" as const, content: "world" },
|
||||
];
|
||||
const compactionState = createSessionCompactionState({
|
||||
sourceMessages: messages,
|
||||
compactedMessages: [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
],
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const manager = {
|
||||
start: vi.fn().mockResolvedValue({
|
||||
sessionId,
|
||||
manifest: createManifest(sessionId),
|
||||
manifestPath: "/tmp/session.json",
|
||||
messagesPath: "/tmp/session.messages.json",
|
||||
}),
|
||||
readMessages: vi.fn().mockResolvedValue(messages),
|
||||
updateSessionCompactionState: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ updated: true }),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn().mockResolvedValue(undefined),
|
||||
ingestHookEvent: vi.fn().mockResolvedValue(undefined),
|
||||
get: vi.fn(),
|
||||
list: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
send: vi.fn(),
|
||||
getAccumulatedUsage: vi.fn(),
|
||||
};
|
||||
createCliCoreMock.mockResolvedValue(manager);
|
||||
compactInteractiveMessagesMock.mockResolvedValue({
|
||||
compacted: true,
|
||||
canonicalMessages: messages,
|
||||
compactionState,
|
||||
});
|
||||
const { createInteractiveSessionRuntime } = await importRuntime();
|
||||
const runtime = createInteractiveSessionRuntime({
|
||||
config: createConfig(),
|
||||
providerSettingsManager: createProviderSettingsManager(),
|
||||
chatCommandState: createChatCommandState(),
|
||||
requestToolApproval: vi.fn(),
|
||||
resolveToolPolicy: () => ({ autoApprove: true }),
|
||||
askQuestionRef: { current: null },
|
||||
resolveMistakeLimitDecision: undefined,
|
||||
switchToActModeTool: {} as never,
|
||||
onAgentEvent: vi.fn(),
|
||||
onTeamEvent: vi.fn(),
|
||||
onPendingPrompts: vi.fn(),
|
||||
onPendingPromptSubmitted: vi.fn(),
|
||||
});
|
||||
|
||||
await runtime.ensureReady();
|
||||
const result = await runtime.compactCurrentSession();
|
||||
|
||||
expect(result).toEqual({
|
||||
messagesBefore: messages.length,
|
||||
messagesAfter: messages.length,
|
||||
workingContextMessagesAfter: compactionState.messages.length,
|
||||
compacted: true,
|
||||
});
|
||||
expect(manager.start).toHaveBeenCalledTimes(1);
|
||||
expect(manager.stop).not.toHaveBeenCalled();
|
||||
expect(manager.readMessages).toHaveBeenCalledWith(sessionId);
|
||||
expect(compactInteractiveMessagesMock).toHaveBeenCalledWith({
|
||||
config: expect.objectContaining({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-test",
|
||||
}),
|
||||
providerSettingsManager: expect.objectContaining({
|
||||
getProviderSettings: expect.any(Function),
|
||||
}),
|
||||
sessionId,
|
||||
messages,
|
||||
abortSignal: expect.any(AbortSignal),
|
||||
});
|
||||
expect(manager.updateSessionCompactionState).toHaveBeenCalledWith(
|
||||
sessionId,
|
||||
compactionState,
|
||||
);
|
||||
expect(runtime.getActiveSessionId()).toBe(sessionId);
|
||||
});
|
||||
|
||||
it("rejects manual compact while the active session is running", async () => {
|
||||
const sessionId = "sess-running";
|
||||
const messages = [{ role: "user" as const, content: "hello" }];
|
||||
const manager = {
|
||||
start: vi.fn().mockResolvedValue({
|
||||
sessionId,
|
||||
manifest: createManifest(sessionId),
|
||||
manifestPath: "/tmp/session.json",
|
||||
messagesPath: "/tmp/session.messages.json",
|
||||
}),
|
||||
readMessages: vi.fn().mockResolvedValue(messages),
|
||||
updateSessionCompactionState: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ updated: true }),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn().mockResolvedValue(undefined),
|
||||
ingestHookEvent: vi.fn().mockResolvedValue(undefined),
|
||||
get: vi.fn().mockResolvedValue({
|
||||
sessionId,
|
||||
status: "running",
|
||||
}),
|
||||
list: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
send: vi.fn(),
|
||||
getAccumulatedUsage: vi.fn(),
|
||||
};
|
||||
createCliCoreMock.mockResolvedValue(manager);
|
||||
const { createInteractiveSessionRuntime } = await importRuntime();
|
||||
const runtime = createInteractiveSessionRuntime({
|
||||
config: createConfig(),
|
||||
providerSettingsManager: createProviderSettingsManager(),
|
||||
chatCommandState: createChatCommandState(),
|
||||
requestToolApproval: vi.fn(),
|
||||
resolveToolPolicy: () => ({ autoApprove: true }),
|
||||
askQuestionRef: { current: null },
|
||||
resolveMistakeLimitDecision: undefined,
|
||||
switchToActModeTool: {} as never,
|
||||
onAgentEvent: vi.fn(),
|
||||
onTeamEvent: vi.fn(),
|
||||
onPendingPrompts: vi.fn(),
|
||||
onPendingPromptSubmitted: vi.fn(),
|
||||
});
|
||||
|
||||
await runtime.ensureReady();
|
||||
|
||||
await expect(runtime.compactCurrentSession()).rejects.toThrow(
|
||||
"Cannot compact while the current turn is running",
|
||||
);
|
||||
expect(manager.readMessages).toHaveBeenCalledWith(sessionId);
|
||||
expect(compactInteractiveMessagesMock).not.toHaveBeenCalled();
|
||||
expect(manager.updateSessionCompactionState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects manual compact when compaction is disabled", async () => {
|
||||
const manager = makeManager();
|
||||
const config = createConfig();
|
||||
config.compaction = { enabled: false };
|
||||
const runtime = await makeRuntime(manager, { config });
|
||||
|
||||
await runtime.ensureReady();
|
||||
|
||||
await expect(runtime.compactCurrentSession()).rejects.toThrow(
|
||||
"compaction is off",
|
||||
);
|
||||
expect(compactInteractiveMessagesMock).not.toHaveBeenCalled();
|
||||
expect(manager.updateSessionCompactionState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("carries compacted working context across mode-switch restarts", async () => {
|
||||
const firstSessionId = "sess-mode-before";
|
||||
const secondSessionId = "sess-mode-after";
|
||||
const prefixMessage = {
|
||||
id: "u1",
|
||||
role: "user" as const,
|
||||
content: "large original",
|
||||
};
|
||||
const tailMessage = {
|
||||
id: "u2",
|
||||
role: "user" as const,
|
||||
content: "new canonical tail",
|
||||
};
|
||||
const messages = [prefixMessage, tailMessage];
|
||||
const summaryMessage = {
|
||||
id: "summary",
|
||||
role: "user" as const,
|
||||
content: "summary",
|
||||
};
|
||||
const compactionState = createSessionCompactionState({
|
||||
sourceMessages: [prefixMessage],
|
||||
compactedMessages: [summaryMessage],
|
||||
conversationId: firstSessionId,
|
||||
systemPrompt: "compacted system",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const manager = {
|
||||
start: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
sessionId: firstSessionId,
|
||||
manifest: createManifest(firstSessionId),
|
||||
manifestPath: "/tmp/session-before.json",
|
||||
messagesPath: "/tmp/session-before.messages.json",
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
sessionId: secondSessionId,
|
||||
manifest: createManifest(secondSessionId),
|
||||
manifestPath: "/tmp/session-after.json",
|
||||
messagesPath: "/tmp/session-after.messages.json",
|
||||
}),
|
||||
readMessages: vi.fn().mockResolvedValue(messages),
|
||||
readSessionCompactionState: vi.fn().mockResolvedValue(compactionState),
|
||||
updateSessionCompactionState: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ updated: true }),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn().mockResolvedValue(undefined),
|
||||
ingestHookEvent: vi.fn().mockResolvedValue(undefined),
|
||||
get: vi.fn(),
|
||||
list: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
send: vi.fn(),
|
||||
getAccumulatedUsage: vi.fn(),
|
||||
};
|
||||
createCliCoreMock.mockResolvedValue(manager);
|
||||
const { createInteractiveSessionRuntime } = await importRuntime();
|
||||
const runtime = createInteractiveSessionRuntime({
|
||||
config: createConfig(),
|
||||
providerSettingsManager: createProviderSettingsManager(),
|
||||
chatCommandState: createChatCommandState(),
|
||||
requestToolApproval: vi.fn(),
|
||||
resolveToolPolicy: () => ({ autoApprove: true }),
|
||||
askQuestionRef: { current: null },
|
||||
resolveMistakeLimitDecision: undefined,
|
||||
switchToActModeTool: {} as never,
|
||||
onAgentEvent: vi.fn(),
|
||||
onTeamEvent: vi.fn(),
|
||||
onPendingPrompts: vi.fn(),
|
||||
onPendingPromptSubmitted: vi.fn(),
|
||||
});
|
||||
|
||||
await runtime.ensureReady();
|
||||
await runtime.applyMode("plan");
|
||||
|
||||
expect(manager.readMessages).toHaveBeenCalledWith(firstSessionId);
|
||||
expect(manager.readSessionCompactionState).toHaveBeenCalledWith(
|
||||
firstSessionId,
|
||||
);
|
||||
expect(manager.stop).toHaveBeenCalledWith(firstSessionId);
|
||||
const restartInput = manager.start.mock.calls[1]?.[0];
|
||||
expect(restartInput).toMatchObject({
|
||||
initialMessages: messages,
|
||||
initialCompactionState: expect.objectContaining({
|
||||
source_message_count: messages.length,
|
||||
messages: [summaryMessage, tailMessage],
|
||||
system_prompt: "compacted system",
|
||||
}),
|
||||
});
|
||||
expect(restartInput.initialCompactionState).not.toHaveProperty(
|
||||
"conversation_id",
|
||||
);
|
||||
expect(manager.updateSessionCompactionState).not.toHaveBeenCalled();
|
||||
expect(runtime.getActiveSessionId()).toBe(secondSessionId);
|
||||
});
|
||||
|
||||
it("defers creating the replacement session after a new-session reset", async () => {
|
||||
const manager = makeManager();
|
||||
const runtime = makeRuntime(manager);
|
||||
let startCount = 0;
|
||||
const manager = {
|
||||
start: vi.fn().mockImplementation(async () => {
|
||||
startCount += 1;
|
||||
const sessionId = `session-${startCount}`;
|
||||
return {
|
||||
sessionId,
|
||||
manifest: createManifest(sessionId),
|
||||
manifestPath: `/tmp/${sessionId}.json`,
|
||||
messagesPath: `/tmp/${sessionId}.messages.json`,
|
||||
};
|
||||
}),
|
||||
readMessages: vi.fn().mockResolvedValue([]),
|
||||
readSessionCompactionState: vi.fn().mockResolvedValue(undefined),
|
||||
updateSessionCompactionState: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn().mockResolvedValue(undefined),
|
||||
ingestHookEvent: vi.fn().mockResolvedValue(undefined),
|
||||
get: vi.fn(),
|
||||
list: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
send: vi.fn(),
|
||||
getAccumulatedUsage: vi.fn(),
|
||||
};
|
||||
createCliCoreMock.mockResolvedValue(manager);
|
||||
const { createInteractiveSessionRuntime } = await importRuntime();
|
||||
const runtime = createInteractiveSessionRuntime({
|
||||
config: createConfig(),
|
||||
providerSettingsManager: createProviderSettingsManager(),
|
||||
chatCommandState: createChatCommandState(),
|
||||
requestToolApproval: vi.fn(),
|
||||
resolveToolPolicy: () => ({ autoApprove: true }),
|
||||
askQuestionRef: { current: null },
|
||||
resolveMistakeLimitDecision: undefined,
|
||||
switchToActModeTool: {} as never,
|
||||
onAgentEvent: vi.fn(),
|
||||
onTeamEvent: vi.fn(),
|
||||
onPendingPrompts: vi.fn(),
|
||||
onPendingPromptSubmitted: vi.fn(),
|
||||
});
|
||||
|
||||
await runtime.ensureReady();
|
||||
expect(manager.start).toHaveBeenCalledOnce();
|
||||
@@ -202,7 +542,7 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
expect(manager.stop).toHaveBeenCalledWith("session-1");
|
||||
expect(manager.start).toHaveBeenCalledOnce();
|
||||
expect(runtime.getActiveSessionId()).toBe("");
|
||||
expect(mockSetActiveCliSession).toHaveBeenLastCalledWith(undefined);
|
||||
expect(setActiveCliSessionMock).toHaveBeenLastCalledWith(undefined);
|
||||
|
||||
await runtime.ensureReady();
|
||||
|
||||
@@ -212,7 +552,7 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
|
||||
it("holds concurrent ensureReady during a restart instead of booting an empty session", async () => {
|
||||
const manager = makeManager();
|
||||
const runtime = makeRuntime(manager);
|
||||
const runtime = await makeRuntime(manager);
|
||||
|
||||
await runtime.ensureReady();
|
||||
expect(runtime.getActiveSessionId()).toBe("session-1");
|
||||
@@ -224,7 +564,9 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
await gate.promise;
|
||||
return {
|
||||
sessionId: "session-restarted",
|
||||
manifest: { session_id: "session-restarted" },
|
||||
manifest: createManifest("session-restarted"),
|
||||
manifestPath: "/tmp/session-restarted.json",
|
||||
messagesPath: "/tmp/session-restarted.messages.json",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -249,13 +591,13 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
const upstreamBeforeTool = vi.fn(async () => ({
|
||||
input: { text: "updated" },
|
||||
}));
|
||||
mockCreateRuntimeHooks.mockReturnValueOnce({
|
||||
createRuntimeHooksMock.mockReturnValueOnce({
|
||||
hooks: {
|
||||
beforeTool: upstreamBeforeTool,
|
||||
},
|
||||
shutdown: vi.fn(async () => {}),
|
||||
});
|
||||
const runtime = makeRuntime(manager, {
|
||||
const runtime = await makeRuntime(manager, {
|
||||
resolveToolPolicy: (toolName) => ({
|
||||
autoApprove: toolName === "echo",
|
||||
}),
|
||||
@@ -307,14 +649,51 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
});
|
||||
|
||||
it("starts fresh after resetting an initially resumed session", async () => {
|
||||
const manager = makeManager();
|
||||
const runtime = makeRuntime(manager, {
|
||||
let startCount = 0;
|
||||
const manager = {
|
||||
start: vi.fn().mockImplementation(async () => {
|
||||
startCount += 1;
|
||||
const sessionId = `session-${startCount}`;
|
||||
return {
|
||||
sessionId,
|
||||
manifest: createManifest(sessionId),
|
||||
manifestPath: `/tmp/${sessionId}.json`,
|
||||
messagesPath: `/tmp/${sessionId}.messages.json`,
|
||||
};
|
||||
}),
|
||||
readMessages: vi.fn().mockResolvedValue([]),
|
||||
readSessionCompactionState: vi.fn().mockResolvedValue(undefined),
|
||||
updateSessionCompactionState: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn().mockResolvedValue(undefined),
|
||||
ingestHookEvent: vi.fn().mockResolvedValue(undefined),
|
||||
get: vi.fn(),
|
||||
list: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
send: vi.fn(),
|
||||
getAccumulatedUsage: vi.fn(),
|
||||
};
|
||||
createCliCoreMock.mockResolvedValue(manager);
|
||||
const { createInteractiveSessionRuntime } = await importRuntime();
|
||||
const runtime = createInteractiveSessionRuntime({
|
||||
config: createConfig(),
|
||||
providerSettingsManager: createProviderSettingsManager(),
|
||||
resumeSessionId: "resumed-session",
|
||||
chatCommandState: createChatCommandState(),
|
||||
requestToolApproval: vi.fn(),
|
||||
resolveToolPolicy: () => ({ autoApprove: true }),
|
||||
askQuestionRef: { current: null },
|
||||
resolveMistakeLimitDecision: undefined,
|
||||
switchToActModeTool: {} as never,
|
||||
onAgentEvent: vi.fn(),
|
||||
onTeamEvent: vi.fn(),
|
||||
onPendingPrompts: vi.fn(),
|
||||
onPendingPromptSubmitted: vi.fn(),
|
||||
});
|
||||
|
||||
await runtime.ensureReady();
|
||||
|
||||
expect(mockLoadInteractiveResumeMessages).toHaveBeenNthCalledWith(
|
||||
expect(loadInteractiveResumeMessagesMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
manager,
|
||||
"resumed-session",
|
||||
@@ -322,16 +701,14 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
expect(manager.start).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
sessionId: "resumed-session",
|
||||
}),
|
||||
config: expect.objectContaining({ sessionId: "resumed-session" }),
|
||||
}),
|
||||
);
|
||||
|
||||
await runtime.resetForNewSession();
|
||||
await runtime.ensureReady();
|
||||
|
||||
expect(mockLoadInteractiveResumeMessages).toHaveBeenNthCalledWith(
|
||||
expect(loadInteractiveResumeMessagesMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
manager,
|
||||
undefined,
|
||||
@@ -348,8 +725,46 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
});
|
||||
|
||||
it("keeps explicit empty restarts eager for config-driven restarts", async () => {
|
||||
const manager = makeManager();
|
||||
const runtime = makeRuntime(manager);
|
||||
let startCount = 0;
|
||||
const manager = {
|
||||
start: vi.fn().mockImplementation(async () => {
|
||||
startCount += 1;
|
||||
const sessionId = `session-${startCount}`;
|
||||
return {
|
||||
sessionId,
|
||||
manifest: createManifest(sessionId),
|
||||
manifestPath: `/tmp/${sessionId}.json`,
|
||||
messagesPath: `/tmp/${sessionId}.messages.json`,
|
||||
};
|
||||
}),
|
||||
readMessages: vi.fn().mockResolvedValue([]),
|
||||
readSessionCompactionState: vi.fn().mockResolvedValue(undefined),
|
||||
updateSessionCompactionState: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn().mockResolvedValue(undefined),
|
||||
ingestHookEvent: vi.fn().mockResolvedValue(undefined),
|
||||
get: vi.fn(),
|
||||
list: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
send: vi.fn(),
|
||||
getAccumulatedUsage: vi.fn(),
|
||||
};
|
||||
createCliCoreMock.mockResolvedValue(manager);
|
||||
const { createInteractiveSessionRuntime } = await importRuntime();
|
||||
const runtime = createInteractiveSessionRuntime({
|
||||
config: createConfig(),
|
||||
providerSettingsManager: createProviderSettingsManager(),
|
||||
chatCommandState: createChatCommandState(),
|
||||
requestToolApproval: vi.fn(),
|
||||
resolveToolPolicy: () => ({ autoApprove: true }),
|
||||
askQuestionRef: { current: null },
|
||||
resolveMistakeLimitDecision: undefined,
|
||||
switchToActModeTool: {} as never,
|
||||
onAgentEvent: vi.fn(),
|
||||
onTeamEvent: vi.fn(),
|
||||
onPendingPrompts: vi.fn(),
|
||||
onPendingPromptSubmitted: vi.fn(),
|
||||
});
|
||||
|
||||
await runtime.ensureReady();
|
||||
await runtime.restartEmpty();
|
||||
@@ -371,7 +786,7 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
manager.send
|
||||
.mockRejectedValueOnce(new SessionNotFoundError("session-1"))
|
||||
.mockResolvedValueOnce(makeTurnResult());
|
||||
const runtime = makeRuntime(manager);
|
||||
const runtime = await makeRuntime(manager);
|
||||
|
||||
await runtime.ensureReady();
|
||||
const result = await runtime.sendCurrentTurn({
|
||||
@@ -404,7 +819,7 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
manager.readMessages.mockRejectedValueOnce(
|
||||
new SessionNotFoundError("session-1"),
|
||||
);
|
||||
const runtime = makeRuntime(manager);
|
||||
const runtime = await makeRuntime(manager);
|
||||
|
||||
await runtime.ensureReady();
|
||||
await runtime.restartWithCurrentMessages();
|
||||
@@ -422,7 +837,7 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
|
||||
it("does not restart with stale messages when another operation changes the active session during a read", async () => {
|
||||
const manager = makeManager();
|
||||
let runtime!: ReturnType<typeof makeRuntime>;
|
||||
let runtime!: Awaited<ReturnType<typeof makeRuntime>>;
|
||||
manager.readMessages.mockImplementationOnce(async () => {
|
||||
await runtime.restartEmpty();
|
||||
return [
|
||||
@@ -432,7 +847,7 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
},
|
||||
];
|
||||
});
|
||||
runtime = makeRuntime(manager);
|
||||
runtime = await makeRuntime(manager);
|
||||
|
||||
await runtime.ensureReady();
|
||||
await runtime.restartWithCurrentMessages();
|
||||
@@ -450,7 +865,7 @@ describe("createInteractiveSessionRuntime", () => {
|
||||
manager.get.mockResolvedValue(undefined);
|
||||
manager.getAccumulatedUsage.mockResolvedValue(undefined);
|
||||
manager.send.mockRejectedValueOnce(new SessionNotFoundError("session-1"));
|
||||
const runtime = makeRuntime(manager);
|
||||
const runtime = await makeRuntime(manager);
|
||||
|
||||
await runtime.ensureReady();
|
||||
const sendPromise = runtime
|
||||
|
||||
@@ -2,10 +2,13 @@ import {
|
||||
type AgentEvent,
|
||||
type AgentHooks,
|
||||
type CheckpointEntry,
|
||||
createSessionCompactionState,
|
||||
isSessionNotFoundError,
|
||||
type PendingPromptMutationResult,
|
||||
type ProviderSettingsManager,
|
||||
projectSessionCompactionState,
|
||||
readSessionCheckpointHistory,
|
||||
type SessionCompactionState,
|
||||
SessionSource,
|
||||
type TeamEvent,
|
||||
type ToolApprovalRequest,
|
||||
@@ -116,6 +119,7 @@ export function createInteractiveSessionRuntime(input: {
|
||||
// A reset can happen while an earlier manager.start() is still in flight.
|
||||
// Bump this before resets and restarts so stale starts cannot become active.
|
||||
let sessionStartGeneration = 0;
|
||||
let manualCompactionAbortController: AbortController | undefined;
|
||||
|
||||
let pendingResumeSessionId = input.resumeSessionId?.trim() || undefined;
|
||||
|
||||
@@ -205,6 +209,7 @@ export function createInteractiveSessionRuntime(input: {
|
||||
const startFreshSession = async (
|
||||
initial: Message[] = [],
|
||||
sessionMetadata?: Record<string, unknown>,
|
||||
initialCompactionState?: SessionCompactionState,
|
||||
): Promise<void> => {
|
||||
const generation = sessionStartGeneration;
|
||||
const manager = await ensureSessionManager();
|
||||
@@ -214,6 +219,7 @@ export function createInteractiveSessionRuntime(input: {
|
||||
toolPolicies: input.config.toolPolicies,
|
||||
interactive: true,
|
||||
initialMessages: initial,
|
||||
...(initialCompactionState ? { initialCompactionState } : {}),
|
||||
...(sessionMetadata ? { sessionMetadata } : {}),
|
||||
localRuntime: {
|
||||
onTeamRestored: () => {},
|
||||
@@ -309,6 +315,25 @@ export function createInteractiveSessionRuntime(input: {
|
||||
}
|
||||
};
|
||||
|
||||
const readCompactionState = async (
|
||||
sessionId: string,
|
||||
): Promise<SessionCompactionState | undefined> => {
|
||||
const manager = sessionManager;
|
||||
if (!manager) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return await manager.readSessionCompactionState(sessionId);
|
||||
} catch (error) {
|
||||
input.config.logger?.log?.("Failed to read session compaction state", {
|
||||
sessionId,
|
||||
error,
|
||||
severity: "warn",
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const recoverMissingActiveSession = async (
|
||||
error: unknown,
|
||||
): Promise<MissingSessionRecovery> => {
|
||||
@@ -343,6 +368,15 @@ export function createInteractiveSessionRuntime(input: {
|
||||
return await missingSessionRecoveryPromise;
|
||||
};
|
||||
|
||||
const readCurrentCompactionState = async (): Promise<
|
||||
SessionCompactionState | undefined
|
||||
> => {
|
||||
if (!activeSessionId) {
|
||||
return undefined;
|
||||
}
|
||||
return await readCompactionState(activeSessionId);
|
||||
};
|
||||
|
||||
const stopCurrentSession = async (): Promise<void> => {
|
||||
const sessionId = activeSessionId;
|
||||
if (sessionManager && sessionId) {
|
||||
@@ -377,48 +411,69 @@ export function createInteractiveSessionRuntime(input: {
|
||||
});
|
||||
};
|
||||
|
||||
const restartWithMessages = async (
|
||||
messages: Message[],
|
||||
sessionMetadata?: Record<string, unknown>,
|
||||
): Promise<void> => {
|
||||
sessionStartGeneration += 1;
|
||||
pendingResumeSessionId = undefined;
|
||||
startupError = undefined;
|
||||
// Publish the restart as the in-flight startup. Teardown leaves a window
|
||||
// with no active session, and without this barrier a concurrent
|
||||
// ensureReady() (e.g. a message submitted right after a plan/act toggle)
|
||||
// reads that window as "no session" and boots an empty session that then
|
||||
// races the restarted one for the active slot.
|
||||
const restart = (async () => {
|
||||
await stopCurrentSession();
|
||||
clearActiveSession();
|
||||
await startFreshSession(messages, sessionMetadata);
|
||||
})().catch((error) => {
|
||||
startupError = error;
|
||||
throw error;
|
||||
});
|
||||
startupPromise = restart;
|
||||
try {
|
||||
await restart;
|
||||
} finally {
|
||||
// Restore the pre-restart steady state (startupPromise unset) so a
|
||||
// failed restart stays retryable by the next ensureReady(). A newer
|
||||
// startup that already replaced the barrier is left alone.
|
||||
if (startupPromise === restart) {
|
||||
startupPromise = undefined;
|
||||
const restartWithMessages = async (
|
||||
messages: Message[],
|
||||
sessionMetadata?: Record<string, unknown>,
|
||||
initialCompactionState?: SessionCompactionState,
|
||||
): Promise<void> => {
|
||||
sessionStartGeneration += 1;
|
||||
pendingResumeSessionId = undefined;
|
||||
startupError = undefined;
|
||||
// Publish the restart as the in-flight startup. Teardown leaves a window
|
||||
// with no active session, and without this barrier a concurrent
|
||||
// ensureReady() (e.g. a message submitted right after a plan/act toggle)
|
||||
// reads that window as "no session" and boots an empty session that then
|
||||
// races the restarted one for the active slot.
|
||||
const restart = (async () => {
|
||||
await stopCurrentSession();
|
||||
clearActiveSession();
|
||||
await startFreshSession(
|
||||
messages,
|
||||
sessionMetadata,
|
||||
initialCompactionState,
|
||||
);
|
||||
})().catch((error) => {
|
||||
startupError = error;
|
||||
throw error;
|
||||
});
|
||||
startupPromise = restart;
|
||||
try {
|
||||
await restart;
|
||||
} finally {
|
||||
// Restore the pre-restart steady state (startupPromise unset) so a
|
||||
// failed restart stays retryable by the next ensureReady(). A newer
|
||||
// startup that already replaced the barrier is left alone.
|
||||
if (startupPromise === restart) {
|
||||
startupPromise = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const restartWithCurrentMessages = async (): Promise<void> => {
|
||||
const { messages, status } = await readCurrentMessages();
|
||||
const [{ messages, status }, compactionState] = await Promise.all([
|
||||
readCurrentMessages(),
|
||||
readCurrentCompactionState(),
|
||||
]);
|
||||
if (status !== "read") {
|
||||
// If reading recovered a missing hub session, the current messages are
|
||||
// already in the replacement session. If the read is stale, another async
|
||||
// operation changed the active session while this read was in flight.
|
||||
return;
|
||||
}
|
||||
await restartWithMessages(messages);
|
||||
const projectedMessages = compactionState
|
||||
? projectSessionCompactionState(compactionState, messages)
|
||||
: undefined;
|
||||
await restartWithMessages(
|
||||
messages,
|
||||
undefined,
|
||||
projectedMessages
|
||||
? createSessionCompactionState({
|
||||
sourceMessages: messages,
|
||||
compactedMessages: projectedMessages,
|
||||
systemPrompt: compactionState?.system_prompt,
|
||||
})
|
||||
: undefined,
|
||||
);
|
||||
};
|
||||
|
||||
const restartEmpty = async (): Promise<void> => {
|
||||
@@ -532,6 +587,10 @@ export function createInteractiveSessionRuntime(input: {
|
||||
if (messages.length === 0) {
|
||||
throw new Error("Cannot fork an empty session.");
|
||||
}
|
||||
const compactionState = await readCompactionState(forkedFromSessionId);
|
||||
const projectedMessages = compactionState
|
||||
? projectSessionCompactionState(compactionState, messages)
|
||||
: undefined;
|
||||
await manager.stop(forkedFromSessionId);
|
||||
const forkMetadata = buildForkSessionMetadata({
|
||||
forkedFromSessionId,
|
||||
@@ -539,7 +598,17 @@ export function createInteractiveSessionRuntime(input: {
|
||||
sourceSession: sessionRecord,
|
||||
messages,
|
||||
});
|
||||
await startFreshSession(messages, forkMetadata);
|
||||
await startFreshSession(
|
||||
messages,
|
||||
forkMetadata,
|
||||
projectedMessages
|
||||
? createSessionCompactionState({
|
||||
sourceMessages: messages,
|
||||
compactedMessages: projectedMessages,
|
||||
systemPrompt: compactionState?.system_prompt,
|
||||
})
|
||||
: undefined,
|
||||
);
|
||||
return { forkedFromSessionId, newSessionId: activeSessionId };
|
||||
};
|
||||
|
||||
@@ -561,9 +630,17 @@ export function createInteractiveSessionRuntime(input: {
|
||||
const compactCurrentSession = async (): Promise<{
|
||||
messagesBefore: number;
|
||||
messagesAfter: number;
|
||||
workingContextMessagesAfter?: number;
|
||||
compacted: boolean;
|
||||
}> => {
|
||||
if (!sessionManager) {
|
||||
if (input.config.compaction?.enabled === false) {
|
||||
throw new Error(
|
||||
"Cannot compact because compaction is off for this session.",
|
||||
);
|
||||
}
|
||||
const manager = sessionManager;
|
||||
const sourceSessionId = activeSessionId;
|
||||
if (!manager || !sourceSessionId) {
|
||||
return { messagesBefore: 0, messagesAfter: 0, compacted: false };
|
||||
}
|
||||
const { messages, status } = await readCurrentMessages();
|
||||
@@ -577,12 +654,28 @@ export function createInteractiveSessionRuntime(input: {
|
||||
if (messagesBefore === 0) {
|
||||
return { messagesBefore: 0, messagesAfter: 0, compacted: false };
|
||||
}
|
||||
const result = await compactInteractiveMessages({
|
||||
config: input.config,
|
||||
providerSettingsManager: input.providerSettingsManager,
|
||||
sessionId: activeSessionId,
|
||||
messages,
|
||||
});
|
||||
const sessionRecord = await manager.get(sourceSessionId);
|
||||
if (sessionRecord?.status === "running") {
|
||||
throw new Error(
|
||||
"Cannot compact while the current turn is running. Wait for it to finish or abort it first.",
|
||||
);
|
||||
}
|
||||
let result: Awaited<ReturnType<typeof compactInteractiveMessages>>;
|
||||
const abortController = new AbortController();
|
||||
manualCompactionAbortController = abortController;
|
||||
try {
|
||||
result = await compactInteractiveMessages({
|
||||
config: input.config,
|
||||
providerSettingsManager: input.providerSettingsManager,
|
||||
sessionId: sourceSessionId,
|
||||
messages,
|
||||
abortSignal: abortController.signal,
|
||||
});
|
||||
} finally {
|
||||
if (manualCompactionAbortController === abortController) {
|
||||
manualCompactionAbortController = undefined;
|
||||
}
|
||||
}
|
||||
if (!result.compacted) {
|
||||
return {
|
||||
messagesBefore,
|
||||
@@ -590,10 +683,24 @@ export function createInteractiveSessionRuntime(input: {
|
||||
compacted: false,
|
||||
};
|
||||
}
|
||||
await restartWithMessages(result.messages);
|
||||
if (!result.compactionState) {
|
||||
return {
|
||||
messagesBefore,
|
||||
messagesAfter: messagesBefore,
|
||||
compacted: false,
|
||||
};
|
||||
}
|
||||
const updated = await manager.updateSessionCompactionState(
|
||||
sourceSessionId,
|
||||
result.compactionState,
|
||||
);
|
||||
if (!updated.updated) {
|
||||
throw new Error("Compaction could not be saved. Try again.");
|
||||
}
|
||||
return {
|
||||
messagesBefore,
|
||||
messagesAfter: result.messages.length,
|
||||
messagesAfter: result.canonicalMessages.length,
|
||||
workingContextMessagesAfter: result.compactionState?.messages.length,
|
||||
compacted: true,
|
||||
};
|
||||
};
|
||||
@@ -683,6 +790,9 @@ export function createInteractiveSessionRuntime(input: {
|
||||
}
|
||||
abortRequested = true;
|
||||
markAbortInProgress();
|
||||
manualCompactionAbortController?.abort(
|
||||
new Error("Interactive runtime abort requested"),
|
||||
);
|
||||
sessionManager
|
||||
.abort(activeSessionId, new Error("Interactive runtime abort requested"))
|
||||
.catch(() => {});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
type ContentBlock,
|
||||
formatDisplayUserInput,
|
||||
type MessageWithMetadata,
|
||||
normalizeUserInput,
|
||||
type ToolResultContent,
|
||||
type ToolUseContent,
|
||||
} from "@cline/shared";
|
||||
@@ -681,7 +681,7 @@ function renderContentHTML(
|
||||
toolResultsMap: Map<string, ToolResultContent>,
|
||||
): string {
|
||||
if (typeof content === "string") {
|
||||
const text = isUser ? normalizeUserInput(content) : content;
|
||||
const text = isUser ? formatDisplayUserInput(content) : content;
|
||||
return renderTextHTML(text);
|
||||
}
|
||||
|
||||
@@ -689,7 +689,7 @@ function renderContentHTML(
|
||||
.map((block) => {
|
||||
switch (block.type) {
|
||||
case "text": {
|
||||
const text = isUser ? normalizeUserInput(block.text) : block.text;
|
||||
const text = isUser ? formatDisplayUserInput(block.text) : block.text;
|
||||
return renderTextHTML(text);
|
||||
}
|
||||
case "tool_use":
|
||||
|
||||
@@ -18,12 +18,12 @@ import { useTerminalBackground } from "../hooks/use-terminal-background";
|
||||
import {
|
||||
getDefaultForeground,
|
||||
getModeAccent,
|
||||
getModeInputBackground,
|
||||
getUserMessageBackground,
|
||||
palette,
|
||||
type TerminalTheme,
|
||||
} from "../palette";
|
||||
import type { ChatEntry } from "../types";
|
||||
import { getSyntaxStyle } from "../utils/syntax-style";
|
||||
import { getSyntaxStyle, type SyntaxAccentMode } from "../utils/syntax-style";
|
||||
import { isWarningToolError } from "../utils/tool-errors";
|
||||
import {
|
||||
parseApplyPatchInput,
|
||||
@@ -291,7 +291,7 @@ function ClineCreditsClinePassErrorView(props: { defaultFg?: string }) {
|
||||
/>
|
||||
<box flexDirection="row">
|
||||
<text fg="gray">Purchase Credits: </text>
|
||||
<text fg="cyan" selectable>
|
||||
<text fg={palette.act} selectable>
|
||||
<a href={CLINE_CREDITS_DASHBOARD_URL}>
|
||||
{CLINE_CREDITS_DASHBOARD_URL}
|
||||
</a>
|
||||
@@ -299,7 +299,7 @@ function ClineCreditsClinePassErrorView(props: { defaultFg?: string }) {
|
||||
</box>
|
||||
<box flexDirection="row">
|
||||
<text fg="gray">Purchase ClinePass: </text>
|
||||
<text fg="cyan" selectable>
|
||||
<text fg={palette.act} selectable>
|
||||
<a href={subscriptionUrl}>{subscriptionUrl}</a>
|
||||
</text>
|
||||
</box>
|
||||
@@ -377,13 +377,13 @@ function ClinePassSubscriptionErrorView(props: {
|
||||
)}
|
||||
<box flexDirection="row">
|
||||
<text fg="gray">Subscribe: </text>
|
||||
<text fg="cyan" selectable>
|
||||
<text fg={palette.act} selectable>
|
||||
<a href={subscriptionUrl}>Open subscription page</a>
|
||||
</text>
|
||||
</box>
|
||||
<box flexDirection="row">
|
||||
<text fg="gray">URL: </text>
|
||||
<text fg="cyan" selectable>
|
||||
<text fg={palette.act} selectable>
|
||||
<a href={subscriptionUrl}>{subscriptionUrl}</a>
|
||||
</text>
|
||||
</box>
|
||||
@@ -422,16 +422,15 @@ function ClineOrgIndividualInferenceSubscriptionErrorView(props: {
|
||||
export function ChatEntryView(props: {
|
||||
entry: ChatEntry;
|
||||
accent?: string;
|
||||
/** Mode the entry was produced in (resolved with the current-mode fallback). */
|
||||
mode?: SyntaxAccentMode;
|
||||
loadIndividualSubscriptionPlans?: () => Promise<ClineSubscriptionPlan[]>;
|
||||
terminalTheme: TerminalTheme;
|
||||
}) {
|
||||
const { entry, accent = palette.act, terminalTheme } = props;
|
||||
const { entry, accent = palette.act, mode = "act", terminalTheme } = props;
|
||||
const terminalBg = useTerminalBackground();
|
||||
const defaultFg = getDefaultForeground(terminalBg);
|
||||
const userMsgBg = getModeInputBackground(
|
||||
accent === palette.plan ? "plan" : "act",
|
||||
terminalBg,
|
||||
);
|
||||
const userMsgBg = getUserMessageBackground(terminalBg);
|
||||
|
||||
switch (entry.kind) {
|
||||
case "user":
|
||||
@@ -442,10 +441,9 @@ export function ChatEntryView(props: {
|
||||
marginX={-1}
|
||||
paddingLeft={1}
|
||||
paddingRight={2}
|
||||
paddingY={1}
|
||||
>
|
||||
<box width={2}>
|
||||
<text fg={accent}>{">"}</text>
|
||||
<text fg={accent}>{"❯"}</text>
|
||||
</box>
|
||||
<text fg={defaultFg} selectable>
|
||||
{entry.text}
|
||||
@@ -461,10 +459,9 @@ export function ChatEntryView(props: {
|
||||
marginX={-1}
|
||||
paddingLeft={1}
|
||||
paddingRight={2}
|
||||
paddingY={1}
|
||||
>
|
||||
<box width={2}>
|
||||
<text fg={accent}>{">"}</text>
|
||||
<text fg={accent}>{"❯"}</text>
|
||||
</box>
|
||||
{entry.delivery === "steer" && <text fg="yellow">[steer] </text>}
|
||||
{entry.delivery === "queue" && <text fg="gray">[queued] </text>}
|
||||
@@ -489,7 +486,7 @@ export function ChatEntryView(props: {
|
||||
<box flexGrow={1}>
|
||||
<markdown
|
||||
content={content}
|
||||
syntaxStyle={getSyntaxStyle(terminalTheme)}
|
||||
syntaxStyle={getSyntaxStyle(terminalTheme, mode)}
|
||||
streaming={entry.streaming}
|
||||
fg={defaultFg}
|
||||
/>
|
||||
@@ -565,7 +562,7 @@ export function ChatEntryView(props: {
|
||||
if (entry.elapsed) parts.push(`${entry.elapsed}s`);
|
||||
if (entry.tokens > 0)
|
||||
parts.push(`${entry.tokens.toLocaleString()} tokens`);
|
||||
if (entry.cost > 0) parts.push(`$${entry.cost.toFixed(3)}`);
|
||||
if (entry.cost > 0) parts.push(`$${entry.cost.toFixed(2)}`);
|
||||
if (entry.iterations > 0)
|
||||
parts.push(
|
||||
`${entry.iterations} iteration${entry.iterations !== 1 ? "s" : ""}`,
|
||||
|
||||
@@ -96,11 +96,15 @@ export const ChatMessageList = forwardRef<
|
||||
<box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
|
||||
{props.entries.map((entry, i) => {
|
||||
const key = `${i}:${entry.kind}`;
|
||||
// Single source of truth for the entry's mode: the glyph accent
|
||||
// and the markdown accent must never diverge.
|
||||
const entryMode = entry.mode ?? props.uiMode ?? "act";
|
||||
return (
|
||||
<ChatEntryView
|
||||
key={key}
|
||||
entry={entry}
|
||||
accent={accent}
|
||||
accent={getModeAccent(entryMode, terminalTheme)}
|
||||
mode={entryMode === "plan" ? "plan" : "act"}
|
||||
loadIndividualSubscriptionPlans={
|
||||
props.loadIndividualSubscriptionPlans
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ export function AccountDialogContent(
|
||||
if (state.status === "loading") {
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">Cline Account</text>
|
||||
<text fg={palette.act}>Cline Account</text>
|
||||
<text fg="gray">{state.message}</text>
|
||||
<text fg="gray">Esc to close</text>
|
||||
</box>
|
||||
@@ -434,7 +434,7 @@ export function AccountDialogContent(
|
||||
if (state.status === "error") {
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">Cline Account</text>
|
||||
<text fg={palette.act}>Cline Account</text>
|
||||
<text fg="red">{state.message}</text>
|
||||
<text fg="gray">Esc to close</text>
|
||||
</box>
|
||||
@@ -444,7 +444,7 @@ export function AccountDialogContent(
|
||||
if (state.status === "unauthenticated") {
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">Cline Account</text>
|
||||
<text fg={palette.act}>Cline Account</text>
|
||||
<text>Sign in or create a Cline account.</text>
|
||||
<text fg="gray">
|
||||
Get access to the latest models with regular free promos and
|
||||
@@ -473,7 +473,7 @@ export function AccountDialogContent(
|
||||
if (view === "organizations") {
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1}>
|
||||
<text fg="cyan">Change Account</text>
|
||||
<text fg={palette.act}>Change Account</text>
|
||||
<box flexDirection="column" gap={0}>
|
||||
{orgRows.map((row, index) => (
|
||||
<OrganizationRow
|
||||
@@ -503,7 +503,7 @@ export function AccountDialogContent(
|
||||
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">Cline Account</text>
|
||||
<text fg={palette.act}>Cline Account</text>
|
||||
|
||||
<box flexDirection="row" gap={2}>
|
||||
<box
|
||||
@@ -514,7 +514,7 @@ export function AccountDialogContent(
|
||||
border
|
||||
borderColor="gray"
|
||||
>
|
||||
<text fg="cyan">{userInitial(loaded)}</text>
|
||||
<text fg={palette.act}>{userInitial(loaded)}</text>
|
||||
</box>
|
||||
<box flexDirection="column" flexGrow={1}>
|
||||
<text selectable>{displayName}</text>
|
||||
|
||||
@@ -191,7 +191,7 @@ export function CommandPaletteContent(
|
||||
{" "}
|
||||
</text>
|
||||
<text
|
||||
fg={isSelected ? palette.textOnSelection : "cyan"}
|
||||
fg={isSelected ? palette.textOnSelection : palette.act}
|
||||
width={shortcutWidth}
|
||||
flexShrink={0}
|
||||
>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function ExtDetailContent(
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<text fg="cyan">
|
||||
<text fg={palette.act}>
|
||||
<strong>{row.name}</strong>
|
||||
</text>
|
||||
<text
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @jsxImportSource @opentui/react
|
||||
import type { ChoiceContext } from "@opentui-ui/dialog";
|
||||
import { useDialogKeyboard } from "@opentui-ui/dialog/react";
|
||||
import { palette } from "../../palette";
|
||||
|
||||
type HelpRow =
|
||||
| { kind: "heading"; id: string; text: string }
|
||||
@@ -277,7 +278,7 @@ export function HelpDialogContent(props: ChoiceContext<void>) {
|
||||
}
|
||||
return (
|
||||
<box key={row.id} flexDirection="row" paddingX={1}>
|
||||
<text fg="cyan" width={KEY_WIDTH} flexShrink={0}>
|
||||
<text fg={palette.act} width={KEY_WIDTH} flexShrink={0}>
|
||||
{row.key}
|
||||
</text>
|
||||
<text fg="gray">{row.desc}</text>
|
||||
|
||||
@@ -121,7 +121,7 @@ export function McpManagerContent(
|
||||
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1}>
|
||||
<text fg="cyan">MCP Servers</text>
|
||||
<text fg={palette.act}>MCP Servers</text>
|
||||
|
||||
<text fg="gray" marginTop={1}>
|
||||
Settings file:
|
||||
@@ -141,7 +141,7 @@ export function McpManagerContent(
|
||||
const enabledIcon =
|
||||
typeof srv.enabled === "boolean" ? (enabled ? "● " : "○ ") : "";
|
||||
const status = getMcpManagerEntryStatus(srv);
|
||||
let rowColor = isSel ? "cyan" : "gray";
|
||||
let rowColor = isSel ? palette.act : "gray";
|
||||
if (enabled && typeof srv.enabled === "boolean") {
|
||||
rowColor = palette.success;
|
||||
}
|
||||
|
||||
@@ -371,14 +371,14 @@ function ClinePassBrowserPageContent(
|
||||
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">
|
||||
<text fg={palette.act}>
|
||||
<strong>{providerName}</strong>
|
||||
</text>
|
||||
|
||||
<text>{status}</text>
|
||||
|
||||
<text fg="gray">{pageLabel}:</text>
|
||||
<text fg="cyan" selectable>
|
||||
<text fg={palette.act} selectable>
|
||||
<a href={url}>{url}</a>
|
||||
</text>
|
||||
|
||||
@@ -596,7 +596,7 @@ export function ProviderConfigInputContent(
|
||||
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">
|
||||
<text fg={palette.act}>
|
||||
<strong>{providerName}</strong>
|
||||
</text>
|
||||
|
||||
@@ -689,7 +689,7 @@ export function CodexCliStatusContent(
|
||||
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">
|
||||
<text fg={palette.act}>
|
||||
<strong>{providerName}</strong>
|
||||
</text>
|
||||
|
||||
@@ -707,7 +707,7 @@ export function CodexCliStatusContent(
|
||||
<text fg="yellow">Codex CLI was not found</text>
|
||||
<text fg="gray">{status.reason}</text>
|
||||
<text fg="gray">Install Codex CLI from:</text>
|
||||
<text fg="cyan" selectable>
|
||||
<text fg={palette.act} selectable>
|
||||
{CODEX_CLI_INSTALL_URL}
|
||||
</text>
|
||||
</box>
|
||||
@@ -869,7 +869,7 @@ export function OAuthLoginContent(
|
||||
if (mode === "device") {
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">
|
||||
<text fg={palette.act}>
|
||||
<strong>{providerName}</strong>
|
||||
</text>
|
||||
|
||||
@@ -884,7 +884,7 @@ export function OAuthLoginContent(
|
||||
<strong>{deviceUserCode}</strong>
|
||||
</text>
|
||||
<text fg="gray">Visit this URL and enter the code above:</text>
|
||||
<text fg="cyan" selectable>
|
||||
<text fg={palette.act} selectable>
|
||||
<a href={deviceVerifyUrl}>{deviceVerifyUrl}</a>
|
||||
</text>
|
||||
</box>
|
||||
@@ -901,7 +901,7 @@ export function OAuthLoginContent(
|
||||
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1} gap={1}>
|
||||
<text fg="cyan">
|
||||
<text fg={palette.act}>
|
||||
<strong>{providerName}</strong>
|
||||
</text>
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ export function SkillsPickerContent(props: SkillsPickerContentProps) {
|
||||
onMouseDown={() => resolve(SKILLS_MARKETPLACE_ACTION)}
|
||||
height={1}
|
||||
>
|
||||
<text fg={isSelected ? palette.textOnSelection : "cyan"}>
|
||||
<text fg={isSelected ? palette.textOnSelection : palette.act}>
|
||||
{isSelected ? "❯ " : " "}
|
||||
Browse more skills at {SKILLS_MARKETPLACE_URL}
|
||||
</text>
|
||||
|
||||
@@ -155,7 +155,7 @@ export function ToolApprovalContent(
|
||||
<box flexDirection="column" paddingX={1}>
|
||||
<text fg="yellow">Approve tool call?</text>
|
||||
|
||||
<text fg="cyan" marginTop={1}>
|
||||
<text fg={palette.act} marginTop={1}>
|
||||
<strong>{props.request.toolName}</strong>
|
||||
</text>
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export type TextareaHandle = Pick<
|
||||
|
||||
export interface InputBarProps {
|
||||
accent: string;
|
||||
inputBackground: string;
|
||||
ruleColor: string;
|
||||
inputForeground: string;
|
||||
inputPlaceholder: string;
|
||||
placeholder: string;
|
||||
@@ -62,7 +62,7 @@ function readTextPaste(event: PasteEvent): string | null {
|
||||
export function InputBar(props: InputBarProps) {
|
||||
const {
|
||||
accent,
|
||||
inputBackground,
|
||||
ruleColor,
|
||||
inputForeground,
|
||||
inputPlaceholder,
|
||||
placeholder,
|
||||
@@ -197,13 +197,13 @@ export function InputBar(props: InputBarProps) {
|
||||
<box
|
||||
flexDirection="row"
|
||||
alignItems="flex-start"
|
||||
backgroundColor={inputBackground}
|
||||
paddingX={2}
|
||||
paddingY={1}
|
||||
border={["top", "bottom"]}
|
||||
borderStyle="single"
|
||||
borderColor={ruleColor}
|
||||
onMouseDown={props.onFocusRequest}
|
||||
>
|
||||
<text fg={accent}>
|
||||
<strong>{">"}</strong>
|
||||
<strong>{"❯"}</strong>
|
||||
</text>
|
||||
<box flexGrow={1} paddingLeft={1}>
|
||||
<textarea
|
||||
|
||||
@@ -27,7 +27,7 @@ export type ClineModelPickerEntry =
|
||||
function tagColor(tag: string): string {
|
||||
if (tag === "FREE") return palette.success;
|
||||
if (tag === "BEST") return "magenta";
|
||||
return "cyan";
|
||||
return palette.act;
|
||||
}
|
||||
|
||||
function resolveDisplayName(
|
||||
|
||||
@@ -17,7 +17,7 @@ type ClineModelEntriesState =
|
||||
function tagColor(tag: string): string {
|
||||
if (tag === "FREE") return palette.success;
|
||||
if (tag === "BEST") return "magenta";
|
||||
return "cyan";
|
||||
return palette.act;
|
||||
}
|
||||
|
||||
function resolveDisplayName(
|
||||
@@ -272,7 +272,7 @@ export function ClineModelSelectorDialogContent(
|
||||
if (state.status === "error") {
|
||||
return (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg="cyan">Choose a model</text>
|
||||
<text fg={palette.act}>Choose a model</text>
|
||||
<ProviderRow providerName={props.currentProviderName} focused={false} />
|
||||
<text fg="red">{state.message}</text>
|
||||
<text fg="gray">R to retry, Esc to go back</text>
|
||||
@@ -282,7 +282,7 @@ export function ClineModelSelectorDialogContent(
|
||||
|
||||
return (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<text fg="cyan">Choose a model</text>
|
||||
<text fg={palette.act}>Choose a model</text>
|
||||
<ProviderRow providerName={props.currentProviderName} focused={false} />
|
||||
<text fg="gray">{state.message}</text>
|
||||
<text fg="gray">Esc to go back</text>
|
||||
|
||||
@@ -329,7 +329,8 @@ export function ThinkingLevelContent(
|
||||
) {
|
||||
const { resolve, dismiss, dialogId, modelName, currentLevel } = props;
|
||||
const [selected, setSelected] = useState(() => {
|
||||
const idx = THINKING_LEVELS.findIndex((l) => l.value === currentLevel);
|
||||
const initialLevel = currentLevel === "none" ? "medium" : currentLevel;
|
||||
const idx = THINKING_LEVELS.findIndex((l) => l.value === initialLevel);
|
||||
return idx >= 0 ? idx : 0;
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export function ProviderRow({
|
||||
<text fg={focused ? palette.selection : "gray"} flexShrink={0}>
|
||||
{focused ? "❯" : " "}
|
||||
</text>
|
||||
<text fg={focused ? palette.selection : "cyan"} flexShrink={0}>
|
||||
<text fg={focused ? palette.selection : palette.act} flexShrink={0}>
|
||||
Provider:
|
||||
</text>
|
||||
<text fg="white">{providerName}</text>
|
||||
|
||||
@@ -14,14 +14,14 @@ describe("createContextBar", () => {
|
||||
it("keeps a stable width while changing segment lengths", () => {
|
||||
expect(createContextBar(0, 100)).toEqual({
|
||||
filled: "",
|
||||
empty: "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
empty: "\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
});
|
||||
expect(createContextBar(50, 100)).toEqual({
|
||||
filled: "\u2588\u2588\u2588\u2588",
|
||||
empty: "\u2588\u2588\u2588\u2588",
|
||||
filled: "\u2588\u2588\u2588",
|
||||
empty: "\u2588\u2588\u2588",
|
||||
});
|
||||
expect(createContextBar(100, 100)).toEqual({
|
||||
filled: "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
filled: "\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
empty: "",
|
||||
});
|
||||
});
|
||||
@@ -29,17 +29,17 @@ describe("createContextBar", () => {
|
||||
it("shows a non-empty fill when usage is above zero", () => {
|
||||
expect(createContextBar(7_000, 1_000_000)).toEqual({
|
||||
filled: "\u2588",
|
||||
empty: "\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
empty: "\u2588\u2588\u2588\u2588\u2588",
|
||||
});
|
||||
});
|
||||
|
||||
it("reserves the final segment for usage at or above the limit", () => {
|
||||
expect(createContextBar(999_999, 1_000_000)).toEqual({
|
||||
filled: "\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
filled: "\u2588\u2588\u2588\u2588\u2588",
|
||||
empty: "\u2588",
|
||||
});
|
||||
expect(createContextBar(1_000_000, 1_000_000)).toEqual({
|
||||
filled: "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
filled: "\u2588\u2588\u2588\u2588\u2588\u2588",
|
||||
empty: "",
|
||||
});
|
||||
});
|
||||
@@ -58,22 +58,32 @@ describe("formatStatusBarUsageText", () => {
|
||||
totalCost: 0.123,
|
||||
providerId: "cline",
|
||||
}),
|
||||
).toBe("(12,345 tokens) $0.12");
|
||||
).toBe("(12,345) $0.12");
|
||||
});
|
||||
|
||||
it("displays subscription message when the provider is a subscription provider", () => {
|
||||
it("rounds cost to two decimals even when tiny", () => {
|
||||
expect(
|
||||
formatStatusBarUsageText({
|
||||
totalTokens: 12_345,
|
||||
totalCost: 0.0004,
|
||||
providerId: "cline",
|
||||
}),
|
||||
).toBe("(12,345) $0.00");
|
||||
});
|
||||
|
||||
it("hides cost entirely for subscription providers", () => {
|
||||
expect(
|
||||
formatStatusBarUsageText({
|
||||
totalTokens: 12_345,
|
||||
totalCost: 0.123,
|
||||
providerId: "cline-pass",
|
||||
}),
|
||||
).toBe("(12,345 tokens) $0.00 (included with subscription)");
|
||||
).toBe("(12,345)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveModelDisplayName", () => {
|
||||
it("keeps ClinePass visible when model ids have provider prefixes", () => {
|
||||
it("uses the friendly model name with a ClinePass prefix", () => {
|
||||
expect(
|
||||
resolveModelDisplayName({
|
||||
providerId: "cline-pass",
|
||||
@@ -82,7 +92,30 @@ describe("resolveModelDisplayName", () => {
|
||||
"zai/glm-5.2": { name: "GLM 5.2" },
|
||||
},
|
||||
}),
|
||||
).toBe("ClinePass/glm-5.2");
|
||||
).toBe("ClinePass: GLM 5.2");
|
||||
});
|
||||
|
||||
it("falls back to the bare model id with a ClinePass prefix when unknown", () => {
|
||||
expect(
|
||||
resolveModelDisplayName({
|
||||
providerId: "cline-pass",
|
||||
modelId: "zai/glm-5.2",
|
||||
}),
|
||||
).toBe("ClinePass: glm-5.2");
|
||||
});
|
||||
|
||||
it("keeps the reasoning effort next to the model name", () => {
|
||||
expect(
|
||||
resolveModelDisplayName({
|
||||
providerId: "cline-pass",
|
||||
modelId: "zai/glm-5.2",
|
||||
knownModels: {
|
||||
"zai/glm-5.2": { name: "GLM 5.2" },
|
||||
},
|
||||
thinking: true,
|
||||
reasoningEffort: "high",
|
||||
}),
|
||||
).toBe("ClinePass: GLM 5.2 (high)");
|
||||
});
|
||||
|
||||
it("uses the friendly model name for non-ClinePass providers", () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { HOME_VIEW_MAX_WIDTH } from "../types";
|
||||
export function createContextBar(
|
||||
used: number,
|
||||
total?: number,
|
||||
width = 8,
|
||||
width = 6,
|
||||
): { filled: string; empty: string } {
|
||||
const normalizedWidth = Math.max(0, Math.floor(width));
|
||||
const ratio = total && total > 0 ? Math.min(used / total, 1) : 0;
|
||||
@@ -45,13 +45,13 @@ export function resolveContextBarFilledForeground(
|
||||
}
|
||||
|
||||
function formatCost(cost: number): string {
|
||||
if (cost < 0.01) return `$${cost.toFixed(4)}`;
|
||||
return `$${cost.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function formatCostText(providerId: string, totalCost: number): string {
|
||||
// Subscription providers (ClinePass) have no per-use cost worth surfacing.
|
||||
if (shouldShowCliUsageCoveredBySubscription(providerId)) {
|
||||
return "$0.00 (included with subscription)";
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!shouldShowCliUsageCost(providerId)) {
|
||||
@@ -66,7 +66,7 @@ export function formatStatusBarUsageText(input: {
|
||||
totalCost: number;
|
||||
providerId: string;
|
||||
}): string {
|
||||
const tokens = `(${input.totalTokens.toLocaleString()} tokens)`;
|
||||
const tokens = `(${input.totalTokens.toLocaleString()})`;
|
||||
const costText = formatCostText(input.providerId, input.totalCost);
|
||||
|
||||
if (!costText) {
|
||||
@@ -102,12 +102,12 @@ export function resolveModelDisplayName(config: {
|
||||
}): string {
|
||||
const info = lookupModelInfo(config.modelId, config.knownModels);
|
||||
const modelIdTail = config.modelId.split("/").pop() ?? config.modelId;
|
||||
const displayName =
|
||||
config.providerId === "cline-pass"
|
||||
? `ClinePass/${modelIdTail}`
|
||||
: (info?.name ?? modelIdTail);
|
||||
let displayName = info?.name ?? modelIdTail;
|
||||
if (config.thinking && config.reasoningEffort) {
|
||||
return `${displayName} (${config.reasoningEffort})`;
|
||||
displayName = `${displayName} (${config.reasoningEffort})`;
|
||||
}
|
||||
if (config.providerId === "cline-pass") {
|
||||
displayName = `ClinePass: ${displayName}`;
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@@ -103,9 +103,16 @@ export function SessionProvider(props: {
|
||||
const [hasSubmitted, setHasSubmitted] = useState(
|
||||
(initialEntries?.length ?? 0) > 0,
|
||||
);
|
||||
const [uiMode, setUiMode] = useState<AgentMode>(
|
||||
const [uiMode, _setUiMode] = useState<AgentMode>(
|
||||
config.mode === "plan" ? "plan" : "act",
|
||||
);
|
||||
// Mirror for appendEntry: entries are appended from event-handler
|
||||
// callbacks that must see the mode at append time, not at closure time.
|
||||
const uiModeRef = useRef<AgentMode>(config.mode === "plan" ? "plan" : "act");
|
||||
const setUiMode = useCallback((mode: AgentMode) => {
|
||||
uiModeRef.current = mode;
|
||||
_setUiMode(mode);
|
||||
}, []);
|
||||
const initialAutoApproveAll = config.toolPolicies["*"]?.autoApprove !== false;
|
||||
const autoApproveAllRef = useRef(initialAutoApproveAll);
|
||||
const [autoApproveAll, _setAutoApproveAll] = useState(initialAutoApproveAll);
|
||||
@@ -132,8 +139,9 @@ export function SessionProvider(props: {
|
||||
);
|
||||
|
||||
const appendEntry = useCallback((entry: ChatEntry) => {
|
||||
const stamped = entry.mode ? entry : { ...entry, mode: uiModeRef.current };
|
||||
setEntries((prev) => {
|
||||
const next = [...prev, entry];
|
||||
const next = [...prev, stamped];
|
||||
return next.length <= MAX_BUFFERED_LINES
|
||||
? next
|
||||
: next.slice(next.length - MAX_BUFFERED_LINES);
|
||||
@@ -188,8 +196,8 @@ export function SessionProvider(props: {
|
||||
}, []);
|
||||
|
||||
const toggleMode = useCallback(() => {
|
||||
setUiMode((m) => (m === "act" ? "plan" : "act"));
|
||||
}, []);
|
||||
setUiMode(uiModeRef.current === "act" ? "plan" : "act");
|
||||
}, [setUiMode]);
|
||||
|
||||
const toggleAutoApprove = useCallback(() => {
|
||||
const next = !autoApproveAllRef.current;
|
||||
|
||||
@@ -161,7 +161,7 @@ describe("formatCompactionStatus", () => {
|
||||
messagesAfter: 300,
|
||||
compacted: true,
|
||||
}),
|
||||
).toBe("Compacted context; message count stayed at 300.");
|
||||
).toBe("Compacted context; message count stayed at 300 messages.");
|
||||
});
|
||||
|
||||
it("reports empty sessions separately", () => {
|
||||
|
||||
@@ -75,9 +75,10 @@ export function useLocalCommandActions(input: {
|
||||
});
|
||||
} else {
|
||||
session.clearEntries();
|
||||
for (const entry of entries) {
|
||||
session.appendEntry(entry);
|
||||
}
|
||||
// replaceEntries rather than appendEntry: appendEntry
|
||||
// stamps unstamped entries with the CURRENT mode, which
|
||||
// would lock hydrated history to the resume-time accent.
|
||||
session.replaceEntries(entries);
|
||||
if (typeof result.currentContextSize === "number") {
|
||||
session.setLastTotalTokens(result.currentContextSize);
|
||||
}
|
||||
|
||||
@@ -23,15 +23,15 @@ describe("getTerminalTheme", () => {
|
||||
});
|
||||
|
||||
describe("theme-aware palette helpers", () => {
|
||||
it("preserves the existing named ANSI colors for dark terminals", () => {
|
||||
expect(getModeAccent("act", "dark")).toBe("cyan");
|
||||
expect(getModeAccent("plan", "dark")).toBe("yellow");
|
||||
expect(getSuccessColor("dark")).toBe("brightGreen");
|
||||
it("uses the brand accent colors for dark terminals", () => {
|
||||
expect(getModeAccent("act", "dark")).toBe("#79b8ff");
|
||||
expect(getModeAccent("plan", "dark")).toBe("#ffea7f");
|
||||
expect(getSuccessColor("dark")).toBe("#99e89b");
|
||||
});
|
||||
|
||||
it("uses darker accents on light terminals", () => {
|
||||
expect(getModeAccent("act", "light")).toBe("#0969da");
|
||||
expect(getModeAccent("plan", "light")).toBe("#9a6700");
|
||||
expect(getModeAccent("act", "light")).toBe("#0f72cb");
|
||||
expect(getModeAccent("plan", "light")).toBe("#867100");
|
||||
expect(getSuccessColor("light")).toBe("#116329");
|
||||
});
|
||||
});
|
||||
|
||||
+50
-17
@@ -1,9 +1,9 @@
|
||||
export const palette = {
|
||||
act: "cyan",
|
||||
plan: "yellow",
|
||||
selection: "cyan",
|
||||
act: "#79b8ff",
|
||||
plan: "#ffea7f",
|
||||
selection: "#79b8ff",
|
||||
error: "red",
|
||||
success: "brightGreen",
|
||||
success: "#99e89b",
|
||||
muted: "gray",
|
||||
textOnSelection: "black",
|
||||
} as const;
|
||||
@@ -16,9 +16,11 @@ export const themePalette = {
|
||||
plan: palette.plan,
|
||||
success: palette.success,
|
||||
},
|
||||
// Same OKLCH hues as the dark accents, darkened to hold >=4.5:1 contrast
|
||||
// on white so the plan/act identity carries across themes.
|
||||
light: {
|
||||
act: "#0969da",
|
||||
plan: "#9a6700",
|
||||
act: "#0f72cb",
|
||||
plan: "#867100",
|
||||
success: "#116329",
|
||||
},
|
||||
} as const;
|
||||
@@ -29,7 +31,7 @@ export const diffPalettes = {
|
||||
removedBg: "#4d1a1a",
|
||||
addedLineNumberBg: "#1a4d1a",
|
||||
removedLineNumberBg: "#4d1a1a",
|
||||
addedSignColor: "#22c55e",
|
||||
addedSignColor: "#99e89b",
|
||||
removedSignColor: "#ef4444",
|
||||
lineNumberFg: "#888888",
|
||||
},
|
||||
@@ -75,8 +77,8 @@ export function getSuccessColor(theme: TerminalTheme = "dark"): string {
|
||||
// overshoot.
|
||||
// 3. On dark themes, raise L (lighten). On light themes, lower L (darken).
|
||||
// 4. Nudge the a/b chromatic channels by CHROMA_NUDGE toward the mode's
|
||||
// accent color. For plan (warm/yellow): +a, +b. For act (cool/cyan):
|
||||
// -a, +b. At 0.003 this is ~10x below OKLAB's just-noticeable-difference
|
||||
// accent color. For plan (warm/yellow): +a, +b. For act (cool/blue):
|
||||
// -a, -b. At 0.003 this is ~10x below OKLAB's just-noticeable-difference
|
||||
// threshold (~0.03), so it registers as a "feel" rather than visible color.
|
||||
//
|
||||
// Sample outputs on common terminals (act mode / plan mode bg):
|
||||
@@ -131,22 +133,53 @@ export function getDefaultForeground(
|
||||
return isLightTheme(terminalBg) ? "#1a1a1a" : undefined;
|
||||
}
|
||||
|
||||
export function getModeInputBackground(
|
||||
mode: string,
|
||||
function liftedFromTerminalBg(
|
||||
terminalBg: string | null,
|
||||
baseLift: number,
|
||||
nudgeA: number,
|
||||
nudgeB: number,
|
||||
): string {
|
||||
const hex = normalizeHex(terminalBg) ?? "#000000";
|
||||
const base = hexToOklab(hex);
|
||||
const light = base.L > LIGHT_THEME_THRESHOLD;
|
||||
const lift = BASE_LIFT / (1 + (light ? 1 - base.L : base.L) * LIFT_DAMPING);
|
||||
const warm = mode === "plan";
|
||||
const lift = baseLift / (1 + (light ? 1 - base.L : base.L) * LIFT_DAMPING);
|
||||
return oklabToHex(
|
||||
base.L + (light ? -lift : lift),
|
||||
base.a + (warm ? CHROMA_NUDGE : -CHROMA_NUDGE),
|
||||
base.b + CHROMA_NUDGE,
|
||||
base.a + nudgeA,
|
||||
base.b + nudgeB,
|
||||
);
|
||||
}
|
||||
|
||||
export function getModeInputBackground(
|
||||
mode: string,
|
||||
terminalBg: string | null,
|
||||
): string {
|
||||
const warm = mode === "plan";
|
||||
return liftedFromTerminalBg(
|
||||
terminalBg,
|
||||
BASE_LIFT,
|
||||
warm ? CHROMA_NUDGE : -CHROMA_NUDGE,
|
||||
warm ? CHROMA_NUDGE : -CHROMA_NUDGE,
|
||||
);
|
||||
}
|
||||
|
||||
// The `─` rules framing the input field are thin foreground strokes rather
|
||||
// than filled cells, so they need a much larger lift than a background tint
|
||||
// to register at the same perceptual weight — this lands them around mid-gray
|
||||
// on both black and white terminals. They stay neutral (no mode chroma) so
|
||||
// the frame doesn't shift color when toggling plan/act.
|
||||
const RULE_BASE_LIFT = 0.5;
|
||||
|
||||
export function getInputRuleColor(terminalBg: string | null): string {
|
||||
return liftedFromTerminalBg(terminalBg, RULE_BASE_LIFT, 0, 0);
|
||||
}
|
||||
|
||||
// User message bubbles stay neutral (no mode chroma) so the transcript reads
|
||||
// as history rather than tracking whichever mode is currently active.
|
||||
export function getUserMessageBackground(terminalBg: string | null): string {
|
||||
return liftedFromTerminalBg(terminalBg, BASE_LIFT, 0, 0);
|
||||
}
|
||||
|
||||
export function getModeInputForeground(
|
||||
mode: string,
|
||||
terminalBg: string | null,
|
||||
@@ -157,7 +190,7 @@ export function getModeInputForeground(
|
||||
return oklabToHex(
|
||||
base.L,
|
||||
base.a + (warm ? CHROMA_NUDGE : -CHROMA_NUDGE),
|
||||
base.b + CHROMA_NUDGE,
|
||||
base.b + (warm ? CHROMA_NUDGE : -CHROMA_NUDGE),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,7 +204,7 @@ export function getModeInputPlaceholder(
|
||||
return oklabToHex(
|
||||
base.L,
|
||||
base.a + (warm ? CHROMA_NUDGE * 2 : -CHROMA_NUDGE * 2),
|
||||
base.b + CHROMA_NUDGE * 2,
|
||||
base.b + (warm ? CHROMA_NUDGE * 2 : -CHROMA_NUDGE * 2),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -401,9 +401,10 @@ function App(props: TuiProps) {
|
||||
if (lastEntry && lastEntry.kind === "user_submitted") {
|
||||
entries.pop();
|
||||
}
|
||||
for (const entry of entries) {
|
||||
session.appendEntry(entry);
|
||||
}
|
||||
// replaceEntries rather than appendEntry: appendEntry stamps
|
||||
// unstamped entries with the CURRENT mode, which would lock
|
||||
// hydrated history to the restore-time accent.
|
||||
session.replaceEntries(entries);
|
||||
session.setHasSubmitted(entries.length > 0);
|
||||
setAppView(entries.length > 0 ? "chat" : "home");
|
||||
populateInputRef.current(picked.fullText);
|
||||
|
||||
@@ -25,7 +25,7 @@ import type {
|
||||
} from "./interactive-config";
|
||||
import type { InteractiveSlashCommand } from "./interactive-welcome";
|
||||
|
||||
export type ChatEntry =
|
||||
export type ChatEntry = (
|
||||
| { kind: "user"; text: string }
|
||||
| { kind: "assistant_text"; text: string; streaming: boolean }
|
||||
| { kind: "reasoning"; text: string; streaming: boolean }
|
||||
@@ -52,7 +52,17 @@ export type ChatEntry =
|
||||
cost: number;
|
||||
elapsed: string;
|
||||
iterations: number;
|
||||
};
|
||||
}
|
||||
) & {
|
||||
/**
|
||||
* Agent mode active when the entry was produced. Stamped by appendEntry
|
||||
* (live sessions) and hydrateSessionMessages (resumed sessions) so the
|
||||
* transcript renders each entry with the accent of its own mode instead
|
||||
* of retinting everything to the current mode. Absent on entries from
|
||||
* transcripts that predate mode stamping.
|
||||
*/
|
||||
mode?: AgentMode;
|
||||
};
|
||||
|
||||
export interface InteractiveTurnResult {
|
||||
usage: {
|
||||
@@ -80,6 +90,7 @@ export interface ResumedSessionResult {
|
||||
export interface InteractiveCompactionResult {
|
||||
messagesBefore: number;
|
||||
messagesAfter: number;
|
||||
workingContextMessagesAfter?: number;
|
||||
compacted: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { InteractiveCompactionResult } from "../types";
|
||||
|
||||
function formatMessageCount(count: number): string {
|
||||
return `${count} ${count === 1 ? "message" : "messages"}`;
|
||||
}
|
||||
|
||||
export function formatCompactionStatus(
|
||||
result: InteractiveCompactionResult,
|
||||
): string {
|
||||
@@ -9,8 +13,11 @@ export function formatCompactionStatus(
|
||||
if (!result.compacted) {
|
||||
return "No compaction needed.";
|
||||
}
|
||||
if (result.messagesBefore === result.messagesAfter) {
|
||||
return `Compacted context; message count stayed at ${result.messagesAfter}.`;
|
||||
if (typeof result.workingContextMessagesAfter === "number") {
|
||||
return `Compacted working context to ${formatMessageCount(result.workingContextMessagesAfter)}; saved history remains ${formatMessageCount(result.messagesAfter)}.`;
|
||||
}
|
||||
return `Compacted ${result.messagesBefore} messages to ${result.messagesAfter}.`;
|
||||
if (result.messagesBefore === result.messagesAfter) {
|
||||
return `Compacted context; message count stayed at ${formatMessageCount(result.messagesAfter)}.`;
|
||||
}
|
||||
return `Compacted ${formatMessageCount(result.messagesBefore)} to ${formatMessageCount(result.messagesAfter)}.`;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ describe("hydrateSessionMessages", () => {
|
||||
] as Message[];
|
||||
|
||||
expect(hydrateSessionMessages(messages)).toEqual([
|
||||
{ kind: "user_submitted", text: "lets do it" },
|
||||
{ kind: "user_submitted", text: "lets do it", mode: "plan" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,116 @@ describe("hydrateSessionMessages", () => {
|
||||
] as Message[];
|
||||
|
||||
expect(hydrateSessionMessages(messages)).toEqual([
|
||||
{ kind: "assistant_text", text: "On it.", streaming: false },
|
||||
{ kind: "assistant_text", text: "On it.", streaming: false, mode: "act" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("stamps entries with the mode of the user message that produced them", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "user",
|
||||
content: '<user_input mode="plan">plan this out</user_input>',
|
||||
},
|
||||
{ role: "assistant", content: "Here is the plan." },
|
||||
{
|
||||
role: "user",
|
||||
content: '<user_input mode="act">do it</user_input>',
|
||||
},
|
||||
{ role: "assistant", content: "Doing it." },
|
||||
] as Message[];
|
||||
|
||||
expect(hydrateSessionMessages(messages)).toEqual([
|
||||
{ kind: "user_submitted", text: "plan this out", mode: "plan" },
|
||||
{
|
||||
kind: "assistant_text",
|
||||
text: "Here is the plan.",
|
||||
streaming: false,
|
||||
mode: "plan",
|
||||
},
|
||||
{ kind: "user_submitted", text: "do it", mode: "act" },
|
||||
{
|
||||
kind: "assistant_text",
|
||||
text: "Doing it.",
|
||||
streaming: false,
|
||||
mode: "act",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("switches to act mode after a switch_to_act_mode tool call", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "user",
|
||||
content: '<user_input mode="plan">plan then build</user_input>',
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "Plan looks good, switching." },
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-1",
|
||||
name: "switch_to_act_mode",
|
||||
input: {},
|
||||
},
|
||||
{ type: "text", text: "Building now." },
|
||||
],
|
||||
},
|
||||
] as Message[];
|
||||
|
||||
expect(hydrateSessionMessages(messages)).toEqual([
|
||||
{ kind: "user_submitted", text: "plan then build", mode: "plan" },
|
||||
{
|
||||
kind: "assistant_text",
|
||||
text: "Plan looks good, switching.",
|
||||
streaming: false,
|
||||
mode: "plan",
|
||||
},
|
||||
{
|
||||
kind: "tool_call",
|
||||
toolName: "switch_to_act_mode",
|
||||
inputSummary: expect.any(String),
|
||||
rawInput: {},
|
||||
streaming: false,
|
||||
mode: "plan",
|
||||
},
|
||||
{
|
||||
kind: "assistant_text",
|
||||
text: "Building now.",
|
||||
streaming: false,
|
||||
mode: "act",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("strips mode switch notices from displayed user text", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
'<user_input mode="plan"><mode_notice>The user switched from act mode to plan mode before sending this message.</mode_notice>\nare you okay?</user_input>',
|
||||
},
|
||||
] as Message[];
|
||||
|
||||
expect(hydrateSessionMessages(messages)).toEqual([
|
||||
{ kind: "user_submitted", text: "are you okay?", mode: "plan" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves mode undefined for transcripts without user_input wrappers", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: "plain old message" },
|
||||
{ role: "assistant", content: "reply" },
|
||||
] as Message[];
|
||||
|
||||
expect(hydrateSessionMessages(messages)).toEqual([
|
||||
{ kind: "user_submitted", text: "plain old message", mode: undefined },
|
||||
{
|
||||
kind: "assistant_text",
|
||||
text: "reply",
|
||||
streaming: false,
|
||||
mode: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { formatDisplayUserInput, type Message } from "@cline/shared";
|
||||
import type { AgentMode } from "@cline/core";
|
||||
import {
|
||||
formatDisplayUserInput,
|
||||
type Message,
|
||||
parseUserInputMode,
|
||||
} from "@cline/shared";
|
||||
import { ACT_MODE_CONTINUATION_PROMPT } from "../../runtime/interactive/mode";
|
||||
import { formatToolInput } from "../../utils/helpers";
|
||||
import type { ChatEntry } from "../types";
|
||||
@@ -37,6 +42,12 @@ function stringifyToolResult(
|
||||
export function hydrateSessionMessages(messages: Message[]): ChatEntry[] {
|
||||
const entries: ChatEntry[] = [];
|
||||
const toolUseMap = new Map<string, number>();
|
||||
// Mode each entry was produced in, recovered from <user_input mode="...">
|
||||
// wrappers and switch_to_act_mode tool calls as we walk the transcript.
|
||||
// Stays undefined for transcripts with no mode markers (pre-wrapper
|
||||
// builds, or transcripts laundered by older builds that stripped the
|
||||
// wrappers on session restarts).
|
||||
let mode: AgentMode | undefined;
|
||||
|
||||
for (const msg of messages as PersistedMessage[]) {
|
||||
const displayRole = getDisplayRole(msg);
|
||||
@@ -46,15 +57,17 @@ export function hydrateSessionMessages(messages: Message[]): ChatEntry[] {
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
if (msg.role === "user") {
|
||||
mode = parseUserInputMode(msg.content) ?? mode;
|
||||
const text = formatDisplayUserInput(msg.content);
|
||||
if (text && !isSyntheticUserText(text)) {
|
||||
entries.push({ kind: "user_submitted", text });
|
||||
entries.push({ kind: "user_submitted", text, mode });
|
||||
}
|
||||
} else {
|
||||
entries.push({
|
||||
kind: "assistant_text",
|
||||
text: msg.content,
|
||||
streaming: false,
|
||||
mode,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
@@ -71,6 +84,7 @@ export function hydrateSessionMessages(messages: Message[]): ChatEntry[] {
|
||||
kind: "assistant_text",
|
||||
text: block.text,
|
||||
streaming: false,
|
||||
mode,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
@@ -81,6 +95,7 @@ export function hydrateSessionMessages(messages: Message[]): ChatEntry[] {
|
||||
kind: "reasoning",
|
||||
text: block.thinking,
|
||||
streaming: false,
|
||||
mode,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -96,8 +111,14 @@ export function hydrateSessionMessages(messages: Message[]): ChatEntry[] {
|
||||
inputSummary: formatToolInput(block.name, block.input),
|
||||
rawInput: block.input,
|
||||
streaming: false,
|
||||
mode,
|
||||
});
|
||||
toolUseMap.set(block.id, entries.length - 1);
|
||||
// The switch tool flips the session to act mid-run; everything
|
||||
// after it was produced in act mode.
|
||||
if (block.name === "switch_to_act_mode") {
|
||||
mode = "act";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -123,9 +144,10 @@ export function hydrateSessionMessages(messages: Message[]): ChatEntry[] {
|
||||
|
||||
if (msg.role === "user" && userTextParts.length > 0) {
|
||||
const combined = userTextParts.join("\n");
|
||||
mode = parseUserInputMode(combined) ?? mode;
|
||||
const text = formatDisplayUserInput(combined);
|
||||
if (text && !isSyntheticUserText(text)) {
|
||||
entries.push({ kind: "user_submitted", text });
|
||||
entries.push({ kind: "user_submitted", text, mode });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,4 +49,33 @@ describe("getSyntaxStyle", () => {
|
||||
|
||||
expect(style?.fg?.toInts()).toEqual([26, 26, 26, 255]);
|
||||
});
|
||||
|
||||
it("tints markdown accents by mode", () => {
|
||||
// act #79b8ff vs plan #ffea7f (dark theme accents)
|
||||
expect(
|
||||
getSyntaxStyle("dark", "act").getStyle("markup.heading")?.fg?.toInts(),
|
||||
).toEqual([0x79, 0xb8, 0xff, 255]);
|
||||
expect(
|
||||
getSyntaxStyle("dark", "plan").getStyle("markup.heading")?.fg?.toInts(),
|
||||
).toEqual([0xff, 0xea, 0x7f, 255]);
|
||||
expect(
|
||||
getSyntaxStyle("dark", "plan").getStyle("markup.link")?.fg?.toInts(),
|
||||
).toEqual([0xff, 0xea, 0x7f, 255]);
|
||||
});
|
||||
|
||||
it("tints light-theme markdown accents by mode", () => {
|
||||
// act #0f72cb vs plan #867100 (light theme accents)
|
||||
expect(
|
||||
getSyntaxStyle("light", "act").getStyle("markup.heading")?.fg?.toInts(),
|
||||
).toEqual([0x0f, 0x72, 0xcb, 255]);
|
||||
expect(
|
||||
getSyntaxStyle("light", "plan").getStyle("markup.heading")?.fg?.toInts(),
|
||||
).toEqual([0x86, 0x71, 0x00, 255]);
|
||||
});
|
||||
|
||||
it("keeps code token colors constant across modes", () => {
|
||||
expect(getSyntaxStyle("dark", "plan").getStyle("keyword")).toEqual(
|
||||
getSyntaxStyle("dark", "act").getStyle("keyword"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { RGBA, type StyleDefinition, SyntaxStyle } from "@opentui/core";
|
||||
import type { TerminalTheme } from "../palette";
|
||||
import { type TerminalTheme, themePalette } from "../palette";
|
||||
|
||||
const instances: Record<TerminalTheme, SyntaxStyle | null> = {
|
||||
dark: null,
|
||||
light: null,
|
||||
};
|
||||
// Markdown's prominent elements (headings, bold, list markers, links) take
|
||||
// the accent of the mode the content was produced in, so assistant output
|
||||
// reads plan-yellow or act-blue alongside the rest of the transcript.
|
||||
export type SyntaxAccentMode = "act" | "plan";
|
||||
|
||||
const instances = new Map<string, SyntaxStyle>();
|
||||
|
||||
interface SyntaxColors {
|
||||
keyword: string;
|
||||
@@ -22,34 +24,34 @@ interface SyntaxColors {
|
||||
attribute: string;
|
||||
escape: string;
|
||||
markdownCode: string;
|
||||
markdownHeading: string;
|
||||
markdownMuted: string;
|
||||
markdownLink: string;
|
||||
markdownItalic: string;
|
||||
markdownDefault?: string;
|
||||
}
|
||||
|
||||
// Dark syntax colors are a pastel family harmonized with the brand accents
|
||||
// (act #79b8ff, plan #ffea7f, success #99e89b): every hue sits near the same
|
||||
// OKLCH lightness/chroma weight (~L 0.78, C 0.11) so code blocks feel like
|
||||
// part of the same palette instead of a bolted-on editor theme.
|
||||
const syntaxColors: Record<TerminalTheme, SyntaxColors> = {
|
||||
dark: {
|
||||
keyword: "#c678dd",
|
||||
operator: "#56b6c2",
|
||||
type: "#e5c07b",
|
||||
functionName: "#61afef",
|
||||
variable: "#e06c75",
|
||||
string: "#98c379",
|
||||
number: "#d19a66",
|
||||
keyword: "#d7a0e3",
|
||||
operator: "#9bbbdd",
|
||||
type: "#dfca7d",
|
||||
functionName: themePalette.dark.act,
|
||||
variable: "#ee939b",
|
||||
string: "#99e89b",
|
||||
number: "#f0ad7f",
|
||||
comment: "#5c6370",
|
||||
punctuation: "#abb2bf",
|
||||
property: "#e06c75",
|
||||
constant: "#d19a66",
|
||||
tag: "#e06c75",
|
||||
attribute: "#d19a66",
|
||||
escape: "#56b6c2",
|
||||
markdownCode: "#98c379",
|
||||
markdownHeading: "#56b6c2",
|
||||
property: "#ee939b",
|
||||
constant: "#f0ad7f",
|
||||
tag: "#ee939b",
|
||||
attribute: "#f0ad7f",
|
||||
escape: "#9bbbdd",
|
||||
markdownCode: "#99e89b",
|
||||
markdownMuted: "#808080",
|
||||
markdownLink: "#56b6c2",
|
||||
markdownItalic: "#e5c07b",
|
||||
markdownItalic: "#dfca7d",
|
||||
},
|
||||
light: {
|
||||
keyword: "#cf222e",
|
||||
@@ -67,9 +69,7 @@ const syntaxColors: Record<TerminalTheme, SyntaxColors> = {
|
||||
attribute: "#0550ae",
|
||||
escape: "#0550ae",
|
||||
markdownCode: "#116329",
|
||||
markdownHeading: "#0969da",
|
||||
markdownMuted: "#6e7781",
|
||||
markdownLink: "#0969da",
|
||||
markdownItalic: "#8250df",
|
||||
markdownDefault: "#1a1a1a",
|
||||
},
|
||||
@@ -91,16 +91,16 @@ function italic(hex: string): StyleDefinition {
|
||||
return { fg: color(hex), italic: true };
|
||||
}
|
||||
|
||||
function underline(hex: string): StyleDefinition {
|
||||
return { fg: color(hex), underline: true };
|
||||
}
|
||||
|
||||
function buildSyntaxStyle(theme: TerminalTheme): SyntaxStyle {
|
||||
function buildSyntaxStyle(
|
||||
theme: TerminalTheme,
|
||||
mode: SyntaxAccentMode,
|
||||
): SyntaxStyle {
|
||||
const colors = syntaxColors[theme];
|
||||
const markdownHeading = color(colors.markdownHeading);
|
||||
const accent = color(themePalette[theme][mode]);
|
||||
const markdownHeading = accent;
|
||||
const markdownCode = color(colors.markdownCode);
|
||||
const markdownMuted = color(colors.markdownMuted);
|
||||
const markdownLink = color(colors.markdownLink);
|
||||
const markdownLink = accent;
|
||||
|
||||
return SyntaxStyle.fromStyles({
|
||||
...(colors.markdownDefault ? { default: fg(colors.markdownDefault) } : {}),
|
||||
@@ -145,10 +145,19 @@ function buildSyntaxStyle(theme: TerminalTheme): SyntaxStyle {
|
||||
"markup.link.url": { fg: markdownLink, underline: true },
|
||||
label: { fg: markdownLink },
|
||||
conceal: { fg: markdownMuted },
|
||||
"string.special.url": underline(colors.markdownLink),
|
||||
"string.special.url": { fg: markdownLink, underline: true },
|
||||
});
|
||||
}
|
||||
|
||||
export function getSyntaxStyle(theme: TerminalTheme = "dark"): SyntaxStyle {
|
||||
return (instances[theme] ??= buildSyntaxStyle(theme));
|
||||
export function getSyntaxStyle(
|
||||
theme: TerminalTheme = "dark",
|
||||
mode: SyntaxAccentMode = "act",
|
||||
): SyntaxStyle {
|
||||
const key = `${theme}:${mode}`;
|
||||
let style = instances.get(key);
|
||||
if (!style) {
|
||||
style = buildSyntaxStyle(theme, mode);
|
||||
instances.set(key, style);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
useTerminalTheme,
|
||||
} from "../hooks/use-terminal-background";
|
||||
import {
|
||||
getInputRuleColor,
|
||||
getModeAccent,
|
||||
getModeInputBackground,
|
||||
getModeInputForeground,
|
||||
@@ -76,6 +77,7 @@ export function ChatView(props: {
|
||||
const terminalTheme = useTerminalTheme();
|
||||
const accent = getModeAccent(session.uiMode, terminalTheme);
|
||||
const inputBackground = getModeInputBackground(session.uiMode, terminalBg);
|
||||
const inputRuleColor = getInputRuleColor(terminalBg);
|
||||
const inputForeground = getModeInputForeground(session.uiMode, terminalBg);
|
||||
const inputPlaceholder = getModeInputPlaceholder(session.uiMode, terminalBg);
|
||||
const placeholder =
|
||||
@@ -123,10 +125,10 @@ export function ChatView(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
<box marginBottom={1}>
|
||||
<box>
|
||||
<InputBar
|
||||
accent={accent}
|
||||
inputBackground={inputBackground}
|
||||
ruleColor={inputRuleColor}
|
||||
inputForeground={inputForeground}
|
||||
inputPlaceholder={inputPlaceholder}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -721,7 +721,7 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
|
||||
return (
|
||||
<box flexDirection="column" paddingX={1}>
|
||||
<text fg="cyan">
|
||||
<text fg={palette.act}>
|
||||
<strong>Settings</strong>
|
||||
</text>
|
||||
|
||||
@@ -793,7 +793,7 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<text fg={isSel ? "cyan" : undefined}>{pfx}Provider</text>
|
||||
<text fg={isSel ? palette.act : undefined}>{pfx}Provider</text>
|
||||
<text fg="white">{props.providerDisplayName}</text>
|
||||
</box>
|
||||
);
|
||||
@@ -804,7 +804,7 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<text fg={isSel ? "cyan" : undefined}>{pfx}Model</text>
|
||||
<text fg={isSel ? palette.act : undefined}>{pfx}Model</text>
|
||||
<text fg="white">{displayName}</text>
|
||||
</box>
|
||||
);
|
||||
@@ -833,7 +833,7 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<text fg={isSel ? "cyan" : undefined}>
|
||||
<text fg={isSel ? palette.act : undefined}>
|
||||
{pfx}
|
||||
{row.label}
|
||||
</text>
|
||||
@@ -866,7 +866,7 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
: enabledState === "partial"
|
||||
? "yellow"
|
||||
: isSel
|
||||
? "cyan"
|
||||
? palette.act
|
||||
: "gray";
|
||||
return (
|
||||
<box
|
||||
@@ -886,7 +886,7 @@ export function ConfigPanelContent(props: ConfigPanelProps) {
|
||||
}
|
||||
case "mcp-manager":
|
||||
return (
|
||||
<text key={absIdx} fg={isSel ? "cyan" : "gray"}>
|
||||
<text key={absIdx} fg={isSel ? palette.act : "gray"}>
|
||||
{pfx}Manage MCP Servers...
|
||||
</text>
|
||||
);
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
} from "../hooks/use-terminal-background";
|
||||
import {
|
||||
getDefaultForeground,
|
||||
getInputRuleColor,
|
||||
getModeAccent,
|
||||
getModeInputBackground,
|
||||
getModeInputForeground,
|
||||
getModeInputPlaceholder,
|
||||
} from "../palette";
|
||||
@@ -69,7 +69,7 @@ export function HomeView(props: {
|
||||
const terminalTheme = useTerminalTheme();
|
||||
const defaultFg = getDefaultForeground(terminalBg);
|
||||
const accent = getModeAccent(session.uiMode, terminalTheme);
|
||||
const inputBackground = getModeInputBackground(session.uiMode, terminalBg);
|
||||
const inputRuleColor = getInputRuleColor(terminalBg);
|
||||
const inputForeground = getModeInputForeground(session.uiMode, terminalBg);
|
||||
const inputPlaceholder = getModeInputPlaceholder(session.uiMode, terminalBg);
|
||||
const placeholder =
|
||||
@@ -80,7 +80,7 @@ export function HomeView(props: {
|
||||
props.autocomplete?.mode && props.autocomplete.options.length > 0;
|
||||
const contentWidth = Math.min(width, HOME_VIEW_MAX_WIDTH);
|
||||
const hasTypedInput = inputValue.trim().length > 0;
|
||||
const inputStartX = Math.floor((width - contentWidth) / 2) + 4;
|
||||
const inputStartX = Math.floor((width - contentWidth) / 2) + 2;
|
||||
const clamp = (value: number, min: number, max: number) =>
|
||||
Math.max(min, Math.min(max, value));
|
||||
const trackedCursorX = hasTypedInput
|
||||
@@ -116,7 +116,7 @@ export function HomeView(props: {
|
||||
<box flexDirection="column" width={contentWidth} flexShrink={0}>
|
||||
<InputBar
|
||||
accent={accent}
|
||||
inputBackground={inputBackground}
|
||||
ruleColor={inputRuleColor}
|
||||
inputForeground={inputForeground}
|
||||
inputPlaceholder={inputPlaceholder}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -58,6 +58,7 @@ import { useOnboardingKeyboard } from "./keyboard";
|
||||
import {
|
||||
CLINE_PASS_SUBSCRIPTION_OPTIONS,
|
||||
type ClinePassSubscriptionStatus,
|
||||
DEFAULT_THINKING_LEVEL_INDEX,
|
||||
getMainMenuOptions,
|
||||
type ModelEntry,
|
||||
type OnboardingResult,
|
||||
@@ -237,7 +238,9 @@ export function useOnboardingController(props: OnboardingControllerProps) {
|
||||
}, []);
|
||||
|
||||
// Thinking level
|
||||
const [thinkingSelected, setThinkingSelected] = useState(0);
|
||||
const [thinkingSelected, setThinkingSelected] = useState(
|
||||
DEFAULT_THINKING_LEVEL_INDEX,
|
||||
);
|
||||
const [selectedModelName, setSelectedModelName] = useState("");
|
||||
const [selectedModelId, setSelectedModelId] = useState("");
|
||||
const [selectedThinking, setSelectedThinking] = useState(false);
|
||||
@@ -641,7 +644,7 @@ export function useOnboardingController(props: OnboardingControllerProps) {
|
||||
const entry = modelEntries.find((m) => m.id === modelId);
|
||||
if (entry?.supportsReasoning) {
|
||||
setSelectedModelName(entry.name);
|
||||
setThinkingSelected(0);
|
||||
setThinkingSelected(DEFAULT_THINKING_LEVEL_INDEX);
|
||||
setStep("thinking_level");
|
||||
} else {
|
||||
setStep("done");
|
||||
@@ -691,7 +694,7 @@ export function useOnboardingController(props: OnboardingControllerProps) {
|
||||
setSelectedModelId(modelId);
|
||||
if (clineModelReasoningIds.has(modelId)) {
|
||||
setSelectedModelName(modelName);
|
||||
setThinkingSelected(0);
|
||||
setThinkingSelected(DEFAULT_THINKING_LEVEL_INDEX);
|
||||
setStep("thinking_level");
|
||||
} else {
|
||||
setStep("done");
|
||||
|
||||
@@ -30,6 +30,10 @@ export const THINKING_LEVELS: {
|
||||
{ value: "xhigh", label: "Extra High", desc: "Maximum reasoning" },
|
||||
];
|
||||
|
||||
export const DEFAULT_THINKING_LEVEL_INDEX = THINKING_LEVELS.findIndex(
|
||||
(l) => l.value === "medium",
|
||||
);
|
||||
|
||||
export interface MenuOption {
|
||||
label: string;
|
||||
value: string;
|
||||
|
||||
@@ -384,7 +384,7 @@ export function OnboardingCodexCliScreen(props: {
|
||||
<text fg="yellow">Codex CLI was not found</text>
|
||||
<text fg="gray">{props.status.reason}</text>
|
||||
<text fg="gray">Install Codex CLI from:</text>
|
||||
<text fg="cyan" selectable>
|
||||
<text fg={palette.act} selectable>
|
||||
{CODEX_CLI_INSTALL_URL}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatDisplayUserInput } from "@cline/shared";
|
||||
import type {
|
||||
WebviewActionSessionSummary,
|
||||
WebviewChatMessage,
|
||||
@@ -213,10 +214,16 @@ export function mapHistoryToWebviewMessages(
|
||||
const currentToolBlockIndexes = new Map<string, number>();
|
||||
let reasoningRedacted = false;
|
||||
|
||||
// Persisted user text arrives raw, including runtime-generated
|
||||
// <user_input>/<mode_notice> wrappers -- format at this display
|
||||
// boundary so the webview never renders them.
|
||||
const displayText = (text: string): string =>
|
||||
role === "user" ? formatDisplayUserInput(text) : text;
|
||||
|
||||
const contentParts = historyContentParts(record.content);
|
||||
if (contentParts.length === 0) {
|
||||
const text = stringifyContent(record.content ?? record.text ?? record);
|
||||
pushTextBlock(blocks, textParts, messageKey, 0, text);
|
||||
pushTextBlock(blocks, textParts, messageKey, 0, displayText(text));
|
||||
}
|
||||
|
||||
for (const [partIndex, part] of contentParts.entries()) {
|
||||
@@ -227,7 +234,7 @@ export function mapHistoryToWebviewMessages(
|
||||
textParts,
|
||||
messageKey,
|
||||
partIndex,
|
||||
asString(part.text) ?? asString(part.content) ?? "",
|
||||
displayText(asString(part.text) ?? asString(part.content) ?? ""),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"apps/cli": {
|
||||
"name": "@cline/cli",
|
||||
"version": "3.0.36",
|
||||
"version": "3.0.37",
|
||||
"bin": {
|
||||
"cline": "src/index.ts",
|
||||
},
|
||||
@@ -616,7 +616,7 @@
|
||||
},
|
||||
"sdk/packages/agents": {
|
||||
"name": "@cline/agents",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"dependencies": {
|
||||
"@cline/llms": "workspace:*",
|
||||
"@cline/shared": "workspace:*",
|
||||
@@ -625,7 +625,7 @@
|
||||
},
|
||||
"sdk/packages/core": {
|
||||
"name": "@cline/core",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"dependencies": {
|
||||
"@cline/agents": "workspace:*",
|
||||
"@cline/llms": "workspace:*",
|
||||
@@ -663,7 +663,7 @@
|
||||
},
|
||||
"sdk/packages/llms": {
|
||||
"name": "@cline/llms",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "^4.0.89",
|
||||
"@ai-sdk/anthropic": "^3.0.68",
|
||||
@@ -697,14 +697,14 @@
|
||||
},
|
||||
"sdk/packages/sdk": {
|
||||
"name": "@cline/sdk",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"dependencies": {
|
||||
"@cline/core": "workspace:*",
|
||||
},
|
||||
},
|
||||
"sdk/packages/shared": {
|
||||
"name": "@cline/shared",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"dependencies": {
|
||||
"aws4fetch": "^1.0.20",
|
||||
"jsonrepair": "^3.13.2",
|
||||
@@ -741,27 +741,27 @@
|
||||
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.123", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.88", "@ai-sdk/openai": "3.0.76", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-lEvz/jOJksH+KYIciVbX0iV7/80I+sG1Er7pullu4fh1fEpbhQC+o2wNwp2H+UzglJehE0uo93erd2RgQtEX6g=="],
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.125", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.90", "@ai-sdk/openai": "3.0.78", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7C+ud1t6biknsr+fSOSOeFJXYrPAjOouSUPu2ZJ94pXywI2W7Y3E3Rn0s2/NYViknRgY0UBcLY9GR1KKwhf1Yg=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.88", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2IqL7Auu0XW3BnulsHJUb7C4lQKXmWQ7MU2Osq+tfJ79in2P0Li2sX2j45HJMenGJL/JhSyunsYhyv63qMRxWQ=="],
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.90", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7K51KyEyyQPcvBdrxB+TPmHzmuXPhyDNwTZuqTcsYp2GbB0E2SzoM1qEJ4qLb1H2Q8xx5SkkOhATmDxC4oVo9A=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.138", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mYvN5va363hgArapRvXcY6ckV5BFvF4bnimT/tasTMBg2FDmA5S7+jltckGuiaUAYa1vKE9fA06AvmBVXaZYrA=="],
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.140", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4VyQTHHqfZ0qI1fCcurJgYgzVDgLfV4svzBMOHGGxCu4srFkAn4ZmwFj2M0J00kNt7QLsjOtZ6PhueupeIJSjA=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.85", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-x6JBywNpZXZZnhxJbZZFut3VhFPL4jq0kh2yWVMvCSXlqT39qaS6535WA23hxqD2Ff90wnAyuPJiap8yWsqKww=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.86", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NZoFXTdK2/C7VuuGhAatoQ/wSiIvxVzw4Xr0AvcD3cotS5+iP/y0eN1J12pvFSZv+nAHa2Xl7xvFHDadzeU90g=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.151", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.88", "@ai-sdk/google": "3.0.85", "@ai-sdk/openai-compatible": "2.0.53", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GV6kwRCCQxEq9jzRX6uBVUOzzJLnt0KeZmnyP6YUHdOZvBrgIn9M/EnUJStNJHV2tk0Vh1Pp5duoalaXc6lKuQ=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.153", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.90", "@ai-sdk/google": "3.0.86", "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZPPjMRkTmdRzDSjxlZFJXskuAtyTYffnPVQjbWDTNqiUTGOHQaxxRuqibepJqQ19pnDe72Yj1r+fL2kNkhml+g=="],
|
||||
|
||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.42", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZlYELoGbNj+SrNm9L1XQSsWkfv3W71j3VEEI32MJwtLaMr0xzJAxC1V/lti+vVHHRUTHkxxp3fjOu9b4MxWe0w=="],
|
||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.43", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FtFcf0eXEm1v8JiDYe4fyQoRz9JmK5/LLH8nawFIttkxc309TsuPNFfwpSgj2Fm5osrBFL49FGPEL9phJVav7A=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.76", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zY4A5gxWFH95jhisbuHG29qqmHPrO6zu9WkGi12SjMbRFDtok9gm676n0+FjTWifHEYoUK1YvR1nOwPorfHThg=="],
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XlRHyAe1zvetAO1lXVQSNy8acsdd+kznTfmedXBCe7Pvu7lEGGePL8iUg/jH2qLqnlrIpYovuCd3jC2hSTOTsA=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SoPSkrL5cbNQnAljRsJ7pOzJ2FmWgnhC0lfFOda873ycCdFJL1A+h3Ib7mX2spcv3XnNaO13y/45/0RyqNWlIQ=="],
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.32", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Kwj499fTcN9bP/AfGoPU7JWIXeP6VZqKI6omsH062c9E2G4gdjeJczkz4z/tYSkzYjLE2AI3DtZbMfs6D7vn2Q=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="],
|
||||
|
||||
"@ai-sdk/react": ["@ai-sdk/react@3.0.215", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.32", "ai": "6.0.213", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-OqGbF5cKZDWT6HXrprGlgzFO1M9RosodE+L5vUknX/Sk1BBsrtHLUR3zNUVYk/nytyLUmagRksOwrvdgOURBVA=="],
|
||||
"@ai-sdk/react": ["@ai-sdk/react@3.0.218", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.33", "ai": "6.0.216", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-gFtotqv2JmJeUIjmMigdIwwUbABXjJb786anZ26AcaLVSC/pKqhhtzYsT0uef5MuEcLquTzBAFtu6SY3Ov2I5Q=="],
|
||||
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
@@ -769,30 +769,28 @@
|
||||
|
||||
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.195", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.195", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.195", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.195", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.195", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.195", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.195", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.195", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.195" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-FVmXu9pvOMbuBKWrF8YsYQdQ/upOpv5rS8lFAnFO5jbyXT/2hN7kEPd2vd2GJpaMvNcO/KptyQUK5AxjjTz3+w=="],
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.196", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.196", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.196", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.196", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.196", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.196", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.196", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.196", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.196" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-yqsp1/04T2/tJ54jz+7YLsTZOPeQ/myUCrv17/IVZ9dbl+izIMP9ULuLpPCH5pj5msXxR80es+hpVgHoFuNm0A=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.195", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WIMM/8HRCLsTDHFTIwQvvE8WCA/oaMJtdQxsP7iNyfzIGwXbuOyU95V8vYIhZfaO2yaSpbBRncunq4CtR5H4ng=="],
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.196", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k1MKRDhSiNKpkTwhtU8QKGzfuRfr3YXS6oqsTuldMROX56L5iMXjzC7AYU1/KmPTeMl3SCQBQeDu0i8ciA0hQA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.195", "", { "os": "darwin", "cpu": "x64" }, "sha512-RY7DB+4LXosE0MJ+XELmakfPrDN1YX4lkk9CTDm28jGCVcESRz9kAEqbyaiC48dZcmN9V1NCLutzINGdcr1TBg=="],
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.196", "", { "os": "darwin", "cpu": "x64" }, "sha512-bBwx/7yKZMQ9NSUt4bg8P+zp6pgd/O/DTkzdqsRIivBvARmwo1QY/2qGrLO8RH0T8CG2lTjFNfDfOlJWbAkvAg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.195", "", { "os": "linux", "cpu": "arm64" }, "sha512-JuIq5Fnz/F1snl0aqi1gcuRZqPWoPNrL9dJ0DuievCxKkO8hnEz/Mmn5Zos7x1X8HE//ZnEvmQXoEQEZXonJew=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.196", "", { "os": "linux", "cpu": "arm64" }, "sha512-fR5fy+pSQSpKZK0zTtAl3LZEGQTuwVK7svutH1bZUS5RGT2HdUWc71oltSZgW4upGaslj+gyusHGJHA5eAc+dw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.195", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZmyBA/AFzhgutcxb7dbhCm6GTjJytwNYXTxJoKE2B3A409WCYccjMqeji6vCMNxyyfylglGo5D8dVMIxW9aoug=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.196", "", { "os": "linux", "cpu": "arm64" }, "sha512-BhLxfx4j6mC3Uzmve1IbhFS1uvNlwATeo6uWyYDOMW4n3XKjiSgjD8bTfkamijrxCMvJo5swLju2y14ayDsokA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.195", "", { "os": "linux", "cpu": "x64" }, "sha512-s1lNi1cL93luoqsItH+fNO4KpIhdkvnVhWGGQUQ/8ftwa2gfmcIQnOg1hG8Ks+KzeD3UUQ8L9YEVHVADnFI/9A=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.196", "", { "os": "linux", "cpu": "x64" }, "sha512-9spZON7/tn0q9J+jICrdfHi7o7Fmjs9pIohCCxL+Yv7HbBXWVtEYJbYipBGLTl8ICG3mgPeEVMNsvOuh/jDTuA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.195", "", { "os": "linux", "cpu": "x64" }, "sha512-nf8Q/LauB+ZOC6QDjxNhbsvwUtYjKYnaWJLTYFwhkmsLujePnety1AtT/1ubaUoq5AM1j297DhMlYTasa79OUA=="],
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.196", "", { "os": "linux", "cpu": "x64" }, "sha512-EOiNbxCXQLYzV7SQhMWkUC1ScWyTw/Qp+JyV2sEmIbzl/e7KMOxNE9J20k+lpPs6CXdxVzuMwH7yAGuDu5y+IQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.195", "", { "os": "win32", "cpu": "arm64" }, "sha512-hbkDE+xPIZzRWm+D+BKrH9uJH6USIZdDIlsyrIlGi3JFHoieYoA1vdUNyldSS9+F3ZqQtfPjr2Qy08IVB6akYA=="],
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.196", "", { "os": "win32", "cpu": "arm64" }, "sha512-0xXkAWlDof/qFi3k5KJZ5WYbgp1X8hZHjyasOWTZWAmldoYaENI0vkL+PVMarvoAFYRRqcWoS81FSgm0QgH2sA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.195", "", { "os": "win32", "cpu": "x64" }, "sha512-av0piEB3X1Dzhpr8A+DqHVZ9y8s1jpn8enzwX0TKKUPBn5IqLTWC7wD6v66aoUgu4f+g4ThZirmDZA6shyPEZQ=="],
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.196", "", { "os": "win32", "cpu": "x64" }, "sha512-FxWLA3aOYgDf2J0o6Ov1/wgg9X6RkrgnX4ifUWO1i3+6mbc4efNLHkDZLxCHEnQgW8yBidX+iro19f9k64vV8A=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.37.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw=="],
|
||||
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
|
||||
|
||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||
|
||||
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
||||
|
||||
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
||||
@@ -801,41 +799,41 @@
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1075.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/credential-provider-node": "^3.972.58", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-typFcdyFwIPt86QPKsO7xwcsYoEmNcDpoNqn0tqa4+Lss7niNKQzPe/765XNgAHO3I1ixiT1OKv8KD6M+CKw2w=="],
|
||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1076.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.24", "@aws-sdk/credential-provider-node": "^3.972.59", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/fetch-http-handler": "^5.6.0", "@smithy/node-http-handler": "^4.9.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-hSkEljVcPBXPSsB/GLeVZaNaIzrMLPAj//jg41rNObfMGa7qRvWHXtbDt7UVxXjAFlVw84wCFWNBMBSql5HynQ=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.974.23", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@aws-sdk/xml-builder": "^3.972.31", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.6", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ=="],
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.974.24", "", { "dependencies": { "@aws-sdk/types": "^3.973.14", "@aws-sdk/xml-builder": "^3.972.32", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.27.0", "@smithy/signature-v4": "^5.5.3", "@smithy/types": "^4.15.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-vWB/qJl21vxGKBkBN8fKPTVXgm14v/bUQWTtR5oikrfAZbIN2bxuSiCY5rRAMR4gs3vtR2Vw0aTfVDU4tdfIPg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.48", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-dTSY4wCPx87Gd1peDcTop1li61f6KLAs3LSlq/omnCxpIOvMDpAQjylJ7ZDaZk6KA88+oZ0k/XuVSou1YuWI/w=="],
|
||||
"@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.49", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-PU8EJj5wMvTqp5oeBVCPK1vqraQ9ZlUVYTM5Bbvq1pBTY3WGr2wgvnGCRalFQpS7BFUQflpjumHcaQBmkOhfBA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ=="],
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.50", "", { "dependencies": { "@aws-sdk/core": "^3.974.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-l8bWzhPFTi9tDcvtURxeMlfsboul5/0sEN3SwwXxdpYudVB9+EuQcxo2pwlTzXwDo4Gm2VLGyiZ8zti3nfdOLw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.51", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA=="],
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.52", "", { "dependencies": { "@aws-sdk/core": "^3.974.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/fetch-http-handler": "^5.6.0", "@smithy/node-http-handler": "^4.9.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-FjAlnsIvemWzO3JTM3ObuuxpqCyrqkXOewlYY2+NiR1MYO1JuFYSIJ8SJN5Q2KD1jkL5lIuab8awjb/AxsvjiQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.56", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/credential-provider-env": "^3.972.49", "@aws-sdk/credential-provider-http": "^3.972.51", "@aws-sdk/credential-provider-login": "^3.972.55", "@aws-sdk/credential-provider-process": "^3.972.49", "@aws-sdk/credential-provider-sso": "^3.972.55", "@aws-sdk/credential-provider-web-identity": "^3.972.55", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w=="],
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.57", "", { "dependencies": { "@aws-sdk/core": "^3.974.24", "@aws-sdk/credential-provider-env": "^3.972.50", "@aws-sdk/credential-provider-http": "^3.972.52", "@aws-sdk/credential-provider-login": "^3.972.56", "@aws-sdk/credential-provider-process": "^3.972.50", "@aws-sdk/credential-provider-sso": "^3.972.56", "@aws-sdk/credential-provider-web-identity": "^3.972.56", "@aws-sdk/nested-clients": "^3.997.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/credential-provider-imds": "^4.4.3", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-8qwNhQ0sK/1KaOpVEFC7TFxrWP3fxzJV1K049MzjouiMIbvTDvIGDEUtj5ND5aTmlHVK/YZxjoYnLCeV/GZU0w=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA=="],
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.56", "", { "dependencies": { "@aws-sdk/core": "^3.974.24", "@aws-sdk/nested-clients": "^3.997.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-S36dCrDaafakFMlaCVGAF4advbQKoJuMcyMtNWVBpUz65uqhbIAsUfvAyp+djA+jkzaEfgZGd+AELjIGzTqyhw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.58", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.49", "@aws-sdk/credential-provider-http": "^3.972.51", "@aws-sdk/credential-provider-ini": "^3.972.56", "@aws-sdk/credential-provider-process": "^3.972.49", "@aws-sdk/credential-provider-sso": "^3.972.55", "@aws-sdk/credential-provider-web-identity": "^3.972.55", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw=="],
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.59", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.50", "@aws-sdk/credential-provider-http": "^3.972.52", "@aws-sdk/credential-provider-ini": "^3.972.57", "@aws-sdk/credential-provider-process": "^3.972.50", "@aws-sdk/credential-provider-sso": "^3.972.56", "@aws-sdk/credential-provider-web-identity": "^3.972.56", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/credential-provider-imds": "^4.4.3", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-LkczBXaEsdManijlEZwbKfEoo1C98Yri3LHF8gQI7CYWv+uFkmpS3OZH3BSew8g1A2ppKsScdPUSlhI6NV7a9g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.49", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ=="],
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.50", "", { "dependencies": { "@aws-sdk/core": "^3.974.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-ARBEVkOQzmowTU0a35smGVyldJ9FN/f57XIGrPatrul4mYN+vvOKxoc1njDOX3nugVze+0sHzQZWJ8kPARAtUA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/token-providers": "3.1074.0", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w=="],
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.56", "", { "dependencies": { "@aws-sdk/core": "^3.974.24", "@aws-sdk/nested-clients": "^3.997.24", "@aws-sdk/token-providers": "3.1076.0", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-LvbWiFcLI/D5RPaT68TrpLLHyv7x5X+dm59wJ5dFizyGPZggBC7OdgJTlP0X1bVjiSSAgE1u1oxxcBps0GCEnA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.55", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg=="],
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.56", "", { "dependencies": { "@aws-sdk/core": "^3.974.24", "@aws-sdk/nested-clients": "^3.997.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-OV3JxmqMphVGMLWupYD2UhZxX07ATk1NwyYk7RgCnAEh0y3owHmtEnkWZ3ciCZ6liiFEwS8dYQpJGmKsR6ml4Q=="],
|
||||
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1075.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1075.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/credential-provider-cognito-identity": "^3.972.48", "@aws-sdk/credential-provider-env": "^3.972.49", "@aws-sdk/credential-provider-http": "^3.972.51", "@aws-sdk/credential-provider-ini": "^3.972.56", "@aws-sdk/credential-provider-login": "^3.972.55", "@aws-sdk/credential-provider-node": "^3.972.58", "@aws-sdk/credential-provider-process": "^3.972.49", "@aws-sdk/credential-provider-sso": "^3.972.55", "@aws-sdk/credential-provider-web-identity": "^3.972.55", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-2HoJ1IxwdzEryyUmZvl6dVKfgWBnS6NzSDl5hOqmbHns5Cy+wCQLDFU+HGVi+kTGZgSyMZIa0rH7fQvNf7v9jQ=="],
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1076.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1076.0", "@aws-sdk/core": "^3.974.24", "@aws-sdk/credential-provider-cognito-identity": "^3.972.49", "@aws-sdk/credential-provider-env": "^3.972.50", "@aws-sdk/credential-provider-http": "^3.972.52", "@aws-sdk/credential-provider-ini": "^3.972.57", "@aws-sdk/credential-provider-login": "^3.972.56", "@aws-sdk/credential-provider-node": "^3.972.59", "@aws-sdk/credential-provider-process": "^3.972.50", "@aws-sdk/credential-provider-sso": "^3.972.56", "@aws-sdk/credential-provider-web-identity": "^3.972.56", "@aws-sdk/nested-clients": "^3.997.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/credential-provider-imds": "^4.4.3", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-1jmzAXZdQzZKT/edDehSuxBfFo9M90nyLMGV65joOaZusa/p3YEPtPdHhTQ1CWoYq9kBBsMcfMElSbKjAfYTJw=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.23", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.23", "@aws-sdk/signature-v4-multi-region": "^3.996.35", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA=="],
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.24", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.24", "@aws-sdk/signature-v4-multi-region": "^3.996.36", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/fetch-http-handler": "^5.6.0", "@smithy/node-http-handler": "^4.9.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-+wFVfVofxeiXdRhUjRwYISB2mVfBCdiCq1wThkRipTeOc10Kyr+LS9QJTjgZuhWsna7jyLMPndrCnzLGWWvZXg=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.35", "", { "dependencies": { "@aws-sdk/types": "^3.973.13", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg=="],
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.36", "", { "dependencies": { "@aws-sdk/types": "^3.973.14", "@smithy/signature-v4": "^5.5.3", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-VSOWIPkI+g3a7NkxIBCO24HnsR0BZXJAi3wrKaGIZwVKyrMtNRdHxPrQI/igazgla5J9FhDzmg4RgnOSr6UQBw=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1074.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.23", "@aws-sdk/nested-clients": "^3.997.23", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg=="],
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1076.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.24", "@aws-sdk/nested-clients": "^3.997.24", "@aws-sdk/types": "^3.973.14", "@smithy/core": "^3.27.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-4rTHETRKe2JWAsFUMo5ENmlzc3i9FD4KqBVXgoaF8DLTADjGid8SA+1LR2nJWjefoafvKAHcQH9F2iKa8uHc6Q=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.973.13", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg=="],
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.973.14", "", { "dependencies": { "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-vH4pEu9YBEwr67yT+GVcmKX0GzfIrIYUn+MF5vXg9OspouVnAekuyVyawFvZHEK7WlcwVDwNrqI3ZBDUAiyu9A=="],
|
||||
|
||||
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.8", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g=="],
|
||||
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.31", "", { "dependencies": { "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ=="],
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.32", "", { "dependencies": { "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-2loKuOMRFDg1nwdni5AtJ9S5juVbRNPNsPC7tWTfkHyycPwACMhxepspUHi8GhvfNlL2cQo3sPMod1uib+KZ0w=="],
|
||||
|
||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="],
|
||||
|
||||
@@ -1753,9 +1751,9 @@
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="],
|
||||
|
||||
"@posthog/core": ["@posthog/core@1.38.0", "", { "dependencies": { "@posthog/types": "^1.391.1" } }, "sha512-QLrJh0hMVpEJXHNyiJR9YhvIO5tzLDedw4UHdvB+ub4fXHMtHCA8H44qgl11HWR3ajpjxtJ8hs3HyOGVF3Zc1w=="],
|
||||
"@posthog/core": ["@posthog/core@1.38.1", "", { "dependencies": { "@posthog/types": "^1.392.0" } }, "sha512-tsJTugsKzx47eRMjNfG272/GFf0FF0LeI+gyJ/anibpbYAzdNuX5kr6enpmJrhdgLESqzJGH8QF0+I9075Xr1Q=="],
|
||||
|
||||
"@posthog/types": ["@posthog/types@1.391.1", "", {}, "sha512-ASwd7Nf4pViqdYRYaNRyPYRVKWa1CcHUAUWR0XeQJLGdNnsWACBwe0sSieb/cHnKsRXjRwO/23KIY83lm/Ccpw=="],
|
||||
"@posthog/types": ["@posthog/types@1.392.0", "", {}, "sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
@@ -2259,25 +2257,25 @@
|
||||
|
||||
"@slack/web-api": ["@slack/web-api@7.17.0", "", { "dependencies": { "@slack/logger": "^4.0.1", "@slack/types": "^2.21.0", "@types/node": ">=18", "@types/retry": "0.12.0", "axios": "^1.16.0", "eventemitter3": "^5.0.1", "form-data": "^4.0.4", "is-electron": "2.2.2", "is-stream": "^2", "p-queue": "^6", "p-retry": "^4", "retry": "^0.13.1" } }, "sha512-jejr34a8B4L5AS713wOAx1LAqNkW16HVMDEa6sYBvFDc/llUBl8hXaiI4BwF+Al+Sug19Vn2O7iokTVIhVvZ1Q=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.26.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g=="],
|
||||
"@smithy/core": ["@smithy/core@3.28.0", "", { "dependencies": { "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-N/LoLG8pZ1zv5cIWpdF6vmSjtZtXKK9G0OqT5yYCOZU+CzPq1+nYA95VoKJBGWRScs7YbMugZ7lZx8Fj1vdHoA=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug=="],
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.4", "", { "dependencies": { "@smithy/core": "^3.28.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-jT0WrDaM88L5na9FX1xRNywCS3B1n75wPY5Ksasjo0PHUtuI7d8FclksN1BbOSYTiaiKxUDqU23nUymH/V+AaQ=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.4.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "tslib": "^2.6.2" } }, "sha512-iv6jeGoL5dIGXglIe0aJb8vvTuJkGB4z0LeB2mKV0PH9iAlr4jNhRhEWU7ZGmIeNC+8Zj6jhmSumDez6DidTOA=="],
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.4.4", "", { "dependencies": { "@smithy/core": "^3.28.0", "tslib": "^2.6.2" } }, "sha512-DStvMemWlcZRXkP9XdCsinolM6yZd4fL2NiQItC8n/I+JC6utkLr0Dc+wLjLqjPyAJR1zXt7inSflea217yGlw=="],
|
||||
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.5.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg=="],
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.6.1", "", { "dependencies": { "@smithy/core": "^3.28.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-fW6l9rWoyk1iyzfuZaERnZLNjB6WIojgGm6Bo9Hpfpy3RUpltjLikNlxTsS/YtxVobcfbCGBuAncREYqT4hvqQ=="],
|
||||
|
||||
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ=="],
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.9.1", "", { "dependencies": { "@smithy/core": "^3.28.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-m/f15di58P6NtLQ7eVEb5N19NdJWn+4c7zfkFHMT/i3JH7U8UtknpPoy8o2tm2R3OdliYvsvQhZHIfACQDqT+Q=="],
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.5.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA=="],
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.6.0", "", { "dependencies": { "@smithy/core": "^3.28.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-IkPHQdbyoebSwBCuMTzJ/2oIhKVqiZZAZxQYSlpDZqq/WhJUpmdgbHvP7ItddxsPzcDUJeI0V4PNMSNtlZ0aqA=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.15.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg=="],
|
||||
|
||||
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.4.2", "", { "dependencies": { "@smithy/core": "^3.26.0", "tslib": "^2.6.2" } }, "sha512-N5CpfaL+/LPQU9PFdOT55ayUo5T0QypG4Almzd1/efJvoDypuT1shkgJk1+hhg/02scYluW6Q2JGnSHIPwCEGQ=="],
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.4.4", "", { "dependencies": { "@smithy/core": "^3.28.0", "tslib": "^2.6.2" } }, "sha512-kvxCWygmILHgwuIQKxocTHblTsF1eWLF/rBN5qjY7QmHfIglcqJoe/p9mo7uOR/dA+h3eVZVLZcmscxp+WtDCA=="],
|
||||
|
||||
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
|
||||
|
||||
@@ -2341,37 +2339,37 @@
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.27", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="],
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.2" } }, "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="],
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.2", "@tailwindcss/oxide-darwin-arm64": "4.3.2", "@tailwindcss/oxide-darwin-x64": "4.3.2", "@tailwindcss/oxide-freebsd-x64": "4.3.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", "@tailwindcss/oxide-linux-x64-musl": "4.3.2", "@tailwindcss/oxide-wasm32-wasi": "4.3.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ=="],
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.2", "", { "os": "android", "cpu": "arm64" }, "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA=="],
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg=="],
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g=="],
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg=="],
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ=="],
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA=="],
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg=="],
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ=="],
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.1", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.2", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg=="],
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA=="],
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ=="],
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "postcss": "8.5.15", "tailwindcss": "4.3.1" } }, "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A=="],
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.2", "@tailwindcss/oxide": "4.3.2", "postcss": "^8.5.15", "tailwindcss": "4.3.2" } }, "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.1", "", { "dependencies": { "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "tailwindcss": "4.3.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ=="],
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.2", "", { "dependencies": { "@tailwindcss/node": "4.3.2", "@tailwindcss/oxide": "4.3.2", "tailwindcss": "4.3.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA=="],
|
||||
|
||||
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.11.3", "", { "dependencies": { "@tanstack/virtual-core": "3.11.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw=="],
|
||||
|
||||
@@ -2379,29 +2377,29 @@
|
||||
|
||||
"@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="],
|
||||
|
||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.11.3", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.3", "@tauri-apps/cli-darwin-x64": "2.11.3", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.3", "@tauri-apps/cli-linux-arm64-gnu": "2.11.3", "@tauri-apps/cli-linux-arm64-musl": "2.11.3", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.3", "@tauri-apps/cli-linux-x64-gnu": "2.11.3", "@tauri-apps/cli-linux-x64-musl": "2.11.3", "@tauri-apps/cli-win32-arm64-msvc": "2.11.3", "@tauri-apps/cli-win32-ia32-msvc": "2.11.3", "@tauri-apps/cli-win32-x64-msvc": "2.11.3" }, "bin": { "tauri": "tauri.js" } }, "sha512-EElQe8z8uD7Pi5++tJ/UfEwWuK08rd3oCDYdeIbJAb6pZRrxlqmoF5gh5H5YvzmUPhS4IRCaLSsQhvWkrfK+GQ=="],
|
||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.11.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.4", "@tauri-apps/cli-darwin-x64": "2.11.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", "@tauri-apps/cli-linux-arm64-musl": "2.11.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-musl": "2.11.4", "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", "@tauri-apps/cli-win32-x64-msvc": "2.11.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ=="],
|
||||
|
||||
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BxpaM8bsCoXs3wd4WKYhas/G1gs7+r7B+e4WnyRk2GEoVOouJB1hoL6E6YLXZDXbYci6VFdrNnobQwd2uVL4ew=="],
|
||||
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ=="],
|
||||
|
||||
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DbZYuPB1ZEzcAHYeyCvo3ltzM27+aXwPloCrtexPnmgPgulYJm3TOq6aC4S+wPhSXteddg8zImtNkvx/gQzmwg=="],
|
||||
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.3", "", { "os": "linux", "cpu": "arm" }, "sha512-741NduqBmz1XkdU8yz3OI/kBZtqHbvxo9F9ytIeWYU69/Ba9dcZEbqOU++Dp0G/XU8vAI0TfTywEl+p+BbLvaA=="],
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.4", "", { "os": "linux", "cpu": "arm" }, "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RWAXT8pTqIczXcoic+LXlo6uEbAXGB0cgh6Pg7Y9xVnEbzryQ1JHtRGj9SxzrKSemBIDBH6Qc24kK2G69i8ofA=="],
|
||||
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA=="],
|
||||
|
||||
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-qomqYS+yAkd0gXMRmhguWXc7RfVN+XKKXaEwbf5QmKURwydLFOTldd6F8/WoZDSsBMrV8dpNxz0YneGLmobiSA=="],
|
||||
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw=="],
|
||||
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.3", "", { "os": "linux", "cpu": "none" }, "sha512-jOCXbDqeDj5XcclsOBAaXjtTgwZCVg8zEZ+dbPUCoADOgljFgL0rOkYTc96vUYgOrYEfuHYihWMxIDGaD6GwJw=="],
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.4", "", { "os": "linux", "cpu": "none" }, "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ=="],
|
||||
|
||||
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.3", "", { "os": "linux", "cpu": "x64" }, "sha512-+u3HO/F3gHwL48t9gWN/urqZvpaEJzBFmTaq5eSIhvy8TOvnhb+LgJr3Q3BG+5JxuBrCUjqtOEz6gMttdJFSBA=="],
|
||||
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ=="],
|
||||
|
||||
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.3", "", { "os": "linux", "cpu": "x64" }, "sha512-spr5Jpr6KF/vehkLwJ0YmdGv8QwpWU+uw7J8bgijO0sox6ZCYsSNMbcsQjTqPi4xl+p0woIYpWXgChgHYpAc8g=="],
|
||||
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A=="],
|
||||
|
||||
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-abkoRQih5xBa3vz2spWaex0kP/MzVzVPQHom2f8jnCq46R/luOD6Uy85EMU9/bfzf6ZzdorWJsgO+OMX90Fx2w=="],
|
||||
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ=="],
|
||||
|
||||
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-Vy6AvzFm1G40hg3r+OYDB3jkuu7R4wnMzbQBKuun9v6Cgg8IierpLL7toMzrZKs/8NlG8Sg4x1iLFR52oknyHg=="],
|
||||
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA=="],
|
||||
|
||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.3", "", { "os": "win32", "cpu": "x64" }, "sha512-GlciF75GdbseajOyib2aCHwE3BXIqZ1liGKWLFRvCdN5wm8h8hFssEVKQ/6E+2jsMLg9v7LCTb983YFnn0QSww=="],
|
||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="],
|
||||
|
||||
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||
|
||||
@@ -2625,25 +2623,25 @@
|
||||
|
||||
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/type-utils": "8.62.0", "@typescript-eslint/utils": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.62.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.62.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/type-utils": "8.62.1", "@typescript-eslint/utils": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.62.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.62.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/types": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.62.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.0", "@typescript-eslint/types": "^8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.62.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.1", "@typescript-eslint/types": "^8.62.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0" } }, "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1" } }, "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.62.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.62.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1", "@typescript-eslint/utils": "8.62.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.62.0", "", {}, "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.62.1", "", {}, "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.62.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.62.0", "@typescript-eslint/tsconfig-utils": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.62.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.62.1", "@typescript-eslint/tsconfig-utils": "8.62.1", "@typescript-eslint/types": "8.62.1", "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.62.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.1", "@typescript-eslint/types": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.1", "", { "dependencies": { "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g=="],
|
||||
|
||||
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.6", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og=="],
|
||||
|
||||
@@ -2739,7 +2737,7 @@
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ai": ["ai@6.0.213", "", { "dependencies": { "@ai-sdk/gateway": "3.0.138", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yL5KBSlhi+gOVfmTs7lOgwF0GmLaDkwIdbzMDouGL6e5SVwEUviijRMPRnMXf/3UYupUmzUrxKVFCAYaHr3egA=="],
|
||||
"ai": ["ai@6.0.216", "", { "dependencies": { "@ai-sdk/gateway": "3.0.140", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-K6/H1H6b+IJuz79Nc44A/tEPzAH+dUCmQhHtP2In943OxvLekosVYBwUUsr1oQjc/JvN8WRpL1mLuefUBHxs/w=="],
|
||||
|
||||
"ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.5.0", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.3.170" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-7SZrTGkuR4G4zeNrjnJgDzYkrKZqzq7GUQJcFBKCxFH5dJpS9lbs+g9BCt7bwT1gDm4SAUmOQ0T5rKqcC2HkVQ=="],
|
||||
|
||||
@@ -2823,7 +2821,7 @@
|
||||
|
||||
"bare-fs": ["bare-fs@4.7.2", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg=="],
|
||||
|
||||
"bare-os": ["bare-os@3.9.1", "", {}, "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ=="],
|
||||
"bare-os": ["bare-os@3.9.3", "", {}, "sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ=="],
|
||||
|
||||
"bare-path": ["bare-path@3.0.1", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ=="],
|
||||
|
||||
@@ -3259,7 +3257,7 @@
|
||||
|
||||
"eight-colors": ["eight-colors@1.3.3", "", {}, "sha512-4B54S2Qi4pJjeHmCbDIsveQZWQ/TSSQng4ixYJ9/SYHHpeS5nYK0pzcHvWzWUfRsvJQjwoIENhAwqg59thQceg=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.380", "", {}, "sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.381", "", {}, "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg=="],
|
||||
|
||||
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
|
||||
|
||||
@@ -3295,7 +3293,7 @@
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
|
||||
"es-module-lexer": ["es-module-lexer@2.2.0", "", {}, "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||
|
||||
@@ -3403,7 +3401,7 @@
|
||||
|
||||
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||
"fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="],
|
||||
|
||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
|
||||
|
||||
@@ -3471,7 +3469,7 @@
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
|
||||
"fs-extra": ["fs-extra@11.3.6", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
@@ -4247,7 +4245,7 @@
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
|
||||
"p-map": ["p-map@7.0.5", "", {}, "sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA=="],
|
||||
|
||||
"p-mutex": ["p-mutex@1.0.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-UlthGzEMsg2VnZAR58wkzL7muskxtNamoTR1Q6/VYBUKqPaMM+YtSncjWIvyjfUvVECKck1SYC/4XIWWJU3gBw=="],
|
||||
|
||||
@@ -4353,15 +4351,15 @@
|
||||
|
||||
"points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
||||
|
||||
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||
"postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"posthog-js": ["posthog-js@1.395.0", "", { "dependencies": { "@posthog/core": "^1.38.0", "@posthog/types": "^1.391.1", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw=="],
|
||||
"posthog-js": ["posthog-js@1.396.2", "", { "dependencies": { "@posthog/core": "^1.38.1", "@posthog/types": "^1.392.0", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", "preact": "^10.29.2", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^5.3.0" } }, "sha512-WFdS0JL+r/M7A9XQwIGbw1Xn6W7/V5TEmv1wgrq7GFcPxZ0I3TktNGtGQNmzARbI8nKedAHFkY9UPDDg+NTSQg=="],
|
||||
|
||||
"posthog-node": ["posthog-node@5.38.6", "", { "dependencies": { "@posthog/core": "^1.38.0" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-Sm2mCAa9/lTTYppnKyy0AhQrriq8fOd8B2vwd3EE/9uihyIx9qkJ1xGYbvADxQJlF7HqM1/TIFCKsI0JvF8gjQ=="],
|
||||
"posthog-node": ["posthog-node@5.38.8", "", { "dependencies": { "@posthog/core": "^1.38.1" }, "peerDependencies": { "rxjs": "^7.0.0" }, "optionalPeers": ["rxjs"] }, "sha512-AWsp9Tigf4iZepabPErAt/2sLTGwJ7w6631JP+3ifDqWzaBPyzcukD7cTPeoNDKGwJreQ69Ju4l53n9g2+X6nQ=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
@@ -4483,7 +4481,7 @@
|
||||
|
||||
"react-use": ["react-use@17.6.1", "", { "dependencies": { "@types/js-cookie": "^3.0.0", "@xobotyi/scrollbar-width": "^1.9.5", "copy-to-clipboard": "^3.3.1", "fast-deep-equal": "^3.1.3", "fast-shallow-equal": "^1.0.0", "js-cookie": "^3.0.0", "nano-css": "^5.6.2", "react-universal-interface": "^0.6.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.1.0", "set-harmonic-interval": "^1.0.1", "throttle-debounce": "^3.0.1", "ts-easing": "^0.2.0", "tslib": "^2.1.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-uibb3pgzV4LFsYPHyXYGu7dD2+pyk/ZJlPH+AizBR3zolqPWyCleKcWWbUQaKSKfydwgnQ3ymGm1Ab3/saGHCA=="],
|
||||
|
||||
"react-virtuoso": ["react-virtuoso@4.18.9", "", { "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", "react-dom": ">=16 || >=17 || >= 18 || >=19" } }, "sha512-hnS+4ip23UfMOAbiEfkpUu9OHau9SAYWgu3fUcKKnCmngV4eorkPSMm7/4XzjLqj4EKwIXk6Jeyd1azIbVnMkQ=="],
|
||||
"react-virtuoso": ["react-virtuoso@4.18.10", "", { "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", "react-dom": ">=16 || >=17 || >= 18 || >=19" } }, "sha512-P6GIZ7kWAPOYB2H16yRQNgy+VF9pJOuTFw1EUc1EAtCj5WxVSAF1Sql3x3fbLwaLeBFsiPnu+3U9o6sIOyTdFw=="],
|
||||
|
||||
"read": ["read@1.0.7", "", { "dependencies": { "mute-stream": "~0.0.4" } }, "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ=="],
|
||||
|
||||
@@ -4801,15 +4799,15 @@
|
||||
|
||||
"tailwind-variants": ["tailwind-variants@3.2.2", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge"] }, "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="],
|
||||
"tailwindcss": ["tailwindcss@4.3.2", "", {}, "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA=="],
|
||||
|
||||
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tar": ["tar@7.5.17", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-wPEBwzapC+2PaTYPH6e2L+cNOEE227S47wUYFqlegcs8zlLLmeb9Fcff1HVZY4Fwku/1Eyv38n7GYwB2aaS71g=="],
|
||||
"tar": ["tar@7.5.19", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw=="],
|
||||
|
||||
"tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="],
|
||||
"tar-fs": ["tar-fs@3.1.3", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="],
|
||||
|
||||
@@ -4903,7 +4901,7 @@
|
||||
|
||||
"ts-poet": ["ts-poet@6.12.0", "", { "dependencies": { "dprint-node": "^1.0.8" } }, "sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA=="],
|
||||
|
||||
"ts-proto": ["ts-proto@2.11.8", "", { "dependencies": { "@bufbuild/protobuf": "^2.10.2", "case-anything": "^2.1.13", "ts-poet": "^6.12.0", "ts-proto-descriptors": "2.1.0" }, "bin": { "protoc-gen-ts_proto": "protoc-gen-ts_proto" } }, "sha512-+5hzECnyVB33jxjG1BIdzAHcRBm7hjnm8womdJVp2A7xJWihP0drHHVsXYTr9i/LpWNGfh80I+AVVNzFM5AwJw=="],
|
||||
"ts-proto": ["ts-proto@2.11.10", "", { "dependencies": { "@bufbuild/protobuf": "^2.10.2", "case-anything": "^2.1.13", "ts-poet": "^6.12.0", "ts-proto-descriptors": "2.1.0" }, "bin": { "protoc-gen-ts_proto": "protoc-gen-ts_proto" } }, "sha512-7mvz2RbOZc0J/+x8biIcVHJ0nx7xjH79vtoEnkpKT5l8yFF5ERe03dee2W3CbB5vb1QgnbQXGzeBGGryJ+Q9EA=="],
|
||||
|
||||
"ts-proto-descriptors": ["ts-proto-descriptors@2.1.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0" } }, "sha512-S5EZYEQ6L9KLFfjSRpZWDIXDV/W7tAj8uW7pLsihIxyr62EAVSiKuVPwE8iWnr849Bqa53enex1jhDUcpgquzA=="],
|
||||
|
||||
@@ -4933,7 +4931,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.62.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.0", "@typescript-eslint/parser": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.62.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.1", "@typescript-eslint/parser": "8.62.1", "@typescript-eslint/typescript-estree": "8.62.1", "@typescript-eslint/utils": "8.62.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw=="],
|
||||
|
||||
"uc.micro": ["uc.micro@1.0.6", "", {}, "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="],
|
||||
|
||||
@@ -5835,7 +5833,7 @@
|
||||
|
||||
"dify-ai-provider/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="],
|
||||
|
||||
"dify-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-JFhJK5ynprll2FR3e+sHagJJIwvIagsNA0FLbLPq2Os4yLUK2/eiaCU0jXsADik73/hhvcPPLmD+Uo8eu5kFaQ=="],
|
||||
"dify-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.28", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bXlX1WX7E50a2N+AJW+1a/x63m52aPhm+6xYe5THxWrx9vW9NR7E2Ay+1G1ndlCdMdYKo2Fnsd7kBhuyQPaphw=="],
|
||||
|
||||
"dify-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
@@ -6217,6 +6215,8 @@
|
||||
|
||||
"unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.17", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.13", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.10", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.12", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw=="],
|
||||
|
||||
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||
|
||||
"vite-node/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
@@ -6611,7 +6611,7 @@
|
||||
|
||||
"@types/jest/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
|
||||
|
||||
"@vscode/test-cli/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
@@ -6625,7 +6625,7 @@
|
||||
|
||||
"@vscode/vsce/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"@vscode/vsce/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||
"@vscode/vsce/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
|
||||
|
||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
@@ -6785,7 +6785,7 @@
|
||||
|
||||
"gauge/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
|
||||
|
||||
"googleapis-common/gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
@@ -6987,12 +6987,34 @@
|
||||
|
||||
"test-exclude/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
|
||||
|
||||
"unzipper/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
|
||||
"unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-escape-keydown": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.10", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.12", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.6", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.6", "", { "dependencies": { "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
|
||||
|
||||
"webview-ui/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"webview-ui/@vitejs/plugin-react-swc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
@@ -7213,7 +7235,7 @@
|
||||
|
||||
"react-remark/unified/vfile/vfile-message": ["vfile-message@2.0.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" } }, "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
|
||||
|
||||
"shadcn/open/wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
|
||||
@@ -7239,6 +7261,10 @@
|
||||
|
||||
"test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
|
||||
|
||||
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
|
||||
|
||||
"webview-ui/vitest/@vitest/utils/loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
|
||||
"webview-ui/vitest/chai/check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
||||
@@ -7339,7 +7365,7 @@
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"shadcn/ts-morph/@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
||||
"shadcn/ts-morph/@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
|
||||
|
||||
"test-exclude/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
|
||||
+6
-1
@@ -323,15 +323,20 @@ Context compaction is owned by `core`.
|
||||
|
||||
- `@cline/agents` owns the generic turn-preparation seam:
|
||||
- run normal lifecycle hooks
|
||||
- allow hosts to rewrite message history or system prompt before the provider call
|
||||
- allow hosts to project message history or system prompt before the provider call
|
||||
- keep its canonical runtime transcript append-only when a projection is returned
|
||||
- `@cline/core` owns compaction policy:
|
||||
- inject a prepare-turn pipeline for root sessions
|
||||
- choose between built-in strategies through a registry map
|
||||
- persist the latest compacted working context as a session compaction artifact
|
||||
- keep compaction logic out of the low-level agent message builder
|
||||
|
||||
Design implications:
|
||||
|
||||
- compaction is a context-pipeline concern owned by `core`
|
||||
- canonical session history lives in the session messages artifact at full fidelity; compaction state lives separately in `${sessionId}.compaction.json`
|
||||
- resume loads the canonical transcript for history/debugging and, when present, reuses the latest compaction state only after validating a hash of the canonical prefix covered by that state; valid state is projected by appending canonical messages written after the compaction boundary
|
||||
- sessions that were already persisted with compacted messages before this model are best-effort only because the omitted original transcript is not recoverable from the compacted artifact
|
||||
- `agents` stays focused on the stateless loop and provider/tool orchestration
|
||||
- delegated/subagent flows should inherit compaction behavior through core session config, not through a separate agent-level compaction hook surface
|
||||
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Cline SDK Changelog
|
||||
|
||||
## 0.0.58
|
||||
|
||||
- `read_files` now tolerates malformed input from weaker models: line-range entries (`start_line`/`end_line`) sent as separate array items are coalesced back onto the preceding file path instead of being rejected
|
||||
|
||||
## 0.0.57
|
||||
|
||||
- Models in the live catalog that don't report a context window now default to a 128K input-token limit (up from 4,096), so under-specified models get a usable context budget
|
||||
- The default max input-token budget used for context compaction is now 128K
|
||||
- Added a shared prompt-format helper in `@cline/shared` and simplified runtime host support
|
||||
|
||||
## 0.0.56
|
||||
|
||||
- Tool calls from weaker models that use slightly-off argument shapes (e.g. a bare string where an array is expected) or malformed/truncated JSON are now coerced or repaired and executed, instead of being rejected before the tools can handle them
|
||||
|
||||
@@ -206,6 +206,38 @@ new Agent({
|
||||
For richer, host-side hook orchestration (15-stage `HookEngine`,
|
||||
subprocess-backed hooks, MCP extensions), use `@cline/core`.
|
||||
|
||||
### Preparing Requests with `prepareTurn`
|
||||
|
||||
`prepareTurn` runs before messages are sent to the provider. It can rewrite the
|
||||
messages or system prompt for the next request:
|
||||
|
||||
```text
|
||||
saved transcript
|
||||
|
|
||||
| turn preparation
|
||||
v
|
||||
prepareTurn
|
||||
|
|
||||
v
|
||||
prepared provider request
|
||||
```
|
||||
|
||||
Returned messages affect only the provider request for the current model call.
|
||||
They do not replace saved history and are not returned from
|
||||
`AgentRunResult.messages`.
|
||||
|
||||
```text
|
||||
prepareTurn returns prepared messages
|
||||
|
|
||||
+--> provider request: yes
|
||||
+--> saved transcript: no
|
||||
+--> AgentRunResult.messages: no
|
||||
```
|
||||
|
||||
This is intentionally different from changing saved history. Hosts that need
|
||||
durable redaction, normalization, or policy filtering must apply that change
|
||||
before a message enters the transcript.
|
||||
|
||||
### Plugins
|
||||
|
||||
Plugins can contribute tools and hooks at setup time:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cline/agents",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline",
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("AgentRuntime", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("injects pending user messages before prepareTurn rewrites the transcript", async () => {
|
||||
it("injects pending user messages before prepareTurn projects the provider request", async () => {
|
||||
const consumePendingUserMessage = vi.fn(() => "steer before prepare");
|
||||
const prepareTurn = vi.fn(
|
||||
(context: { messages: readonly AgentMessage[] }) => ({
|
||||
@@ -261,7 +261,7 @@ describe("AgentRuntime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("lets prepareTurn compact tool results after pending user input is added", async () => {
|
||||
it("lets prepareTurn project tool results after pending user input is added", async () => {
|
||||
const consumePendingUserMessage = vi.fn(() => "latest steering");
|
||||
const hugeToolOutput = "x".repeat(100_000);
|
||||
const prepareTurn = vi.fn(
|
||||
@@ -1180,11 +1180,11 @@ describe("AgentRuntime", () => {
|
||||
expect(model.requests).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("runs prepareTurn before beforeModel and persists rewritten messages", async () => {
|
||||
const compactedMessage: AgentMessage = {
|
||||
id: "msg_compacted",
|
||||
it("projects the provider request without overwriting canonical messages", async () => {
|
||||
const projectedMessage: AgentMessage = {
|
||||
id: "msg_projected",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "compacted context" }],
|
||||
content: [{ type: "text", text: "projected context" }],
|
||||
createdAt: 1,
|
||||
};
|
||||
const notices: string[] = [];
|
||||
@@ -1197,19 +1197,19 @@ describe("AgentRuntime", () => {
|
||||
reason: "auto_compaction",
|
||||
});
|
||||
return {
|
||||
messages: [compactedMessage],
|
||||
systemPrompt: "compacted system",
|
||||
messages: [projectedMessage],
|
||||
systemPrompt: "projected system",
|
||||
};
|
||||
});
|
||||
const beforeModel = vi.fn(({ request }) => {
|
||||
expect(request.systemPrompt).toBe("compacted system");
|
||||
expect(request.messages).toEqual([compactedMessage]);
|
||||
expect(request.systemPrompt).toBe("projected system");
|
||||
expect(request.messages).toEqual([projectedMessage]);
|
||||
return undefined;
|
||||
});
|
||||
const model = new ScriptedModel([
|
||||
(request) => {
|
||||
expect(request.systemPrompt).toBe("compacted system");
|
||||
expect(request.messages).toEqual([compactedMessage]);
|
||||
expect(request.systemPrompt).toBe("projected system");
|
||||
expect(request.messages).toEqual([projectedMessage]);
|
||||
return [
|
||||
{ type: "text-delta", text: "done" },
|
||||
{ type: "finish", reason: "stop" },
|
||||
@@ -1233,8 +1233,13 @@ describe("AgentRuntime", () => {
|
||||
expect(prepareTurn).toHaveBeenCalledTimes(1);
|
||||
expect(beforeModel).toHaveBeenCalledTimes(1);
|
||||
expect(notices).toEqual(["auto-compacting"]);
|
||||
expect(result.messages[0]).toEqual(compactedMessage);
|
||||
expect(model.requests[0]?.messages).toEqual([projectedMessage]);
|
||||
expect(result.messages[0]).toMatchObject({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "large context" }],
|
||||
});
|
||||
expect(result.messages).toHaveLength(2);
|
||||
expect(result.messages).not.toContainEqual(projectedMessage);
|
||||
expect(model.requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -1340,16 +1345,16 @@ describe("AgentRuntime", () => {
|
||||
});
|
||||
|
||||
it("preserves the existing system prompt when prepareTurn returns only messages", async () => {
|
||||
const compactedMessage: AgentMessage = {
|
||||
id: "msg_compacted",
|
||||
const projectedMessage: AgentMessage = {
|
||||
id: "msg_projected",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "compacted context" }],
|
||||
content: [{ type: "text", text: "projected context" }],
|
||||
createdAt: 1,
|
||||
};
|
||||
const model = new ScriptedModel([
|
||||
(request) => {
|
||||
expect(request.systemPrompt).toBe("original system");
|
||||
expect(request.messages).toEqual([compactedMessage]);
|
||||
expect(request.messages).toEqual([projectedMessage]);
|
||||
return [
|
||||
{ type: "text-delta", text: "done" },
|
||||
{ type: "finish", reason: "stop" },
|
||||
@@ -1359,7 +1364,7 @@ describe("AgentRuntime", () => {
|
||||
const runtime = new AgentRuntime({
|
||||
model,
|
||||
systemPrompt: "original system",
|
||||
prepareTurn: () => ({ messages: [compactedMessage] }),
|
||||
prepareTurn: () => ({ messages: [projectedMessage] }),
|
||||
});
|
||||
|
||||
await runtime.run("large context");
|
||||
|
||||
@@ -797,8 +797,15 @@ export class AgentRuntime {
|
||||
};
|
||||
|
||||
if (this.state.iteration > 1) {
|
||||
if (await this.consumePendingUserMessage()) {
|
||||
request = { ...request, messages: cloneMessages(this.state.messages) };
|
||||
const pendingUserMessage = await this.consumePendingUserMessage();
|
||||
if (pendingUserMessage) {
|
||||
request = {
|
||||
...request,
|
||||
messages: [
|
||||
...request.messages,
|
||||
...cloneMessages([pendingUserMessage]),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,7 +1086,6 @@ export class AgentRuntime {
|
||||
let next = request;
|
||||
if (result.messages) {
|
||||
const preparedMessages = cloneMessages(result.messages);
|
||||
this.state.messages = preparedMessages;
|
||||
next = { ...next, messages: cloneMessages(preparedMessages) };
|
||||
}
|
||||
if (result.systemPrompt !== undefined) {
|
||||
@@ -1088,14 +1094,14 @@ export class AgentRuntime {
|
||||
return next;
|
||||
}
|
||||
|
||||
private async consumePendingUserMessage(): Promise<boolean> {
|
||||
private async consumePendingUserMessage(): Promise<AgentMessage | undefined> {
|
||||
const consumePendingUserMessage = this.config.consumePendingUserMessage;
|
||||
if (!consumePendingUserMessage) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
const pending = (await consumePendingUserMessage())?.trim();
|
||||
if (!pending) {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
const message = createMessage("user", [{ type: "text", text: pending }]);
|
||||
this.state.messages.push(message);
|
||||
@@ -1104,7 +1110,7 @@ export class AgentRuntime {
|
||||
snapshot: this.snapshot(),
|
||||
message,
|
||||
});
|
||||
return true;
|
||||
return message;
|
||||
}
|
||||
|
||||
private async updateUsage(usage: Partial<AgentUsage>): Promise<void> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/core",
|
||||
"description": "Cline Core SDK for Node Runtime",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline",
|
||||
|
||||
@@ -491,6 +491,18 @@ export class ClineCore {
|
||||
*/
|
||||
update: RuntimeHost["updateSession"] = (...args) =>
|
||||
this.host.updateSession(...args);
|
||||
/**
|
||||
* Stores the compacted working-context state for an existing session.
|
||||
*/
|
||||
updateSessionCompactionState: RuntimeHost["updateSessionCompactionState"] = (
|
||||
...args
|
||||
) => this.host.updateSessionCompactionState(...args);
|
||||
/**
|
||||
* Reads the compacted working-context sidecar for a session, if one exists.
|
||||
*/
|
||||
readSessionCompactionState: RuntimeHost["readSessionCompactionState"] = (
|
||||
...args
|
||||
) => this.host.readSessionCompactionState(...args);
|
||||
/**
|
||||
* Reads message history for a session.
|
||||
*
|
||||
|
||||
@@ -6,6 +6,10 @@ import type {
|
||||
CoreCompactionSummarizerConfig,
|
||||
} from "../../types/config";
|
||||
import type { ProviderConfig } from "../../types/provider-settings";
|
||||
import {
|
||||
buildBudgetProjection,
|
||||
type BudgetProjectionResult,
|
||||
} from "./budget-projection";
|
||||
import {
|
||||
buildSummaryMessage,
|
||||
buildSummaryRequest,
|
||||
@@ -20,6 +24,43 @@ import {
|
||||
serializeConversation,
|
||||
} from "./compaction-shared";
|
||||
|
||||
const MIN_AGENTIC_SUMMARY_INPUT_TOKENS = 1_024;
|
||||
|
||||
function resolveProviderMaxInputTokens(
|
||||
providerConfig: ProviderConfig,
|
||||
): number | undefined {
|
||||
const explicit = providerConfig.maxInputTokens;
|
||||
if (typeof explicit === "number" && Number.isFinite(explicit)) {
|
||||
return explicit;
|
||||
}
|
||||
const modelInfoLimit =
|
||||
providerConfig.modelInfo?.maxInputTokens ??
|
||||
providerConfig.modelInfo?.contextWindow;
|
||||
if (typeof modelInfoLimit === "number" && Number.isFinite(modelInfoLimit)) {
|
||||
return modelInfoLimit;
|
||||
}
|
||||
const knownModelInfo = providerConfig.knownModels?.[providerConfig.modelId];
|
||||
const knownModelLimit =
|
||||
knownModelInfo?.maxInputTokens ?? knownModelInfo?.contextWindow;
|
||||
if (typeof knownModelLimit === "number" && Number.isFinite(knownModelLimit)) {
|
||||
return knownModelLimit;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function buildAgenticSummaryInputBudget(options: {
|
||||
messages: CoreCompactionContext["messages"];
|
||||
targetTokens: number;
|
||||
estimateMessageTokens: EstimateMessageTokens;
|
||||
}): BudgetProjectionResult {
|
||||
return buildBudgetProjection({
|
||||
messages: options.messages,
|
||||
targetTokens: Math.max(1, options.targetTokens),
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: options.estimateMessageTokens,
|
||||
});
|
||||
}
|
||||
|
||||
async function generateSummary(options: {
|
||||
providerConfig: ProviderConfig;
|
||||
request: string;
|
||||
@@ -92,8 +133,80 @@ export async function runAgenticCompaction(options: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fileOps = extractFileOps(messagesToSummarize);
|
||||
const conversationText = serializeConversation(newMessagesToFold);
|
||||
const preProjectionFileOps = extractFileOps(messagesToSummarize);
|
||||
const summarizerProviderConfig = resolveSummarizerConfig({
|
||||
activeProviderConfig: options.providerConfig,
|
||||
summarizer: options.summarizer,
|
||||
});
|
||||
const resolvedSummarizerInputLimit = resolveProviderMaxInputTokens(
|
||||
summarizerProviderConfig,
|
||||
);
|
||||
const canUseActiveContextLimit = options.summarizer === undefined;
|
||||
const activeCompactionInputLimit = Math.max(
|
||||
options.context.maxInputTokens,
|
||||
options.context.triggerTokens,
|
||||
MIN_AGENTIC_SUMMARY_INPUT_TOKENS,
|
||||
);
|
||||
if (
|
||||
resolvedSummarizerInputLimit === undefined &&
|
||||
!canUseActiveContextLimit
|
||||
) {
|
||||
options.logger?.log(
|
||||
"Agentic compaction summarizer has no known input limit; using conservative summary budget",
|
||||
{
|
||||
severity: "warn",
|
||||
summarizerProviderId: summarizerProviderConfig.providerId,
|
||||
summarizerModelId: summarizerProviderConfig.modelId,
|
||||
fallbackInputLimit: MIN_AGENTIC_SUMMARY_INPUT_TOKENS,
|
||||
},
|
||||
);
|
||||
}
|
||||
const summarizerInputLimit =
|
||||
resolvedSummarizerInputLimit ??
|
||||
(canUseActiveContextLimit
|
||||
? activeCompactionInputLimit
|
||||
: MIN_AGENTIC_SUMMARY_INPUT_TOKENS);
|
||||
const summaryRequestOverheadTokens = estimateTokens(
|
||||
buildSummaryRequest({
|
||||
previousSummary,
|
||||
conversationText: "",
|
||||
fileOps: preProjectionFileOps,
|
||||
}).length,
|
||||
);
|
||||
const availableSummaryInputTokens =
|
||||
summarizerInputLimit - summaryRequestOverheadTokens;
|
||||
if (availableSummaryInputTokens <= 0) {
|
||||
options.logger?.debug("Skipped agentic compaction: summarizer budget exhausted", {
|
||||
summarizerProviderId: summarizerProviderConfig.providerId,
|
||||
summarizerModelId: summarizerProviderConfig.modelId,
|
||||
summarizerInputLimit,
|
||||
summaryRequestOverheadTokens,
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
const summaryInputBudget = buildAgenticSummaryInputBudget({
|
||||
messages: newMessagesToFold,
|
||||
targetTokens: availableSummaryInputTokens,
|
||||
estimateMessageTokens: options.estimateMessageTokens,
|
||||
});
|
||||
if (summaryInputBudget.status === "failed") {
|
||||
options.logger?.log(
|
||||
"Skipped agentic compaction: summary input budget failed",
|
||||
{
|
||||
severity: "warn",
|
||||
budgetWarnings: summaryInputBudget.warnings.map(
|
||||
(warning) => warning.code,
|
||||
),
|
||||
summaryInputEstimatedTokens: summaryInputBudget.estimatedTokens,
|
||||
targetTokens: availableSummaryInputTokens,
|
||||
summarizerProviderId: summarizerProviderConfig.providerId,
|
||||
summarizerModelId: summarizerProviderConfig.modelId,
|
||||
},
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
const fileOps = extractFileOps(summaryInputBudget.messages);
|
||||
const conversationText = serializeConversation(summaryInputBudget.messages);
|
||||
const summaryRequest = buildSummaryRequest({
|
||||
previousSummary,
|
||||
conversationText,
|
||||
@@ -108,14 +221,20 @@ export async function runAgenticCompaction(options: {
|
||||
summaryRequestChars: summaryRequest.length,
|
||||
summaryRequestEstimatedTokens: estimateTokens(summaryRequest.length),
|
||||
newMessagesJsonChars: safeJsonSize(newMessagesToFold),
|
||||
summaryInputEstimatedTokens: summaryInputBudget.estimatedTokens,
|
||||
summaryInputActions: summaryInputBudget.actions.length,
|
||||
summaryInputWarnings: summaryInputBudget.warnings.map(
|
||||
(warning) => warning.code,
|
||||
),
|
||||
summaryRequestOverheadTokens,
|
||||
summarizerProviderId: summarizerProviderConfig.providerId,
|
||||
summarizerModelId: summarizerProviderConfig.modelId,
|
||||
summarizerInputLimit,
|
||||
maxInputTokens: options.context.maxInputTokens,
|
||||
triggerTokens: options.context.triggerTokens,
|
||||
});
|
||||
const rawSummary = await generateSummary({
|
||||
providerConfig: resolveSummarizerConfig({
|
||||
activeProviderConfig: options.providerConfig,
|
||||
summarizer: options.summarizer,
|
||||
}),
|
||||
providerConfig: summarizerProviderConfig,
|
||||
request: summaryRequest,
|
||||
logger: options.logger,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
buildBudgetProjection,
|
||||
findLatestTypedUserMessageIndex,
|
||||
} from "./project";
|
||||
export type {
|
||||
BlockBudgetClass,
|
||||
BudgetAction,
|
||||
BudgetActionKind,
|
||||
BudgetActionReason,
|
||||
BudgetPath,
|
||||
BudgetPolicyIntent,
|
||||
BudgetProjectionOptions,
|
||||
BudgetProjectionResult,
|
||||
BudgetProjectionWarning,
|
||||
ContentBlockBudgetClassification,
|
||||
LiveTailHandling,
|
||||
} from "./types";
|
||||
@@ -0,0 +1,476 @@
|
||||
import type { MessageWithMetadata } from "@cline/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildBudgetProjection,
|
||||
findLatestTypedUserMessageIndex,
|
||||
} from "./project";
|
||||
|
||||
const estimateChars = (message: MessageWithMetadata) =>
|
||||
JSON.stringify(message).length;
|
||||
|
||||
describe("buildBudgetProjection", () => {
|
||||
it("fails explicitly for impossible budgets", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [{ role: "user", content: "keep me" }],
|
||||
targetTokens: 0,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("failed");
|
||||
expect(result.messages).toHaveLength(1);
|
||||
expect(result.warnings[0]?.code).toBe("budget_impossible");
|
||||
});
|
||||
|
||||
it("drops unsafe image and redacted thinking blocks instead of truncating them", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "old context" },
|
||||
{
|
||||
type: "redacted_thinking",
|
||||
data: "x".repeat(500),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
data: "y".repeat(500),
|
||||
mediaType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "latest task" },
|
||||
],
|
||||
targetTokens: 150,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(result.messages);
|
||||
expect(serialized).not.toContain("redacted_thinking");
|
||||
expect(serialized).not.toContain("image/png");
|
||||
expect(result.actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "dropped_block",
|
||||
reason: "unsafe_to_truncate",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(result.liveTailHandling).toBe("included_degraded");
|
||||
});
|
||||
|
||||
it("keeps unsafe blocks when input is already under budget", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "look at this" },
|
||||
{
|
||||
type: "image",
|
||||
data: "small-image",
|
||||
mediaType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
targetTokens: 1_000,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("ok");
|
||||
expect(result.actions).toEqual([]);
|
||||
expect(result.liveTailHandling).toBe("included_verbatim");
|
||||
expect(JSON.stringify(result.messages)).toContain("small-image");
|
||||
});
|
||||
|
||||
it("preserves unsafe blocks in the latest typed user message", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{ role: "user", content: "old task " + "x".repeat(500) },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "what is in this image?" },
|
||||
{
|
||||
type: "image",
|
||||
data: "live-image",
|
||||
mediaType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
targetTokens: 120,
|
||||
policyIntent: "basic_compaction_projection",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
expect(JSON.stringify(result.messages)).toContain("live-image");
|
||||
expect(result.actions).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "dropped_block" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("protects latest typed user after thinking-only messages are pruned", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{ role: "user", content: "old task" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking: "discard me" }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "what is in this image?" },
|
||||
{
|
||||
type: "image",
|
||||
data: "live-image",
|
||||
mediaType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
targetTokens: 1_000,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(result.messages);
|
||||
expect(serialized).toContain("live-image");
|
||||
expect(serialized).not.toContain("discard me");
|
||||
});
|
||||
|
||||
it("keeps tool-use and tool-result pairs coherent when dropping history", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{ role: "user", content: "original task" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool_1",
|
||||
name: "read_files",
|
||||
input: { file_paths: ["/tmp/a.ts"] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool_1",
|
||||
name: "read_files",
|
||||
content: "x".repeat(1000),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "latest task" },
|
||||
],
|
||||
targetTokens: 140,
|
||||
policyIntent: "basic_compaction_projection",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(result.messages);
|
||||
expect(serialized).not.toContain("tool_1");
|
||||
expect(serialized).toContain("latest task");
|
||||
expect(result.actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ reason: "tool_pair_boundary" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("records budget action paths against original message indexes", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "image", data: "x", mediaType: "image/png" }],
|
||||
},
|
||||
{ role: "user", content: "old task " + "x".repeat(500) },
|
||||
{ role: "assistant", content: "old answer " + "y".repeat(500) },
|
||||
{ role: "user", content: "latest task" },
|
||||
],
|
||||
targetTokens: 80,
|
||||
policyIntent: "basic_compaction_projection",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
expect(result.actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "preserved",
|
||||
path: expect.objectContaining({ messageIndex: 1 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dropped_message",
|
||||
path: expect.objectContaining({ messageIndex: 2 }),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("detects the latest typed user message when tool results follow it", () => {
|
||||
const messages: MessageWithMetadata[] = [
|
||||
{ role: "user", content: "old task" },
|
||||
{ role: "user", content: "latest typed prompt" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool_1", name: "read", input: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool_1",
|
||||
name: "read",
|
||||
content: "result",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(findLatestTypedUserMessageIndex(messages)).toBe(1);
|
||||
});
|
||||
|
||||
it("preserves the latest typed prompt under pressure", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{ role: "user", content: "old task " + "x".repeat(500) },
|
||||
{ role: "user", content: "latest typed prompt" },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool_1",
|
||||
name: "read",
|
||||
content: "result " + "y".repeat(500),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
targetTokens: 120,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
expect(JSON.stringify(result.messages)).toContain("latest typed prompt");
|
||||
expect(result.actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ reason: "protected_live_tail" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("drops completed tool pairs after the latest typed prompt", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{ role: "user", content: "old task " + "x".repeat(500) },
|
||||
{ role: "user", content: "latest typed prompt" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool_after", name: "read", input: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool_after",
|
||||
name: "read",
|
||||
content: "huge result " + "y".repeat(2_000),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
targetTokens: 140,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(result.messages);
|
||||
expect(serialized).toContain("latest typed prompt");
|
||||
expect(serialized).not.toContain("tool_after");
|
||||
expect(result.actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "dropped_message",
|
||||
reason: "tool_pair_boundary",
|
||||
path: expect.objectContaining({ messageIndex: 2 }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dropped_message",
|
||||
reason: "tool_pair_boundary",
|
||||
path: expect.objectContaining({ messageIndex: 3 }),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves unresolved tool use after the latest typed prompt", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{ role: "user", content: "old task " + "x".repeat(500) },
|
||||
{ role: "user", content: "latest typed prompt" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool_live",
|
||||
name: "run_command",
|
||||
input: { command: "sleep 1" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
targetTokens: 80,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(result.messages);
|
||||
expect(serialized).toContain("latest typed prompt");
|
||||
expect(serialized).toContain("tool_live");
|
||||
expect(result.status).toBe("failed");
|
||||
expect(result.warnings[0]?.code).toBe(
|
||||
"budget_unachievable_with_protections",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not preserve later text or file blocks after tool-result budget is exhausted", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{ role: "user", content: "latest typed prompt" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", id: "tool_live", name: "read", input: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool_live",
|
||||
name: "read",
|
||||
content: [
|
||||
{ type: "text", text: "a".repeat(200) },
|
||||
{ type: "file", path: "/tmp/huge.txt", content: "b".repeat(1_000) },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
targetTokens: 260,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(result.messages);
|
||||
expect(serialized).toContain("latest typed prompt");
|
||||
expect(serialized).not.toContain("b".repeat(100));
|
||||
expect(result.actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "truncated_text",
|
||||
reason: "over_budget",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("drops thinking blocks instead of mutating provider-native reasoning", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{ role: "user", content: "latest typed prompt" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "a".repeat(1_000) },
|
||||
{ type: "thinking", thinking: "b".repeat(1_000) },
|
||||
],
|
||||
},
|
||||
],
|
||||
targetTokens: 900,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
const assistant = result.messages.find(
|
||||
(message) => message.role === "assistant",
|
||||
);
|
||||
expect(JSON.stringify(assistant)).not.toContain("b".repeat(100));
|
||||
expect(JSON.stringify(assistant)).not.toContain("\"thinking\"");
|
||||
expect(result.actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "dropped_block",
|
||||
reason: "unsafe_to_truncate",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("drops nested unsafe tool-result blocks outside the protected tail", () => {
|
||||
const result = buildBudgetProjection({
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool_old",
|
||||
name: "read",
|
||||
content: [
|
||||
{ type: "text", text: "old output" },
|
||||
{
|
||||
type: "image",
|
||||
data: "old-image-data",
|
||||
mediaType: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "latest typed prompt" },
|
||||
],
|
||||
targetTokens: 1_000,
|
||||
policyIntent: "agentic_summary",
|
||||
estimateMessageTokens: estimateChars,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(result.messages);
|
||||
expect(serialized).toContain("old output");
|
||||
expect(serialized).not.toContain("old-image-data");
|
||||
expect(result.actions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "dropped_block",
|
||||
reason: "unsafe_to_truncate",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,670 @@
|
||||
import type {
|
||||
ContentBlock,
|
||||
MessageWithMetadata,
|
||||
ToolResultContent,
|
||||
} from "@cline/shared";
|
||||
import type {
|
||||
BudgetAction,
|
||||
BudgetMutationAction,
|
||||
BudgetProjectionOptions,
|
||||
BudgetProjectionResult,
|
||||
BudgetProjectionWarning,
|
||||
BudgetPolicyIntent,
|
||||
} from "./types";
|
||||
|
||||
type EstimateMessageTokens = (message: MessageWithMetadata) => number;
|
||||
|
||||
interface ProjectionPolicy {
|
||||
protectLatestTypedUser: boolean;
|
||||
protectLiveTailFromDrop: boolean;
|
||||
dropUnsafeOutsideLiveTail: boolean;
|
||||
dropThinkingBlocks: boolean;
|
||||
}
|
||||
|
||||
function resolveProjectionPolicy(
|
||||
intent: BudgetPolicyIntent,
|
||||
): ProjectionPolicy {
|
||||
switch (intent) {
|
||||
case "agentic_summary":
|
||||
case "basic_compaction_projection":
|
||||
return {
|
||||
protectLatestTypedUser: true,
|
||||
protectLiveTailFromDrop: true,
|
||||
dropUnsafeOutsideLiveTail: true,
|
||||
dropThinkingBlocks: true,
|
||||
};
|
||||
case "normal_provider_request":
|
||||
return {
|
||||
protectLatestTypedUser: true,
|
||||
protectLiveTailFromDrop: true,
|
||||
dropUnsafeOutsideLiveTail: false,
|
||||
dropThinkingBlocks: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function cloneMessages(messages: MessageWithMetadata[]): MessageWithMetadata[] {
|
||||
return messages.map((message) => ({
|
||||
...message,
|
||||
content: Array.isArray(message.content)
|
||||
? message.content.map((block) => ({ ...block }) as ContentBlock)
|
||||
: message.content,
|
||||
...(message.metadata ? { metadata: { ...message.metadata } } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
function safeJsonSize(value: unknown): number {
|
||||
try {
|
||||
return JSON.stringify(value).length;
|
||||
} catch {
|
||||
return String(value).length;
|
||||
}
|
||||
}
|
||||
|
||||
function totalTokens(
|
||||
messages: MessageWithMetadata[],
|
||||
estimateMessageTokens: EstimateMessageTokens,
|
||||
): number {
|
||||
return messages.reduce(
|
||||
(total, message) => total + estimateMessageTokens(message),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
function isToolResultOnlyUserMessage(message: MessageWithMetadata): boolean {
|
||||
return (
|
||||
message.role === "user" &&
|
||||
Array.isArray(message.content) &&
|
||||
message.content.length > 0 &&
|
||||
message.content.every((block) => block.type === "tool_result")
|
||||
);
|
||||
}
|
||||
|
||||
export function findLatestTypedUserMessageIndex(
|
||||
messages: MessageWithMetadata[],
|
||||
): number {
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (message.role === "user" && !isToolResultOnlyUserMessage(message)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function findFirstTypedUserMessageIndex(
|
||||
messages: MessageWithMetadata[],
|
||||
): number {
|
||||
for (let index = 0; index < messages.length; index += 1) {
|
||||
const message = messages[index];
|
||||
if (message.role === "user" && !isToolResultOnlyUserMessage(message)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function collectToolIds(message: MessageWithMetadata): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
if (!Array.isArray(message.content)) {
|
||||
return ids;
|
||||
}
|
||||
for (const block of message.content) {
|
||||
if (block.type === "tool_use") {
|
||||
ids.add(block.id);
|
||||
} else if (block.type === "tool_result") {
|
||||
ids.add(block.tool_use_id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function buildToolPairIndex(
|
||||
messages: MessageWithMetadata[],
|
||||
): Map<string, Set<number>> {
|
||||
const index = new Map<string, Set<number>>();
|
||||
for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) {
|
||||
for (const id of collectToolIds(messages[messageIndex])) {
|
||||
const existing = index.get(id);
|
||||
if (existing) {
|
||||
existing.add(messageIndex);
|
||||
} else {
|
||||
index.set(id, new Set([messageIndex]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function findProtectedTailStartIndex(messages: MessageWithMetadata[]): number {
|
||||
const resolvedToolUseIds = new Set<string>();
|
||||
for (const message of messages) {
|
||||
if (!Array.isArray(message.content)) {
|
||||
continue;
|
||||
}
|
||||
for (const block of message.content) {
|
||||
if (block.type === "tool_result") {
|
||||
resolvedToolUseIds.add(block.tool_use_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
if (!Array.isArray(message.content)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
message.content.some(
|
||||
(block) =>
|
||||
block.type === "tool_use" && !resolvedToolUseIds.has(block.id),
|
||||
)
|
||||
) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return messages.length;
|
||||
}
|
||||
|
||||
function collectMessageClosure(
|
||||
messages: MessageWithMetadata[],
|
||||
startIndex: number,
|
||||
): Set<number> {
|
||||
const pairIndex = buildToolPairIndex(messages);
|
||||
const removal = new Set<number>();
|
||||
const queue = [startIndex];
|
||||
while (queue.length > 0) {
|
||||
const index = queue.shift();
|
||||
if (index === undefined || removal.has(index)) {
|
||||
continue;
|
||||
}
|
||||
removal.add(index);
|
||||
for (const id of collectToolIds(messages[index])) {
|
||||
for (const linked of pairIndex.get(id) ?? []) {
|
||||
if (!removal.has(linked)) {
|
||||
queue.push(linked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return removal;
|
||||
}
|
||||
|
||||
function isUnsafeBlock(block: ContentBlock): boolean {
|
||||
return block.type === "image" || block.type === "redacted_thinking";
|
||||
}
|
||||
|
||||
function isNestedUnsafeToolResultBlock(
|
||||
block: Extract<ToolResultContent["content"], unknown[]>[number],
|
||||
): boolean {
|
||||
return block.type === "image";
|
||||
}
|
||||
|
||||
function shouldDropWholeBlock(
|
||||
block: ContentBlock,
|
||||
policy: ProjectionPolicy,
|
||||
isProtected: boolean,
|
||||
): boolean {
|
||||
if (policy.dropThinkingBlocks && block.type === "thinking") {
|
||||
return true;
|
||||
}
|
||||
return policy.dropUnsafeOutsideLiveTail && !isProtected && isUnsafeBlock(block);
|
||||
}
|
||||
|
||||
function pruneEmptyMessages(
|
||||
messages: MessageWithMetadata[],
|
||||
originalIndexes: number[],
|
||||
actions: BudgetAction[],
|
||||
reason: BudgetMutationAction["reason"] = "over_budget",
|
||||
): { messages: MessageWithMetadata[]; originalIndexes: number[] } {
|
||||
const next: MessageWithMetadata[] = [];
|
||||
const nextOriginalIndexes: number[] = [];
|
||||
for (let index = 0; index < messages.length; index += 1) {
|
||||
const message = messages[index];
|
||||
if (Array.isArray(message.content) && message.content.length === 0) {
|
||||
actions.push({
|
||||
kind: "dropped_message",
|
||||
path: { messageIndex: originalIndexes[index] },
|
||||
reason,
|
||||
originalSize: safeJsonSize(message),
|
||||
finalSize: 0,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
next.push(message);
|
||||
nextOriginalIndexes.push(originalIndexes[index]);
|
||||
}
|
||||
return { messages: next, originalIndexes: nextOriginalIndexes };
|
||||
}
|
||||
|
||||
function dropUnsafeBlocks(
|
||||
messages: MessageWithMetadata[],
|
||||
originalIndexes: number[],
|
||||
actions: BudgetAction[],
|
||||
latestTypedUserIndex: number,
|
||||
protectedTailStartIndex: number,
|
||||
policy: ProjectionPolicy,
|
||||
): MessageWithMetadata[] {
|
||||
return messages.map((message, messageIndex) => {
|
||||
if (!Array.isArray(message.content)) {
|
||||
return message;
|
||||
}
|
||||
let changed = false;
|
||||
const protectedBlock =
|
||||
messageIndex === latestTypedUserIndex ||
|
||||
messageIndex >= protectedTailStartIndex;
|
||||
const content = message.content.flatMap((block, blockIndex) => {
|
||||
if (shouldDropWholeBlock(block, policy, protectedBlock)) {
|
||||
changed = true;
|
||||
actions.push({
|
||||
kind: "dropped_block",
|
||||
path: { messageIndex: originalIndexes[messageIndex], blockIndex },
|
||||
reason: "unsafe_to_truncate",
|
||||
originalSize: safeJsonSize(block),
|
||||
finalSize: 0,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
if (block.type === "tool_result" && Array.isArray(block.content)) {
|
||||
const nestedContent = block.content.filter((nestedBlock) => {
|
||||
if (
|
||||
policy.dropUnsafeOutsideLiveTail &&
|
||||
!protectedBlock &&
|
||||
isNestedUnsafeToolResultBlock(nestedBlock)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (nestedContent.length !== block.content.length) {
|
||||
changed = true;
|
||||
const nextBlock = { ...block, content: nestedContent };
|
||||
actions.push({
|
||||
kind: "dropped_block",
|
||||
path: { messageIndex: originalIndexes[messageIndex], blockIndex },
|
||||
reason: "unsafe_to_truncate",
|
||||
originalSize: safeJsonSize(block),
|
||||
finalSize: safeJsonSize(nextBlock),
|
||||
});
|
||||
return [nextBlock];
|
||||
}
|
||||
}
|
||||
return [block];
|
||||
});
|
||||
return changed ? { ...message, content } : message;
|
||||
});
|
||||
}
|
||||
|
||||
function dropThinkingBlocks(
|
||||
messages: MessageWithMetadata[],
|
||||
originalIndexes: number[],
|
||||
actions: BudgetAction[],
|
||||
): MessageWithMetadata[] {
|
||||
return messages.map((message, messageIndex) => {
|
||||
if (!Array.isArray(message.content)) {
|
||||
return message;
|
||||
}
|
||||
let changed = false;
|
||||
const content = message.content.filter((block, blockIndex) => {
|
||||
if (block.type !== "thinking") {
|
||||
return true;
|
||||
}
|
||||
changed = true;
|
||||
actions.push({
|
||||
kind: "dropped_block",
|
||||
path: { messageIndex: originalIndexes[messageIndex], blockIndex },
|
||||
reason: "unsafe_to_truncate",
|
||||
originalSize: safeJsonSize(block),
|
||||
finalSize: 0,
|
||||
});
|
||||
return false;
|
||||
});
|
||||
return changed ? { ...message, content } : message;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function truncateText(text: string, maxChars: number): string {
|
||||
if (maxChars <= 0) {
|
||||
return "";
|
||||
}
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
}
|
||||
if (maxChars <= 16) {
|
||||
return text.slice(0, Math.max(1, maxChars));
|
||||
}
|
||||
const estimateMarker = `\n...[truncated ${text.length - maxChars} chars]`;
|
||||
const keep = Math.max(1, maxChars - estimateMarker.length);
|
||||
const marker = `\n...[truncated ${text.length - keep} chars]`;
|
||||
return `${text.slice(0, keep)}${marker}`;
|
||||
}
|
||||
|
||||
function truncateToolResultContent(
|
||||
content: ToolResultContent["content"],
|
||||
maxChars: number,
|
||||
): ToolResultContent["content"] {
|
||||
if (typeof content === "string") {
|
||||
return truncateText(content, maxChars);
|
||||
}
|
||||
let remaining = maxChars;
|
||||
return content.map((block) => {
|
||||
if (remaining <= 0) {
|
||||
if (block.type === "text") {
|
||||
return { ...block, text: "" };
|
||||
}
|
||||
if (block.type === "file") {
|
||||
return { ...block, content: "" };
|
||||
}
|
||||
return block;
|
||||
}
|
||||
if (block.type === "text") {
|
||||
const text = truncateText(block.text, remaining);
|
||||
remaining -= text.length;
|
||||
return { ...block, text };
|
||||
}
|
||||
if (block.type === "file") {
|
||||
const content = truncateText(block.content, remaining);
|
||||
remaining -= content.length;
|
||||
return { ...block, content };
|
||||
}
|
||||
return block;
|
||||
});
|
||||
}
|
||||
|
||||
function toolResultTextLength(content: ToolResultContent["content"]): number {
|
||||
if (typeof content === "string") {
|
||||
return content.length;
|
||||
}
|
||||
return content.reduce((total, block) => {
|
||||
if (block.type === "text") {
|
||||
return total + block.text.length;
|
||||
}
|
||||
if (block.type === "file") {
|
||||
return total + block.content.length;
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function truncateMessageText(
|
||||
message: MessageWithMetadata,
|
||||
maxChars: number,
|
||||
): MessageWithMetadata {
|
||||
if (typeof message.content === "string") {
|
||||
return { ...message, content: truncateText(message.content, maxChars) };
|
||||
}
|
||||
let remaining = maxChars;
|
||||
return {
|
||||
...message,
|
||||
content: message.content.map((block) => {
|
||||
if (remaining <= 0) {
|
||||
if (block.type === "text") {
|
||||
return { ...block, text: "" };
|
||||
}
|
||||
if (block.type === "file") {
|
||||
return { ...block, content: "" };
|
||||
}
|
||||
if (block.type === "tool_result") {
|
||||
return {
|
||||
...block,
|
||||
content: truncateToolResultContent(block.content, 0),
|
||||
};
|
||||
}
|
||||
return block;
|
||||
}
|
||||
if (block.type === "text") {
|
||||
const text = truncateText(block.text, remaining);
|
||||
remaining -= text.length;
|
||||
return { ...block, text };
|
||||
}
|
||||
if (block.type === "file") {
|
||||
const content = truncateText(block.content, remaining);
|
||||
remaining -= content.length;
|
||||
return { ...block, content };
|
||||
}
|
||||
if (block.type === "tool_result") {
|
||||
const content = truncateToolResultContent(block.content, remaining);
|
||||
remaining -= toolResultTextLength(content);
|
||||
return { ...block, content };
|
||||
}
|
||||
return block;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function hasTruncatableText(message: MessageWithMetadata): boolean {
|
||||
if (typeof message.content === "string") {
|
||||
return message.content.length > 0;
|
||||
}
|
||||
return message.content.some(
|
||||
(block) =>
|
||||
block.type === "text" ||
|
||||
block.type === "file" ||
|
||||
block.type === "tool_result",
|
||||
);
|
||||
}
|
||||
|
||||
function removeMessagesAt(
|
||||
messages: MessageWithMetadata[],
|
||||
originalIndexes: number[],
|
||||
removal: Set<number>,
|
||||
): { messages: MessageWithMetadata[]; originalIndexes: number[] } {
|
||||
return {
|
||||
messages: messages.filter((_, index) => !removal.has(index)),
|
||||
originalIndexes: originalIndexes.filter((_, index) => !removal.has(index)),
|
||||
};
|
||||
}
|
||||
|
||||
function closureTouchesProtectedTail(
|
||||
closure: Set<number>,
|
||||
protectedStartIndex: number,
|
||||
): boolean {
|
||||
if (protectedStartIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
for (const removalIndex of closure) {
|
||||
if (removalIndex >= protectedStartIndex) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function closureTouchesPinnedMessage(
|
||||
closure: Set<number>,
|
||||
pinnedIndex: number,
|
||||
): boolean {
|
||||
return pinnedIndex >= 0 && closure.has(pinnedIndex);
|
||||
}
|
||||
|
||||
export function buildBudgetProjection(
|
||||
options: BudgetProjectionOptions,
|
||||
): BudgetProjectionResult {
|
||||
const actions: BudgetAction[] = [];
|
||||
const warnings: BudgetProjectionWarning[] = [];
|
||||
const policy = resolveProjectionPolicy(options.policyIntent);
|
||||
if (options.targetTokens <= 0) {
|
||||
return {
|
||||
status: "failed",
|
||||
messages: cloneMessages(options.messages),
|
||||
actions,
|
||||
liveTailHandling: "preserved_out_of_band",
|
||||
estimatedTokens: totalTokens(
|
||||
options.messages,
|
||||
options.estimateMessageTokens,
|
||||
),
|
||||
warnings: [
|
||||
{
|
||||
code: "budget_impossible",
|
||||
message: "Target budget must be greater than zero.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
let messages = cloneMessages(options.messages);
|
||||
let originalIndexes = messages.map((_, index) => index);
|
||||
if (policy.dropThinkingBlocks) {
|
||||
const prunedThinking = pruneEmptyMessages(
|
||||
dropThinkingBlocks(messages, originalIndexes, actions),
|
||||
originalIndexes,
|
||||
actions,
|
||||
"unsafe_to_truncate",
|
||||
);
|
||||
messages = prunedThinking.messages;
|
||||
originalIndexes = prunedThinking.originalIndexes;
|
||||
}
|
||||
const latestTypedUserIndex = policy.protectLatestTypedUser
|
||||
? findLatestTypedUserMessageIndex(messages)
|
||||
: -1;
|
||||
const protectedTailStartIndex = policy.protectLiveTailFromDrop
|
||||
? findProtectedTailStartIndex(messages)
|
||||
: messages.length;
|
||||
if (policy.dropUnsafeOutsideLiveTail) {
|
||||
const prunedUnsafe = pruneEmptyMessages(
|
||||
dropUnsafeBlocks(
|
||||
messages,
|
||||
originalIndexes,
|
||||
actions,
|
||||
latestTypedUserIndex,
|
||||
protectedTailStartIndex,
|
||||
policy,
|
||||
),
|
||||
originalIndexes,
|
||||
actions,
|
||||
);
|
||||
messages = prunedUnsafe.messages;
|
||||
originalIndexes = prunedUnsafe.originalIndexes;
|
||||
}
|
||||
let estimatedTokens = totalTokens(messages, options.estimateMessageTokens);
|
||||
if (estimatedTokens <= options.targetTokens) {
|
||||
return {
|
||||
status: "ok",
|
||||
messages,
|
||||
actions,
|
||||
liveTailHandling:
|
||||
actions.length > 0 ? "included_degraded" : "included_verbatim",
|
||||
estimatedTokens,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
for (
|
||||
let index = messages.length - 1;
|
||||
index >= 0 && estimatedTokens > options.targetTokens;
|
||||
index -= 1
|
||||
) {
|
||||
const latestTypedUserIndex = findLatestTypedUserMessageIndex(messages);
|
||||
if (index === latestTypedUserIndex) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
policy.protectLiveTailFromDrop &&
|
||||
index >= findProtectedTailStartIndex(messages)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!hasTruncatableText(messages[index])) {
|
||||
continue;
|
||||
}
|
||||
const originalSize = safeJsonSize(messages[index]);
|
||||
const charsPerToken = Math.max(
|
||||
1,
|
||||
originalSize /
|
||||
Math.max(1, options.estimateMessageTokens(messages[index])),
|
||||
);
|
||||
const targetChars = Math.max(
|
||||
16,
|
||||
Math.floor(
|
||||
(options.targetTokens * charsPerToken) /
|
||||
Math.max(1, messages.length),
|
||||
),
|
||||
);
|
||||
messages[index] = truncateMessageText(messages[index], targetChars);
|
||||
actions.push({
|
||||
kind: "truncated_text",
|
||||
path: { messageIndex: originalIndexes[index] },
|
||||
reason: "over_budget",
|
||||
originalSize,
|
||||
finalSize: safeJsonSize(messages[index]),
|
||||
});
|
||||
estimatedTokens = totalTokens(messages, options.estimateMessageTokens);
|
||||
}
|
||||
|
||||
for (
|
||||
let index = 0;
|
||||
index < messages.length && estimatedTokens > options.targetTokens;
|
||||
) {
|
||||
const firstTypedUserIndex = findFirstTypedUserMessageIndex(messages);
|
||||
const latestTypedUserIndex = findLatestTypedUserMessageIndex(messages);
|
||||
const protectedStartIndex = policy.protectLiveTailFromDrop
|
||||
? findProtectedTailStartIndex(messages)
|
||||
: messages.length;
|
||||
if (index === firstTypedUserIndex || index === latestTypedUserIndex) {
|
||||
actions.push({
|
||||
kind: "preserved",
|
||||
path: { messageIndex: originalIndexes[index] },
|
||||
reason: "protected_live_tail",
|
||||
originalSize: safeJsonSize(messages[index]),
|
||||
finalSize: safeJsonSize(messages[index]),
|
||||
});
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const closure = collectMessageClosure(messages, index);
|
||||
if (closureTouchesPinnedMessage(closure, firstTypedUserIndex)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (closureTouchesPinnedMessage(closure, latestTypedUserIndex)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (closureTouchesProtectedTail(closure, protectedStartIndex)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
for (const removalIndex of closure) {
|
||||
actions.push({
|
||||
kind: "dropped_message",
|
||||
path: { messageIndex: originalIndexes[removalIndex] },
|
||||
reason:
|
||||
closure.size > 1 || collectToolIds(messages[removalIndex]).size > 0
|
||||
? "tool_pair_boundary"
|
||||
: "over_budget",
|
||||
originalSize: safeJsonSize(messages[removalIndex]),
|
||||
finalSize: 0,
|
||||
});
|
||||
}
|
||||
const removed = removeMessagesAt(messages, originalIndexes, closure);
|
||||
messages = removed.messages;
|
||||
originalIndexes = removed.originalIndexes;
|
||||
estimatedTokens = totalTokens(messages, options.estimateMessageTokens);
|
||||
}
|
||||
|
||||
if (estimatedTokens > options.targetTokens) {
|
||||
warnings.push({
|
||||
code: "budget_unachievable_with_protections",
|
||||
message:
|
||||
"Projection could not reach budget without violating protected content.",
|
||||
});
|
||||
return {
|
||||
status: "failed",
|
||||
messages,
|
||||
actions,
|
||||
liveTailHandling: "included_degraded",
|
||||
estimatedTokens,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ok",
|
||||
messages,
|
||||
actions,
|
||||
liveTailHandling:
|
||||
actions.length > 0 ? "included_degraded" : "included_verbatim",
|
||||
estimatedTokens,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { ContentBlock, MessageWithMetadata } from "@cline/shared";
|
||||
|
||||
export type BudgetPolicyIntent =
|
||||
| "agentic_summary"
|
||||
| "basic_compaction_projection"
|
||||
| "normal_provider_request";
|
||||
|
||||
export type BudgetActionKind =
|
||||
| "truncated_text"
|
||||
| "dropped_block"
|
||||
| "dropped_message"
|
||||
| "preserved";
|
||||
|
||||
export type BudgetActionReason =
|
||||
| "over_budget"
|
||||
| "unsafe_to_truncate"
|
||||
| "tool_pair_boundary"
|
||||
| "protected_live_tail";
|
||||
|
||||
export type LiveTailHandling =
|
||||
| "included_verbatim"
|
||||
| "included_degraded"
|
||||
| "summarized_as_context"
|
||||
| "omitted_with_warning"
|
||||
| "preserved_out_of_band";
|
||||
|
||||
export type BlockBudgetClass =
|
||||
| "text"
|
||||
| "thinking"
|
||||
| "tool_use"
|
||||
| "tool_result"
|
||||
| "unsafe_binary"
|
||||
| "unsafe_encrypted"
|
||||
| "opaque";
|
||||
|
||||
export interface BudgetPath {
|
||||
messageIndex: number;
|
||||
blockIndex?: number;
|
||||
}
|
||||
|
||||
interface BaseBudgetAction {
|
||||
path: BudgetPath;
|
||||
originalSize: number;
|
||||
finalSize: number;
|
||||
}
|
||||
|
||||
export type BudgetMutationAction =
|
||||
| (BaseBudgetAction & {
|
||||
kind: "truncated_text";
|
||||
reason: Extract<BudgetActionReason, "over_budget">;
|
||||
})
|
||||
| (BaseBudgetAction & {
|
||||
kind: "dropped_block";
|
||||
path: Required<BudgetPath>;
|
||||
reason: Exclude<BudgetActionReason, "protected_live_tail">;
|
||||
})
|
||||
| (BaseBudgetAction & {
|
||||
kind: "dropped_message";
|
||||
reason: Exclude<BudgetActionReason, "protected_live_tail">;
|
||||
});
|
||||
|
||||
export interface BudgetPreservedAction extends BaseBudgetAction {
|
||||
kind: "preserved";
|
||||
reason: Extract<
|
||||
BudgetActionReason,
|
||||
"protected_live_tail" | "tool_pair_boundary"
|
||||
>;
|
||||
}
|
||||
|
||||
export type BudgetAction = BudgetMutationAction | BudgetPreservedAction;
|
||||
|
||||
export type BudgetProjectionWarningCode =
|
||||
| "budget_impossible"
|
||||
| "budget_unachievable_with_protections";
|
||||
|
||||
export interface BudgetProjectionWarning {
|
||||
code: BudgetProjectionWarningCode;
|
||||
message: string;
|
||||
path?: BudgetPath;
|
||||
}
|
||||
|
||||
export interface BudgetProjectionOptions {
|
||||
messages: MessageWithMetadata[];
|
||||
targetTokens: number;
|
||||
policyIntent: BudgetPolicyIntent;
|
||||
estimateMessageTokens: (message: MessageWithMetadata) => number;
|
||||
}
|
||||
|
||||
export interface BudgetProjectionResult {
|
||||
status: "ok" | "failed";
|
||||
messages: MessageWithMetadata[];
|
||||
actions: BudgetAction[];
|
||||
liveTailHandling: LiveTailHandling;
|
||||
estimatedTokens: number;
|
||||
warnings: BudgetProjectionWarning[];
|
||||
}
|
||||
|
||||
export interface ContentBlockBudgetClassification {
|
||||
block: ContentBlock;
|
||||
budgetClass: BlockBudgetClass;
|
||||
canStringTruncate: boolean;
|
||||
canDropWholeBlock: boolean;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
} from "../../types/config";
|
||||
import type { ProviderConfig } from "../../types/provider-settings";
|
||||
|
||||
export const DEFAULT_MAX_INPUT_TOKENS = 200_000;
|
||||
export const DEFAULT_MAX_INPUT_TOKENS = 128_000;
|
||||
export const DEFAULT_THRESHOLD_RATIO = 0.9;
|
||||
export const DEFAULT_TARGET_RATIO = 0.7;
|
||||
export const DEFAULT_RESERVE_TOKENS = 16_384;
|
||||
@@ -485,6 +485,7 @@ export function resolveSummarizerConfig(options: {
|
||||
apiKey: summarizer.apiKey ?? baseProviderConfig?.apiKey,
|
||||
baseUrl: summarizer.baseUrl ?? baseProviderConfig?.baseUrl,
|
||||
headers: summarizer.headers ?? baseProviderConfig?.headers,
|
||||
modelInfo: summarizer.modelInfo ?? baseProviderConfig?.modelInfo,
|
||||
knownModels: summarizer.knownModels ?? baseProviderConfig?.knownModels,
|
||||
maxOutputTokens:
|
||||
summarizer.maxOutputTokens ?? DEFAULT_SUMMARY_MAX_OUTPUT_TOKENS,
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type * as LlmsProviders from "@cline/llms";
|
||||
import type { MessageWithMetadata } from "@cline/shared";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSessionCompactionState } from "../../session/models/session-compaction";
|
||||
import type { CoreCompactionContext } from "../../types/config";
|
||||
import { buildAgenticSummaryInputBudget } from "./agentic-compaction";
|
||||
import { runBasicCompaction } from "./basic-compaction";
|
||||
import { createContextCompactionPrepareTurn } from "./compaction";
|
||||
import {
|
||||
createCompactionStateAwarePrepareTurn,
|
||||
createContextCompactionPrepareTurn,
|
||||
} from "./compaction";
|
||||
import {
|
||||
createTokenEstimator,
|
||||
estimateTokens,
|
||||
resolveSummarizerConfig,
|
||||
serializeMessage,
|
||||
TOOL_RESULT_CHAR_LIMIT,
|
||||
@@ -516,6 +522,23 @@ describe("createContextCompactionPrepareTurn", () => {
|
||||
expect(anthropicConfig.maxOutputTokens).toBe(1_024);
|
||||
});
|
||||
|
||||
it("preserves summarizer modelInfo without a nested providerConfig", () => {
|
||||
const resolved = resolveSummarizerConfig({
|
||||
activeProviderConfig: {
|
||||
providerId: "anthropic",
|
||||
modelId: "primary-model",
|
||||
modelInfo: { id: "primary-model", maxInputTokens: 100_000 },
|
||||
} as LlmsProviders.ProviderConfig,
|
||||
summarizer: {
|
||||
providerId: "openai",
|
||||
modelId: "small-summary",
|
||||
modelInfo: { id: "small-summary", maxInputTokens: 600 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.modelInfo?.maxInputTokens).toBe(600);
|
||||
});
|
||||
|
||||
it("summarizes older messages and keeps recent messages", async () => {
|
||||
const emitStatusNotice = vi.fn();
|
||||
createHandlerMock.mockReturnValue({
|
||||
@@ -768,6 +791,43 @@ describe("createContextCompactionPrepareTurn", () => {
|
||||
expect(summarizerPrompt.length).toBeLessThan(longToolOutput.length);
|
||||
});
|
||||
|
||||
it("budgets agentic summary input before serialization", () => {
|
||||
const result = buildAgenticSummaryInputBudget({
|
||||
messages: [
|
||||
{ role: "user", content: "Run a large command" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-large",
|
||||
name: "execute_command",
|
||||
input: { command: "print-large-output" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-large",
|
||||
name: "execute_command",
|
||||
content: "x".repeat(50_000),
|
||||
},
|
||||
],
|
||||
},
|
||||
{ role: "user", content: "Latest typed prompt" },
|
||||
],
|
||||
targetTokens: 400,
|
||||
estimateMessageTokens: estimateJsonTokens,
|
||||
});
|
||||
|
||||
expect(result.estimatedTokens).toBeLessThanOrEqual(400);
|
||||
expect(JSON.stringify(result.messages)).toContain("Latest typed prompt");
|
||||
expect(result.actions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("never lands the agentic cut in the middle of a tool pair", async () => {
|
||||
// Repro for the "No tool call found for function call output" provider
|
||||
// error: findCutIndex used to walk back by token budget and could land
|
||||
@@ -942,6 +1002,79 @@ describe("createContextCompactionPrepareTurn", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("budgets agentic summary input against the configured summarizer context window", async () => {
|
||||
let summaryRequest = "";
|
||||
createHandlerMock.mockReturnValue({
|
||||
createMessage: vi.fn((_system: string, messages: LlmsProviders.Message[]) => {
|
||||
summaryRequest = String(messages[0]?.content ?? "");
|
||||
return streamChunks([
|
||||
{ type: "text", id: "summary-small", text: "## Goal\nSummarized" },
|
||||
{ type: "done", id: "summary-small", success: true },
|
||||
]);
|
||||
}),
|
||||
});
|
||||
|
||||
const summarizerLimit = 600;
|
||||
const oversizedAssistant = "assistant details ".repeat(5_000);
|
||||
const prepareTurn = createContextCompactionPrepareTurn({
|
||||
providerId: "anthropic",
|
||||
modelId: "primary-model",
|
||||
providerConfig: {
|
||||
providerId: "anthropic",
|
||||
modelId: "primary-model",
|
||||
modelInfo: { id: "primary-model", maxInputTokens: 10_000 },
|
||||
} as LlmsProviders.ProviderConfig,
|
||||
compaction: {
|
||||
enabled: true,
|
||||
strategy: "agentic",
|
||||
preserveRecentTokens: 1,
|
||||
reserveTokens: 5,
|
||||
summarizer: {
|
||||
providerId: "openai",
|
||||
modelId: "small-summary",
|
||||
modelInfo: {
|
||||
id: "small-summary",
|
||||
maxInputTokens: summarizerLimit,
|
||||
},
|
||||
},
|
||||
},
|
||||
logger: undefined,
|
||||
});
|
||||
|
||||
await prepareTurn?.({
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
parentAgentId: null,
|
||||
iteration: 1,
|
||||
abortSignal: new AbortController().signal,
|
||||
systemPrompt: "You are helpful.",
|
||||
tools: [],
|
||||
messages: [
|
||||
{ role: "user", content: "Old request" },
|
||||
{ role: "assistant", content: oversizedAssistant },
|
||||
{ role: "user", content: "Latest turn" },
|
||||
{ role: "assistant", content: "Latest answer" },
|
||||
],
|
||||
apiMessages: [
|
||||
{ role: "user", content: "Old request" },
|
||||
{ role: "assistant", content: oversizedAssistant },
|
||||
{ role: "user", content: "Latest turn" },
|
||||
{ role: "assistant", content: "Latest answer" },
|
||||
],
|
||||
model: {
|
||||
id: "primary-model",
|
||||
provider: "anthropic",
|
||||
info: { id: "primary-model", maxInputTokens: 10_000 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(createHandlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(estimateTokens(summaryRequest.length)).toBeLessThanOrEqual(
|
||||
summarizerLimit,
|
||||
);
|
||||
expect(summaryRequest).not.toContain(oversizedAssistant);
|
||||
});
|
||||
|
||||
it("uses basic compaction without calling the summarizer", async () => {
|
||||
const emitStatusNotice = vi.fn();
|
||||
const prepareTurn = createContextCompactionPrepareTurn({
|
||||
@@ -2273,4 +2406,49 @@ describe("createContextCompactionPrepareTurn", () => {
|
||||
|
||||
expect(secondResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps stale sidecar state when replacement compaction returns no result", async () => {
|
||||
const originalMessages: LlmsProviders.Message[] = [
|
||||
{ role: "user", content: "original" },
|
||||
];
|
||||
const existingState = createSessionCompactionState({
|
||||
sourceMessages: originalMessages,
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const compact = vi.fn().mockResolvedValue(undefined);
|
||||
const saveState = vi.fn();
|
||||
const prepareTurn = createCompactionStateAwarePrepareTurn({
|
||||
compact,
|
||||
getState: () => existingState,
|
||||
saveState,
|
||||
});
|
||||
const currentMessages: LlmsProviders.Message[] = [
|
||||
{ role: "user", content: "edited original" },
|
||||
{ role: "assistant", content: "tail" },
|
||||
];
|
||||
|
||||
const result = await prepareTurn({
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
parentAgentId: null,
|
||||
iteration: 1,
|
||||
abortSignal: new AbortController().signal,
|
||||
systemPrompt: "",
|
||||
tools: [],
|
||||
messages: currentMessages,
|
||||
apiMessages: currentMessages,
|
||||
model: {
|
||||
id: "mock-model",
|
||||
provider: "anthropic",
|
||||
info: { id: "mock-model", maxInputTokens: 100_000 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(compact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ messages: currentMessages }),
|
||||
);
|
||||
expect(saveState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,11 @@ import {
|
||||
captureCompactionSkipped,
|
||||
type TelemetryCompactionStrategy,
|
||||
} from "../../services/telemetry/core-events";
|
||||
import {
|
||||
createSessionCompactionState,
|
||||
projectSessionCompactionState,
|
||||
type SessionCompactionState,
|
||||
} from "../../session/models/session-compaction";
|
||||
import type {
|
||||
CoreCompactionConfig,
|
||||
CoreCompactionContext,
|
||||
@@ -44,6 +49,10 @@ export interface ContextPipelinePrepareTurnResult {
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
export type ContextPipelinePrepareTurn = (
|
||||
context: ContextPipelinePrepareTurnInput,
|
||||
) => Promise<ContextPipelinePrepareTurnResult | undefined>;
|
||||
|
||||
type EstimateMessageTokens = ReturnType<typeof createTokenEstimator>;
|
||||
|
||||
type BuiltinCompactionStrategyOptions = {
|
||||
@@ -508,3 +517,62 @@ export function createContextCompactionPrepareTurn(
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export function createCompactionStateAwarePrepareTurn(input: {
|
||||
compact?: ContextPipelinePrepareTurn;
|
||||
getState?: () => SessionCompactionState | undefined;
|
||||
saveState?: (state: SessionCompactionState) => void | Promise<void>;
|
||||
}): ContextPipelinePrepareTurn {
|
||||
return async (context) => {
|
||||
const existingState = input.getState?.();
|
||||
const projectedMessages = existingState
|
||||
? projectSessionCompactionState(existingState, context.messages)
|
||||
: undefined;
|
||||
if (existingState && projectedMessages) {
|
||||
// Re-compaction intentionally starts from the compacted projection plus
|
||||
// canonical tail. This keeps automatic turns bounded without rebuilding a
|
||||
// full-transcript summary every turn; manual `/compact` is the path for a
|
||||
// fresh summary from canonical history.
|
||||
const result = input.compact
|
||||
? await input.compact({
|
||||
...context,
|
||||
messages: projectedMessages,
|
||||
apiMessages: projectedMessages,
|
||||
})
|
||||
: undefined;
|
||||
if (result?.messages) {
|
||||
const systemPrompt = result.systemPrompt ?? existingState.system_prompt;
|
||||
const nextState = createSessionCompactionState({
|
||||
sourceMessages: context.messages,
|
||||
compactedMessages: result.messages,
|
||||
conversationId: context.conversationId,
|
||||
systemPrompt,
|
||||
});
|
||||
await input.saveState?.(nextState);
|
||||
return {
|
||||
...result,
|
||||
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
messages: projectedMessages,
|
||||
...(result?.systemPrompt !== undefined
|
||||
? { systemPrompt: result.systemPrompt }
|
||||
: existingState.system_prompt !== undefined
|
||||
? { systemPrompt: existingState.system_prompt }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
const result = input.compact ? await input.compact(context) : undefined;
|
||||
if (result?.messages) {
|
||||
const nextState = createSessionCompactionState({
|
||||
sourceMessages: context.messages,
|
||||
compactedMessages: result.messages,
|
||||
conversationId: context.conversationId,
|
||||
systemPrompt: result.systemPrompt,
|
||||
});
|
||||
await input.saveState?.(nextState);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1428,6 +1428,89 @@ describe("default read_files tool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("folds orphan range entries into the preceding file entry", async () => {
|
||||
const execute = vi.fn(
|
||||
async (request: { path: string }) => `content:${request.path}`,
|
||||
);
|
||||
const tool = createReadFilesTool(execute);
|
||||
|
||||
await tool.execute(
|
||||
{
|
||||
files: [
|
||||
{ path: "/tmp/example.ips" },
|
||||
{ start_line: 45, end_line: 100 },
|
||||
],
|
||||
} as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
},
|
||||
);
|
||||
await tool.execute(
|
||||
{ paths: ["/tmp/a.ts", { end_line: 4 }, "/tmp/b.ts"] } as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 2,
|
||||
},
|
||||
);
|
||||
|
||||
expect(execute).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ path: "/tmp/example.ips", start_line: 45, end_line: 100 },
|
||||
expect.objectContaining({ iteration: 1 }),
|
||||
);
|
||||
expect(execute).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ path: "/tmp/a.ts", end_line: 4 },
|
||||
expect.objectContaining({ iteration: 2 }),
|
||||
);
|
||||
expect(execute).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
{ path: "/tmp/b.ts" },
|
||||
expect.objectContaining({ iteration: 2 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects orphan range entries that cannot be attached to a file entry", async () => {
|
||||
const execute = vi.fn(async () => "should not run");
|
||||
const tool = createReadFilesTool(execute);
|
||||
|
||||
// Leading orphan range: no preceding file entry to fold into.
|
||||
await expect(
|
||||
tool.execute(
|
||||
{
|
||||
files: [{ start_line: 1, end_line: 2 }, { path: "/tmp/a.ts" }],
|
||||
} as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 1,
|
||||
},
|
||||
),
|
||||
).rejects.toThrow();
|
||||
|
||||
// Preceding entry already has its own range: keep the conflict visible.
|
||||
await expect(
|
||||
tool.execute(
|
||||
{
|
||||
files: [
|
||||
{ path: "/tmp/a.ts", start_line: 1 },
|
||||
{ start_line: 4, end_line: 8 },
|
||||
],
|
||||
} as never,
|
||||
{
|
||||
agentId: "agent-1",
|
||||
conversationId: "conv-1",
|
||||
iteration: 2,
|
||||
},
|
||||
),
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects invalid union inputs before calling the executor", async () => {
|
||||
const execute = vi.fn(async () => "should not run");
|
||||
const tool = createReadFilesTool(execute);
|
||||
@@ -1609,7 +1692,7 @@ describe("zod schema conversion", () => {
|
||||
required: ["path"],
|
||||
},
|
||||
description:
|
||||
"Array of file read requests. Omit start_line/end_line or set them to null to read from the start; provide integers to return only that inclusive one-based line range. Reads are capped, so page through long files with start_line/end_line. Prefer this tool over running terminal command to get file content for better performance and reliability.",
|
||||
"Array of file read requests; each element is one file and must include path. Omit start_line/end_line or set them to null to read from the start; provide integers on the same object as the path to return only that inclusive one-based line range — never emit a range as its own array element. Reads are capped, so page through long files with start_line/end_line. Prefer this tool over running terminal command to get file content for better performance and reliability.",
|
||||
});
|
||||
expect(inputSchema.required).toEqual(["files"]);
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
MAX_SEARCH_OUTPUT_CHARS,
|
||||
} from "./executors/output-limits";
|
||||
import {
|
||||
coalesceOrphanReadRanges,
|
||||
formatError,
|
||||
formatReadFileQuery,
|
||||
formatRunCommandQueryPreview,
|
||||
@@ -246,9 +247,9 @@ export function createReadFilesTool(
|
||||
return createTool<ReadFilesInput, ToolOperationResult[]>({
|
||||
name: "read_files",
|
||||
description:
|
||||
"Read the content of text or image files at the provided absolute paths, or return only an inclusive one-based line range when start_line/end_line are provided. " +
|
||||
"Read the content of text or image files at the provided absolute paths, or return only an inclusive one-based line range when start_line/end_line are provided on the same file entry as its path. " +
|
||||
"When you already know multiple files you need, read them together in one call, and call this tool in the same response as other independent tool calls. " +
|
||||
`Each read returns at most ${MAX_READ_LINES} lines / ~${Math.round(MAX_READ_OUTPUT_CHARS / 1024)}k characters; longer files report their total line count, page through them with start_line/end_line. ` +
|
||||
`Each read returns at most ${MAX_READ_LINES} lines / ~${Math.round(MAX_READ_OUTPUT_CHARS / 1024)}k characters; longer files report their total line count, page through them with start_line/end_line on that file's entry. ` +
|
||||
"Binary files that are not image and large files are not supported. " +
|
||||
"Returns file contents or error messages for each path. ",
|
||||
inputSchema: zodToJsonSchema(ReadFilesInputSchema),
|
||||
@@ -256,7 +257,10 @@ export function createReadFilesTool(
|
||||
retryable: true,
|
||||
maxRetries: 1,
|
||||
execute: async (input, context) => {
|
||||
const validate = validateWithZod(ReadFilesInputUnionSchema, input);
|
||||
const validate = validateWithZod(
|
||||
ReadFilesInputUnionSchema,
|
||||
coalesceOrphanReadRanges(input),
|
||||
);
|
||||
let requests: ReadFileRequest[];
|
||||
if (typeof validate === "string") {
|
||||
requests = [{ path: validate }];
|
||||
|
||||
@@ -77,6 +77,63 @@ export function getReadFileRangeError(request: ReadFileRequest): string | null {
|
||||
return `start_line must be less than or equal to end_line (received start_line: ${start_line}, end_line: ${end_line})`;
|
||||
}
|
||||
|
||||
const READ_RANGE_KEYS = new Set(["start_line", "end_line"]);
|
||||
|
||||
function isOrphanReadRangeEntry(
|
||||
value: unknown,
|
||||
): value is Record<string, unknown> {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const keys = Object.keys(value);
|
||||
return keys.length > 0 && keys.every((key) => READ_RANGE_KEYS.has(key));
|
||||
}
|
||||
|
||||
function coalesceOrphanReadRangeEntries(entries: unknown[]): unknown[] {
|
||||
const coalesced: unknown[] = [];
|
||||
for (const entry of entries) {
|
||||
if (isOrphanReadRangeEntry(entry)) {
|
||||
const previous = coalesced[coalesced.length - 1];
|
||||
if (typeof previous === "string") {
|
||||
coalesced[coalesced.length - 1] = { path: previous, ...entry };
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
previous !== null &&
|
||||
typeof previous === "object" &&
|
||||
!Array.isArray(previous) &&
|
||||
"path" in previous &&
|
||||
Object.keys(entry).every((key) => !(key in previous))
|
||||
) {
|
||||
coalesced[coalesced.length - 1] = { ...previous, ...entry };
|
||||
continue;
|
||||
}
|
||||
}
|
||||
coalesced.push(entry);
|
||||
}
|
||||
return coalesced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some models emit a file's line range as a separate array element instead of
|
||||
* placing start_line/end_line on the same object as its path. Fold such
|
||||
* orphan range entries into the preceding file entry before validation.
|
||||
*/
|
||||
export function coalesceOrphanReadRanges(input: unknown): unknown {
|
||||
if (Array.isArray(input)) {
|
||||
return coalesceOrphanReadRangeEntries(input);
|
||||
}
|
||||
if (input !== null && typeof input === "object") {
|
||||
for (const key of ["files", "paths"] as const) {
|
||||
const value = (input as Record<string, unknown>)[key];
|
||||
if (Array.isArray(value)) {
|
||||
return { ...input, [key]: coalesceOrphanReadRangeEntries(value) };
|
||||
}
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
export function normalizeRunCommandsInput(
|
||||
input: unknown,
|
||||
): Array<string | StructuredCommandInput> {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const ReadFileRequestSchema = z
|
||||
end_line: ReadFileLineRangeSchema.shape.end_line,
|
||||
})
|
||||
.describe(
|
||||
"A file read request with optional inclusive one-based line bounds",
|
||||
"A file read request with optional inclusive one-based line bounds. Always include path; start_line/end_line must be on the same object as the path they apply to, never in a separate array element",
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -56,7 +56,7 @@ export const ReadFilesInputSchema = z.object({
|
||||
files: z
|
||||
.array(ReadFileRequestSchema)
|
||||
.describe(
|
||||
"Array of file read requests. Omit start_line/end_line or set them to null to read from the start; provide integers to return only that inclusive one-based line range. Reads are capped, so page through long files with start_line/end_line. Prefer this tool over running terminal command to get file content for better performance and reliability.",
|
||||
"Array of file read requests; each element is one file and must include path. Omit start_line/end_line or set them to null to read from the start; provide integers on the same object as the path to return only that inclusive one-based line range — never emit a range as its own array element. Reads are capped, so page through long files with start_line/end_line. Prefer this tool over running terminal command to get file content for better performance and reliability.",
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentToolContext, HubEventEnvelope } from "@cline/shared";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSessionCompactionState } from "../../session/models/session-compaction";
|
||||
import { SessionSource } from "../../types/common";
|
||||
|
||||
const commandMock = vi.hoisted(() => vi.fn());
|
||||
@@ -1410,6 +1411,69 @@ describe("HubRuntimeHost", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("records rejected compaction state updates as handled errors", async () => {
|
||||
const telemetry = { capture: vi.fn() };
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages: [{ role: "user", content: "source" }],
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
conversationId: "sess-1",
|
||||
});
|
||||
commandMock.mockResolvedValue({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "session_wrong_client",
|
||||
message: "Session sess-1 is owned by other-client",
|
||||
},
|
||||
});
|
||||
|
||||
const { HubRuntimeHost } = await import("./hub-runtime-host");
|
||||
const host = new HubRuntimeHost({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
telemetry: telemetry as never,
|
||||
});
|
||||
|
||||
await expect(
|
||||
host.updateSessionCompactionState(" sess-1 ", state),
|
||||
).resolves.toEqual({ updated: false });
|
||||
expect(telemetry.capture).toHaveBeenCalledWith({
|
||||
event: "sdk.error",
|
||||
properties: expect.objectContaining({
|
||||
component: "core",
|
||||
operation: "hub.runtime_host.update_session_compaction_state",
|
||||
severity: "warn",
|
||||
handled: true,
|
||||
command: "session.compaction.update",
|
||||
sessionId: "sess-1",
|
||||
errorCode: "session_wrong_client",
|
||||
error_message: "Session sess-1 is owned by other-client",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("treats stale compaction state updates as non-error no-ops", async () => {
|
||||
const telemetry = { capture: vi.fn() };
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages: [{ role: "user", content: "source" }],
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
conversationId: "sess-1",
|
||||
});
|
||||
commandMock.mockResolvedValue({
|
||||
ok: true,
|
||||
payload: { updated: false },
|
||||
});
|
||||
|
||||
const { HubRuntimeHost } = await import("./hub-runtime-host");
|
||||
const host = new HubRuntimeHost({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
telemetry: telemetry as never,
|
||||
});
|
||||
|
||||
await expect(
|
||||
host.updateSessionCompactionState("sess-1", state),
|
||||
).resolves.toEqual({ updated: false });
|
||||
expect(telemetry.capture).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws when the hub rejects settings list", async () => {
|
||||
commandMock.mockResolvedValue({
|
||||
ok: false,
|
||||
|
||||
@@ -41,6 +41,10 @@ import type {
|
||||
} from "../../runtime/host/runtime-host";
|
||||
import { isSessionNotFoundError } from "../../runtime/host/runtime-host";
|
||||
import { RuntimeHostEventBus } from "../../runtime/host/runtime-host-support";
|
||||
import {
|
||||
parseSessionCompactionState,
|
||||
type SessionCompactionState,
|
||||
} from "../../session/models/session-compaction";
|
||||
import {
|
||||
type SessionManifest,
|
||||
SessionManifestSchema,
|
||||
@@ -821,6 +825,9 @@ export class HubRuntimeHost implements RuntimeHost {
|
||||
input.toolPolicies as Record<string, unknown> | undefined,
|
||||
),
|
||||
initialMessages: input.initialMessages,
|
||||
...(input.initialCompactionState
|
||||
? { initialCompactionState: input.initialCompactionState }
|
||||
: {}),
|
||||
});
|
||||
this.registerPlannedSession(
|
||||
plannedSessionId,
|
||||
@@ -958,6 +965,12 @@ export class HubRuntimeHost implements RuntimeHost {
|
||||
| Record<string, unknown>
|
||||
| undefined,
|
||||
),
|
||||
...(startConfig.initialCompactionState
|
||||
? {
|
||||
initialCompactionState:
|
||||
startConfig.initialCompactionState,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
@@ -1277,6 +1290,54 @@ export class HubRuntimeHost implements RuntimeHost {
|
||||
return { updated: reply.ok };
|
||||
}
|
||||
|
||||
async updateSessionCompactionState(
|
||||
sessionId: string,
|
||||
state: SessionCompactionState,
|
||||
): Promise<{ updated: boolean }> {
|
||||
const target = sessionId.trim();
|
||||
if (!target) return { updated: false };
|
||||
const reply = await this.client.command(
|
||||
"session.compaction.update",
|
||||
{ sessionId: target, state },
|
||||
target,
|
||||
);
|
||||
if (!reply.ok) {
|
||||
captureSdkError(this.telemetry, {
|
||||
component: "core",
|
||||
operation: "hub.runtime_host.update_session_compaction_state",
|
||||
error: new Error(
|
||||
hubReplyErrorMessage(reply, "session.compaction.update"),
|
||||
),
|
||||
severity: "warn",
|
||||
handled: true,
|
||||
context: {
|
||||
command: "session.compaction.update",
|
||||
sessionId: target,
|
||||
errorCode: reply.error?.code,
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
updated: reply.ok && reply.payload?.updated === true,
|
||||
};
|
||||
}
|
||||
|
||||
async readSessionCompactionState(
|
||||
sessionId: string,
|
||||
): Promise<SessionCompactionState | undefined> {
|
||||
const target = sessionId.trim();
|
||||
if (!target) return undefined;
|
||||
const reply = await this.client.command(
|
||||
"session.compaction.get",
|
||||
{ sessionId: target },
|
||||
target,
|
||||
);
|
||||
if (!reply.ok) {
|
||||
throw new Error(hubReplyErrorMessage(reply, "session.compaction.get"));
|
||||
}
|
||||
return parseSessionCompactionState(reply.payload?.state);
|
||||
}
|
||||
|
||||
async readSessionMessages(
|
||||
sessionId: string,
|
||||
): Promise<import("@cline/llms").Message[]> {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type StartSessionInput,
|
||||
type StartSessionResult,
|
||||
} from "../../runtime/host/runtime-host";
|
||||
import { createSessionCompactionState } from "../../session/models/session-compaction";
|
||||
import { createLocalHubScheduleRuntimeHandlers } from "../daemon/runtime-handlers";
|
||||
import { HubServerTransport } from "../server";
|
||||
import {
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
requestToolApproval,
|
||||
} from "./handlers/approval-handlers";
|
||||
import {
|
||||
ensureSessionParticipant,
|
||||
ensureSessionState,
|
||||
type HubTransportContext,
|
||||
} from "./handlers/context";
|
||||
@@ -797,6 +799,383 @@ describe("HubServerTransport boundaries", () => {
|
||||
expect(ctx.pendingCapabilityRequests.has("capreq-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not let session metadata updates overwrite server-owned compaction owner", async () => {
|
||||
const updateSession = vi.fn().mockResolvedValue({ updated: true });
|
||||
const transport = createTransport({
|
||||
sessionHost: {
|
||||
updateSession,
|
||||
},
|
||||
});
|
||||
|
||||
await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-update",
|
||||
command: "session.update",
|
||||
clientId: "attacker-client",
|
||||
sessionId: "session-1",
|
||||
payload: {
|
||||
metadata: {
|
||||
hubCapabilityOwnerClientId: "attacker-client",
|
||||
title: "safe title",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateSession).toHaveBeenCalledWith("session-1", {
|
||||
metadata: { title: "safe title" },
|
||||
});
|
||||
});
|
||||
|
||||
it("authorizes compaction sidecar access from server session state, not mutable metadata", async () => {
|
||||
const readSessionCompactionState = vi.fn();
|
||||
const transport = createTransport({
|
||||
sessionHost: {
|
||||
getSession: vi.fn().mockResolvedValue({
|
||||
sessionId: "session-1",
|
||||
status: "completed",
|
||||
startedAt: new Date(0).toISOString(),
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
workspaceRoot: "/tmp/project",
|
||||
cwd: "/tmp/project",
|
||||
metadata: { hubCapabilityOwnerClientId: "attacker-client" },
|
||||
}),
|
||||
readSessionCompactionState,
|
||||
},
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
ensureSessionState(ctx, "session-1", "owner-client", "creator");
|
||||
|
||||
const reply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact",
|
||||
command: "session.compaction.get",
|
||||
clientId: "attacker-client",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(reply).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "session_wrong_client" },
|
||||
});
|
||||
expect(readSessionCompactionState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not grant compaction sidecar ownership from session attach", async () => {
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages: [{ role: "user", content: "source" }],
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
conversationId: "session-1",
|
||||
});
|
||||
const readSessionCompactionState = vi.fn().mockResolvedValue(state);
|
||||
const updateSessionCompactionState = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ updated: true });
|
||||
const transport = createTransport({
|
||||
sessionHost: {
|
||||
readSessionCompactionState,
|
||||
updateSessionCompactionState,
|
||||
},
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
expect(ctx.sessionState.has("session-1")).toBe(false);
|
||||
|
||||
const attachReply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-attach",
|
||||
command: "session.attach",
|
||||
clientId: "viewer-client",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(attachReply).toMatchObject({ ok: true });
|
||||
expect(
|
||||
ctx.sessionState.get("session-1")?.createdByClientId,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
ctx.sessionState.get("session-1")?.participants.has("viewer-client"),
|
||||
).toBe(true);
|
||||
|
||||
const getReply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-get",
|
||||
command: "session.compaction.get",
|
||||
clientId: "viewer-client",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
const updateReply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-update",
|
||||
command: "session.compaction.update",
|
||||
clientId: "viewer-client",
|
||||
sessionId: "session-1",
|
||||
payload: { state },
|
||||
});
|
||||
|
||||
expect(getReply).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "session_wrong_client" },
|
||||
});
|
||||
expect(updateReply).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "session_wrong_client" },
|
||||
});
|
||||
expect(readSessionCompactionState).not.toHaveBeenCalled();
|
||||
expect(updateSessionCompactionState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows a creator to claim ownerless compaction sidecar ownership", async () => {
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages: [{ role: "user", content: "source" }],
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
conversationId: "session-1",
|
||||
});
|
||||
const readSessionCompactionState = vi.fn().mockResolvedValue(state);
|
||||
const transport = createTransport({
|
||||
sessionHost: { readSessionCompactionState },
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
ensureSessionParticipant(ctx, "session-1", "viewer-client", "participant");
|
||||
|
||||
expect(
|
||||
ctx.sessionState.get("session-1")?.createdByClientId,
|
||||
).toBeUndefined();
|
||||
|
||||
ensureSessionState(ctx, "session-1", "owner-client", "creator");
|
||||
|
||||
expect(ctx.sessionState.get("session-1")?.createdByClientId).toBe(
|
||||
"owner-client",
|
||||
);
|
||||
expect(
|
||||
ctx.sessionState.get("session-1")?.participants.has("viewer-client"),
|
||||
).toBe(true);
|
||||
|
||||
const getReply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-get",
|
||||
command: "session.compaction.get",
|
||||
clientId: "owner-client",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(getReply).toMatchObject({
|
||||
ok: true,
|
||||
payload: { state },
|
||||
});
|
||||
expect(readSessionCompactionState).toHaveBeenCalledWith("session-1");
|
||||
});
|
||||
|
||||
it("clears compaction sidecar ownership when the owner detaches", async () => {
|
||||
const readSessionCompactionState = vi.fn();
|
||||
const transport = createTransport({
|
||||
sessionHost: { readSessionCompactionState },
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
ensureSessionState(ctx, "session-1", "owner-client", "creator");
|
||||
ensureSessionParticipant(ctx, "session-1", "viewer-client", "participant");
|
||||
|
||||
const detachReply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-detach",
|
||||
command: "session.detach",
|
||||
clientId: "owner-client",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(detachReply).toMatchObject({ ok: true });
|
||||
expect(
|
||||
ctx.sessionState.get("session-1")?.participants.has("viewer-client"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
ctx.sessionState.get("session-1")?.createdByClientId,
|
||||
).toBeUndefined();
|
||||
|
||||
const getReply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-get",
|
||||
command: "session.compaction.get",
|
||||
clientId: "viewer-client",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(getReply).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "session_wrong_client" },
|
||||
});
|
||||
expect(readSessionCompactionState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears compaction sidecar ownership when the owner unregisters", async () => {
|
||||
const readSessionCompactionState = vi.fn();
|
||||
const transport = createTransport({
|
||||
sessionHost: { readSessionCompactionState },
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
ensureSessionState(ctx, "session-1", "owner-client", "creator");
|
||||
ensureSessionParticipant(ctx, "session-1", "viewer-client", "participant");
|
||||
|
||||
const unregisterReply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-unregister",
|
||||
command: "client.unregister",
|
||||
clientId: "owner-client",
|
||||
});
|
||||
|
||||
expect(unregisterReply).toMatchObject({ ok: true });
|
||||
expect(
|
||||
ctx.sessionState.get("session-1")?.participants.has("viewer-client"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
ctx.sessionState.get("session-1")?.createdByClientId,
|
||||
).toBeUndefined();
|
||||
|
||||
const getReply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-get",
|
||||
command: "session.compaction.get",
|
||||
clientId: "viewer-client",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(getReply).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "session_wrong_client" },
|
||||
});
|
||||
expect(readSessionCompactionState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns compaction sidecar state to the server-owned session client", async () => {
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages: [{ role: "user", content: "source" }],
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
conversationId: "session-1",
|
||||
});
|
||||
const readSessionCompactionState = vi.fn().mockResolvedValue(state);
|
||||
const transport = createTransport({
|
||||
sessionHost: { readSessionCompactionState },
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
ensureSessionState(ctx, "session-1", "owner-client", "creator");
|
||||
|
||||
const reply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-get",
|
||||
command: "session.compaction.get",
|
||||
clientId: "owner-client",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
expect(reply).toMatchObject({
|
||||
ok: true,
|
||||
payload: { sessionId: "session-1", state },
|
||||
});
|
||||
expect(readSessionCompactionState).toHaveBeenCalledWith("session-1");
|
||||
});
|
||||
|
||||
it("rejects invalid compaction sidecar updates before calling the session host", async () => {
|
||||
const updateSessionCompactionState = vi.fn();
|
||||
const transport = createTransport({
|
||||
sessionHost: { updateSessionCompactionState },
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
ensureSessionState(ctx, "session-1", "owner-client", "creator");
|
||||
|
||||
const reply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-update-invalid",
|
||||
command: "session.compaction.update",
|
||||
clientId: "owner-client",
|
||||
sessionId: "session-1",
|
||||
payload: { state: { version: 1, messages: "bad" } },
|
||||
});
|
||||
|
||||
expect(reply).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "invalid_compaction_state" },
|
||||
});
|
||||
expect(updateSessionCompactionState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("publishes session updates after successful compaction sidecar updates", async () => {
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages: [{ role: "user", content: "source" }],
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
conversationId: "session-1",
|
||||
});
|
||||
const updateSessionCompactionState = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ updated: true });
|
||||
const transport = createTransport({
|
||||
sessionHost: { updateSessionCompactionState },
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
const events: HubEventEnvelope[] = [];
|
||||
ensureSessionState(ctx, "session-1", "owner-client", "creator");
|
||||
transport.subscribe("owner-client", (event) => events.push(event));
|
||||
|
||||
const reply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-update",
|
||||
command: "session.compaction.update",
|
||||
clientId: "owner-client",
|
||||
sessionId: "session-1",
|
||||
payload: { state },
|
||||
});
|
||||
|
||||
expect(reply).toMatchObject({
|
||||
ok: true,
|
||||
payload: { updated: true },
|
||||
});
|
||||
expect(updateSessionCompactionState).toHaveBeenCalledWith(
|
||||
"session-1",
|
||||
state,
|
||||
);
|
||||
expect(events).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
event: "session.updated",
|
||||
sessionId: "session-1",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not publish session updates when compaction sidecar update is stale", async () => {
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages: [{ role: "user", content: "source" }],
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
conversationId: "session-1",
|
||||
});
|
||||
const updateSessionCompactionState = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ updated: false });
|
||||
const transport = createTransport({
|
||||
sessionHost: { updateSessionCompactionState },
|
||||
});
|
||||
const ctx = getContext(transport);
|
||||
const events: HubEventEnvelope[] = [];
|
||||
ensureSessionState(ctx, "session-1", "owner-client", "creator");
|
||||
transport.subscribe("owner-client", (event) => events.push(event));
|
||||
|
||||
const reply = await transport.handleCommand({
|
||||
version: "v1",
|
||||
requestId: "req-compact-stale",
|
||||
command: "session.compaction.update",
|
||||
clientId: "owner-client",
|
||||
sessionId: "session-1",
|
||||
payload: { state },
|
||||
});
|
||||
|
||||
expect(reply).toMatchObject({
|
||||
ok: true,
|
||||
payload: { updated: false },
|
||||
});
|
||||
expect(events).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ event: "session.updated" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("cancels pending capability requests when a run is aborted", async () => {
|
||||
const abort = vi.fn().mockResolvedValue(undefined);
|
||||
const transport = createTransport({
|
||||
|
||||
@@ -183,6 +183,9 @@ export function ensureSessionState(
|
||||
if (options.interactive !== undefined) {
|
||||
existing.interactive = options.interactive;
|
||||
}
|
||||
if (role === "creator" && !existing.createdByClientId) {
|
||||
existing.createdByClientId = clientId;
|
||||
}
|
||||
if (!existing.participants.has(clientId)) {
|
||||
existing.participants.set(clientId, {
|
||||
clientId,
|
||||
@@ -209,3 +212,41 @@ export function ensureSessionState(
|
||||
ctx.sessionState.set(sessionId, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
export function ensureSessionParticipant(
|
||||
ctx: HubTransportContext,
|
||||
sessionId: string,
|
||||
clientId: string,
|
||||
role: SessionParticipant["role"],
|
||||
options: { interactive?: boolean } = {},
|
||||
): HubSessionState {
|
||||
const existing = ctx.sessionState.get(sessionId);
|
||||
if (existing) {
|
||||
if (options.interactive !== undefined) {
|
||||
existing.interactive = options.interactive;
|
||||
}
|
||||
if (!existing.participants.has(clientId)) {
|
||||
existing.participants.set(clientId, {
|
||||
clientId,
|
||||
attachedAt: Date.now(),
|
||||
role,
|
||||
});
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
const state: HubSessionState = {
|
||||
interactive: options.interactive ?? true,
|
||||
participants: new Map([
|
||||
[
|
||||
clientId,
|
||||
{
|
||||
clientId,
|
||||
attachedAt: Date.now(),
|
||||
role,
|
||||
},
|
||||
],
|
||||
]),
|
||||
};
|
||||
ctx.sessionState.set(sessionId, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
} from "@cline/shared";
|
||||
import { createSessionId, parseRuntimeConfigExtensions } from "@cline/shared";
|
||||
import type { RuntimeSessionConfig } from "../../../runtime/host/runtime-host";
|
||||
import { parseSessionCompactionState } from "../../../session/models/session-compaction";
|
||||
import {
|
||||
SessionVersioningError,
|
||||
SessionVersioningService,
|
||||
@@ -19,6 +20,7 @@ import { toHubSessionRecord } from "../hub-session-records";
|
||||
import { cancelPendingCapabilityRequests } from "./capability-handlers";
|
||||
import {
|
||||
asPlainRecord,
|
||||
ensureSessionParticipant,
|
||||
ensureSessionState,
|
||||
errorReply,
|
||||
extractSessionId,
|
||||
@@ -30,18 +32,50 @@ import {
|
||||
|
||||
const CAPABILITY_OWNER_METADATA_KEY = "hubCapabilityOwnerClientId";
|
||||
|
||||
function setCapabilityOwner(
|
||||
metadata: Record<string, unknown>,
|
||||
clientId: string,
|
||||
): void {
|
||||
metadata[CAPABILITY_OWNER_METADATA_KEY] = clientId;
|
||||
function getCapabilityOwnerClientId(
|
||||
ctx: HubTransportContext,
|
||||
sessionId: string,
|
||||
): string | undefined {
|
||||
// Sidecar access follows the live hub owner, not persisted metadata clients
|
||||
// can replay or edit.
|
||||
return ctx.sessionState.get(sessionId)?.createdByClientId;
|
||||
}
|
||||
|
||||
function getCapabilityOwnerClientId(
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
): string | undefined {
|
||||
const owner = metadata?.[CAPABILITY_OWNER_METADATA_KEY];
|
||||
return typeof owner === "string" && owner.trim() ? owner.trim() : undefined;
|
||||
function stripServerOwnedSessionMetadata(
|
||||
metadata: Record<string, JsonValue | undefined> | undefined,
|
||||
): Record<string, JsonValue | undefined> | undefined {
|
||||
// Clients may echo old records back through session.update; keep ownership
|
||||
// on the live hub state only.
|
||||
if (!metadata || !(CAPABILITY_OWNER_METADATA_KEY in metadata)) {
|
||||
return metadata;
|
||||
}
|
||||
const sanitized = { ...metadata };
|
||||
delete sanitized[CAPABILITY_OWNER_METADATA_KEY];
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
function authorizeSessionCompactionAccess(input: {
|
||||
sessionId: string;
|
||||
ctx: HubTransportContext;
|
||||
clientId: string;
|
||||
envelope: HubCommandEnvelope;
|
||||
}): HubReplyEnvelope | undefined {
|
||||
const ownerClientId = getCapabilityOwnerClientId(input.ctx, input.sessionId);
|
||||
if (!ownerClientId) {
|
||||
return errorReply(
|
||||
input.envelope,
|
||||
"session_wrong_client",
|
||||
`Session ${input.sessionId} has no owner`,
|
||||
);
|
||||
}
|
||||
if (ownerClientId !== input.clientId) {
|
||||
return errorReply(
|
||||
input.envelope,
|
||||
"session_wrong_client",
|
||||
`Session ${input.sessionId} is owned by ${ownerClientId}`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function handleSessionCreate(
|
||||
@@ -77,6 +111,9 @@ export async function handleSessionCreate(
|
||||
payload.runtimeOptions && typeof payload.runtimeOptions === "object"
|
||||
? (payload.runtimeOptions as Record<string, unknown>)
|
||||
: {};
|
||||
const initialCompactionState = parseSessionCompactionState(
|
||||
payload.initialCompactionState,
|
||||
);
|
||||
if (typeof sessionConfig?.mode === "string") {
|
||||
metadata.mode = sessionConfig.mode;
|
||||
} else if (typeof runtimeOptions.mode === "string") {
|
||||
@@ -124,9 +161,6 @@ export async function handleSessionCreate(
|
||||
cwd: typeof payload.cwd === "string" ? payload.cwd : undefined,
|
||||
contributionCount: clientContributions.length,
|
||||
});
|
||||
if (clientContributions.length > 0) {
|
||||
setCapabilityOwner(metadata as Record<string, unknown>, clientId);
|
||||
}
|
||||
const requestedSessionId =
|
||||
typeof sessionConfig?.sessionId === "string"
|
||||
? sessionConfig.sessionId.trim()
|
||||
@@ -175,6 +209,7 @@ export async function handleSessionCreate(
|
||||
initialMessages: Array.isArray(payload.initialMessages)
|
||||
? (payload.initialMessages as never[])
|
||||
: undefined,
|
||||
initialCompactionState,
|
||||
localRuntime: {
|
||||
modelCatalogDefaults: {
|
||||
loadLatestOnInit: true,
|
||||
@@ -352,6 +387,9 @@ export async function handleSessionRestore(
|
||||
payload.runtimeOptions && typeof payload.runtimeOptions === "object"
|
||||
? (payload.runtimeOptions as Record<string, unknown>)
|
||||
: {};
|
||||
const initialCompactionState = parseSessionCompactionState(
|
||||
payload.initialCompactionState,
|
||||
);
|
||||
const metadata =
|
||||
payload.metadata && typeof payload.metadata === "object"
|
||||
? JSON.parse(JSON.stringify(payload.metadata))
|
||||
@@ -380,9 +418,6 @@ export async function handleSessionRestore(
|
||||
const clientContributions = parseHubClientContributions(
|
||||
runtimeOptions.clientContributions,
|
||||
);
|
||||
if (clientContributions.length > 0) {
|
||||
setCapabilityOwner(metadata as Record<string, unknown>, clientId);
|
||||
}
|
||||
const requestedSessionId =
|
||||
typeof sessionConfig?.sessionId === "string"
|
||||
? sessionConfig.sessionId.trim()
|
||||
@@ -439,6 +474,7 @@ export async function handleSessionRestore(
|
||||
restoredCheckpointRunCount: checkpointRunCount,
|
||||
},
|
||||
initialMessages: context.initialMessages,
|
||||
initialCompactionState,
|
||||
localRuntime: {
|
||||
modelCatalogDefaults: {
|
||||
loadLatestOnInit: true,
|
||||
@@ -586,23 +622,29 @@ export async function handleSessionAttach(
|
||||
"session.attach requires a session id",
|
||||
);
|
||||
}
|
||||
ensureSessionState(
|
||||
const session = await readHubSessionRecord(ctx, sessionId);
|
||||
if (!session) {
|
||||
return errorReply(
|
||||
envelope,
|
||||
"session_not_found",
|
||||
`Unknown session: ${sessionId}`,
|
||||
);
|
||||
}
|
||||
ensureSessionParticipant(
|
||||
ctx,
|
||||
sessionId,
|
||||
envelope.clientId?.trim() || "hub-client",
|
||||
"participant",
|
||||
);
|
||||
const session = await readHubSessionRecord(ctx, sessionId);
|
||||
if (session) {
|
||||
ctx.publish(ctx.buildEvent("session.attached", { session }, sessionId));
|
||||
}
|
||||
return session
|
||||
? okReply(envelope, { session })
|
||||
: errorReply(
|
||||
envelope,
|
||||
"session_not_found",
|
||||
`Unknown session: ${sessionId}`,
|
||||
);
|
||||
const attachedSession = await readHubSessionRecord(ctx, sessionId);
|
||||
ctx.publish(
|
||||
ctx.buildEvent(
|
||||
"session.attached",
|
||||
{ session: attachedSession ?? session },
|
||||
sessionId,
|
||||
),
|
||||
);
|
||||
return okReply(envelope, { session: attachedSession ?? session });
|
||||
}
|
||||
|
||||
export async function handleSessionDetach(
|
||||
@@ -618,18 +660,11 @@ export async function handleSessionDetach(
|
||||
);
|
||||
}
|
||||
const clientId = envelope.clientId?.trim() || "hub-client";
|
||||
const [existingSession] = await Promise.all([
|
||||
readHubSessionRecord(ctx, sessionId),
|
||||
]);
|
||||
const ownerClientId =
|
||||
getCapabilityOwnerClientId(
|
||||
existingSession?.metadata as Record<string, unknown> | undefined,
|
||||
) ?? clientId;
|
||||
const state = ctx.sessionState.get(sessionId);
|
||||
if (state) {
|
||||
state.participants.delete(clientId);
|
||||
if (state.createdByClientId === clientId) {
|
||||
state.createdByClientId = ownerClientId;
|
||||
state.createdByClientId = undefined;
|
||||
}
|
||||
if (state.participants.size === 0) {
|
||||
ctx.sessionState.delete(sessionId);
|
||||
@@ -702,6 +737,40 @@ export async function handleSessionMessages(
|
||||
return okReply(envelope, { sessionId, messages });
|
||||
}
|
||||
|
||||
export async function handleSessionCompactionGet(
|
||||
ctx: HubTransportContext,
|
||||
envelope: HubCommandEnvelope,
|
||||
): Promise<HubReplyEnvelope> {
|
||||
const sessionId = extractSessionId(envelope);
|
||||
if (!sessionId) {
|
||||
return errorReply(
|
||||
envelope,
|
||||
"invalid_session_id",
|
||||
"session.compaction.get requires a session id",
|
||||
);
|
||||
}
|
||||
const session = await readHubSessionRecord(ctx, sessionId);
|
||||
if (!session) {
|
||||
return errorReply(
|
||||
envelope,
|
||||
"session_not_found",
|
||||
`Unknown session: ${sessionId}`,
|
||||
);
|
||||
}
|
||||
const clientId = envelope.clientId?.trim() || "hub-client";
|
||||
const unauthorized = authorizeSessionCompactionAccess({
|
||||
sessionId,
|
||||
ctx,
|
||||
clientId,
|
||||
envelope,
|
||||
});
|
||||
if (unauthorized) {
|
||||
return unauthorized;
|
||||
}
|
||||
const state = await ctx.sessionHost.readSessionCompactionState(sessionId);
|
||||
return okReply(envelope, { sessionId, state });
|
||||
}
|
||||
|
||||
export async function handleSessionList(
|
||||
ctx: HubTransportContext,
|
||||
envelope: HubCommandEnvelope,
|
||||
@@ -722,7 +791,9 @@ export async function handleSessionUpdate(
|
||||
envelope: HubCommandEnvelope,
|
||||
): Promise<HubReplyEnvelope> {
|
||||
const sessionId = extractSessionId(envelope);
|
||||
const metadata = asPlainRecord(envelope.payload?.metadata);
|
||||
const metadata = stripServerOwnedSessionMetadata(
|
||||
asPlainRecord(envelope.payload?.metadata),
|
||||
);
|
||||
const updated = await ctx.sessionHost.updateSession(sessionId, { metadata });
|
||||
const [session, snapshot] = await Promise.all([
|
||||
readHubSessionRecord(ctx, sessionId),
|
||||
@@ -749,6 +820,82 @@ export async function handleSessionUpdate(
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleSessionCompactionUpdate(
|
||||
ctx: HubTransportContext,
|
||||
envelope: HubCommandEnvelope,
|
||||
): Promise<HubReplyEnvelope> {
|
||||
const sessionId = extractSessionId(envelope);
|
||||
if (!sessionId) {
|
||||
return errorReply(
|
||||
envelope,
|
||||
"invalid_session_id",
|
||||
"session.compaction.update requires a session id",
|
||||
);
|
||||
}
|
||||
const clientId = envelope.clientId?.trim() || "hub-client";
|
||||
const session = await readHubSessionRecord(ctx, sessionId);
|
||||
if (!session) {
|
||||
return errorReply(
|
||||
envelope,
|
||||
"session_not_found",
|
||||
`Unknown session: ${sessionId}`,
|
||||
);
|
||||
}
|
||||
const unauthorized = authorizeSessionCompactionAccess({
|
||||
sessionId,
|
||||
ctx,
|
||||
clientId,
|
||||
envelope,
|
||||
});
|
||||
if (unauthorized) {
|
||||
return unauthorized;
|
||||
}
|
||||
const payload =
|
||||
envelope.payload && typeof envelope.payload === "object"
|
||||
? envelope.payload
|
||||
: {};
|
||||
const state = parseSessionCompactionState(payload.state);
|
||||
if (!state) {
|
||||
return errorReply(
|
||||
envelope,
|
||||
"invalid_compaction_state",
|
||||
"session.compaction.update requires a valid compaction state",
|
||||
);
|
||||
}
|
||||
const updated = await ctx.sessionHost.updateSessionCompactionState(
|
||||
sessionId,
|
||||
state,
|
||||
);
|
||||
const [updatedSession, snapshot] = updated.updated
|
||||
? await Promise.all([
|
||||
readHubSessionRecord(ctx, sessionId),
|
||||
readCoreSessionSnapshot(ctx, sessionId),
|
||||
])
|
||||
: [session, undefined];
|
||||
if (updated.updated) {
|
||||
ctx.publish(
|
||||
ctx.buildEvent(
|
||||
"session.updated",
|
||||
{
|
||||
session: updatedSession ?? session,
|
||||
...(snapshot ? { snapshot } : {}),
|
||||
},
|
||||
sessionId,
|
||||
),
|
||||
);
|
||||
}
|
||||
return {
|
||||
version: envelope.version,
|
||||
requestId: envelope.requestId,
|
||||
ok: true,
|
||||
payload: {
|
||||
updated: updated.updated,
|
||||
session: updatedSession ?? session,
|
||||
...(snapshot ? { snapshot } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleSessionDelete(
|
||||
ctx: HubTransportContext,
|
||||
envelope: HubCommandEnvelope,
|
||||
|
||||
@@ -57,6 +57,8 @@ import {
|
||||
import { projectSessionEvent } from "./handlers/session-event-projector";
|
||||
import {
|
||||
handleSessionAttach,
|
||||
handleSessionCompactionGet,
|
||||
handleSessionCompactionUpdate,
|
||||
handleSessionCreate,
|
||||
handleSessionDelete,
|
||||
handleSessionDetach,
|
||||
@@ -361,10 +363,14 @@ export class HubServerTransport implements NativeHubTransport {
|
||||
return await handleSessionGet(this.ctx, envelope);
|
||||
case "session.messages":
|
||||
return await handleSessionMessages(this.ctx, envelope);
|
||||
case "session.compaction.get":
|
||||
return await handleSessionCompactionGet(this.ctx, envelope);
|
||||
case "session.list":
|
||||
return await handleSessionList(this.ctx, envelope);
|
||||
case "session.update":
|
||||
return await handleSessionUpdate(this.ctx, envelope);
|
||||
case "session.compaction.update":
|
||||
return await handleSessionCompactionUpdate(this.ctx, envelope);
|
||||
case "session.pending_prompts":
|
||||
return await handleSessionPendingPrompts(this.ctx, envelope);
|
||||
case "session.update_pending_prompt":
|
||||
@@ -547,6 +553,9 @@ export class HubServerTransport implements NativeHubTransport {
|
||||
private detachClientFromSessions(clientId: string): void {
|
||||
for (const [sessionId, state] of this.sessionState.entries()) {
|
||||
state.participants.delete(clientId);
|
||||
if (state.createdByClientId === clientId) {
|
||||
state.createdByClientId = undefined;
|
||||
}
|
||||
if (state.participants.size === 0) {
|
||||
this.sessionState.delete(sessionId);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { SessionAccumulatedUsage } from "../../runtime/host/runtime-host";
|
||||
import type { SessionRecord as LocalSessionRecord } from "../../types/sessions";
|
||||
|
||||
export type HubSessionState = {
|
||||
createdByClientId: string;
|
||||
createdByClientId?: string;
|
||||
interactive: boolean;
|
||||
participants: Map<string, SessionParticipant>;
|
||||
};
|
||||
|
||||
@@ -759,7 +759,10 @@ export async function loadOpenTelemetryAdapter() {
|
||||
return import("./services/telemetry/index.js");
|
||||
}
|
||||
export { Agent, createAgentRuntime } from "@cline/agents";
|
||||
export { createContextCompactionPrepareTurn } from "./extensions/context/compaction";
|
||||
export {
|
||||
createCompactionStateAwarePrepareTurn,
|
||||
createContextCompactionPrepareTurn,
|
||||
} from "./extensions/context/compaction";
|
||||
export {
|
||||
ALL_DEFAULT_TOOL_NAMES,
|
||||
type AskQuestionExecutor,
|
||||
@@ -879,6 +882,12 @@ export {
|
||||
TelemetryService,
|
||||
type TelemetryServiceOptions,
|
||||
} from "./services/telemetry/TelemetryService";
|
||||
export {
|
||||
createSessionCompactionState,
|
||||
parseSessionCompactionState,
|
||||
projectSessionCompactionState,
|
||||
type SessionCompactionState,
|
||||
} from "./session/models/session-compaction";
|
||||
// Compatibility barrel (legacy imports).
|
||||
export type { RuntimeEnvironment } from "./types";
|
||||
export type { SessionStatus } from "./types/common";
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
import { setClineDir, setHomeDir } from "@cline/shared/storage";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TelemetryService } from "../../services/telemetry/TelemetryService";
|
||||
import { createSessionCompactionState } from "../../session/models/session-compaction";
|
||||
import type { SessionManifest } from "../../session/models/session-manifest";
|
||||
import { SessionSource } from "../../types/common";
|
||||
import type { CoreSessionConfig } from "../../types/config";
|
||||
@@ -4336,6 +4337,212 @@ describe("LocalRuntimeHost", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("binds unowned initial compaction state to the started session", async () => {
|
||||
const sessionId = "sess-compaction-initial";
|
||||
const manifest = createManifest(sessionId);
|
||||
const initialMessages: MessageWithMetadata[] = [
|
||||
{ role: "user", content: "large source" },
|
||||
];
|
||||
const initialCompactionState = createSessionCompactionState({
|
||||
sourceMessages: initialMessages,
|
||||
compactedMessages: [{ role: "user", content: "summary" }],
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const sessionService = {
|
||||
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
||||
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
||||
manifestPath: "/tmp/manifest-compaction-initial.json",
|
||||
messagesPath: "/tmp/messages-compaction-initial.json",
|
||||
manifest,
|
||||
}),
|
||||
persistSessionMessages: vi.fn(),
|
||||
persistSessionCompactionState: vi.fn(),
|
||||
updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
|
||||
writeSessionManifest: vi.fn(),
|
||||
listSessions: vi.fn().mockResolvedValue([]),
|
||||
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
||||
};
|
||||
const run = vi.fn().mockResolvedValue(createResult());
|
||||
const createAgent = vi.fn().mockReturnValue({
|
||||
run,
|
||||
continue: vi.fn(),
|
||||
abort: vi.fn(),
|
||||
subscribeEvents: vi.fn().mockReturnValue(() => {}),
|
||||
canStartRun: vi.fn().mockReturnValue(true),
|
||||
getAgentId: vi.fn().mockReturnValue("agent-root-1"),
|
||||
getConversationId: vi.fn().mockReturnValue(sessionId),
|
||||
restore: vi.fn(),
|
||||
shutdown: vi.fn().mockResolvedValue(undefined),
|
||||
getMessages: vi.fn().mockReturnValue(initialMessages),
|
||||
messages: initialMessages,
|
||||
});
|
||||
const manager = new RuntimeHostUnderTest({
|
||||
distinctId,
|
||||
sessionService: sessionService as never,
|
||||
runtimeBuilder: {
|
||||
build: vi.fn().mockReturnValue({
|
||||
tools: [],
|
||||
shutdown: vi.fn(),
|
||||
}),
|
||||
},
|
||||
createAgent: createAgent as never,
|
||||
});
|
||||
|
||||
await manager.startSession(
|
||||
normalizeStartInput({
|
||||
config: createConfig({
|
||||
sessionId,
|
||||
compaction: {
|
||||
enabled: true,
|
||||
strategy: "basic",
|
||||
compact: vi.fn(),
|
||||
},
|
||||
}),
|
||||
initialMessages,
|
||||
initialCompactionState,
|
||||
interactive: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(sessionService.persistSessionCompactionState).toHaveBeenCalledWith(
|
||||
sessionId,
|
||||
expect.objectContaining({
|
||||
conversation_id: sessionId,
|
||||
messages: initialCompactionState.messages,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not project compaction state when compaction is disabled", async () => {
|
||||
const sessionId = "sess-compaction-disabled";
|
||||
const manifest = createManifest(sessionId);
|
||||
const initialMessages: MessageWithMetadata[] = [
|
||||
{ role: "user", content: "canonical source" },
|
||||
];
|
||||
const initialCompactionState = createSessionCompactionState({
|
||||
sourceMessages: initialMessages,
|
||||
compactedMessages: [{ role: "user", content: "projected summary" }],
|
||||
conversationId: sessionId,
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const sessionService = {
|
||||
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
||||
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
||||
manifestPath: "/tmp/manifest-compaction-disabled.json",
|
||||
messagesPath: "/tmp/messages-compaction-disabled.json",
|
||||
manifest,
|
||||
}),
|
||||
persistSessionMessages: vi.fn(),
|
||||
persistSessionCompactionState: vi.fn(),
|
||||
updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
|
||||
writeSessionManifest: vi.fn(),
|
||||
listSessions: vi.fn().mockResolvedValue([]),
|
||||
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
||||
};
|
||||
const run = vi.fn().mockResolvedValue(createResult());
|
||||
const createAgent = vi.fn().mockReturnValue({
|
||||
run,
|
||||
continue: vi.fn(),
|
||||
abort: vi.fn(),
|
||||
subscribeEvents: vi.fn().mockReturnValue(() => {}),
|
||||
canStartRun: vi.fn().mockReturnValue(true),
|
||||
getAgentId: vi.fn().mockReturnValue("agent-root-1"),
|
||||
getConversationId: vi.fn().mockReturnValue(sessionId),
|
||||
restore: vi.fn(),
|
||||
shutdown: vi.fn().mockResolvedValue(undefined),
|
||||
getMessages: vi.fn().mockReturnValue(initialMessages),
|
||||
messages: initialMessages,
|
||||
});
|
||||
const manager = new RuntimeHostUnderTest({
|
||||
distinctId,
|
||||
sessionService: sessionService as never,
|
||||
runtimeBuilder: {
|
||||
build: vi.fn().mockReturnValue({
|
||||
tools: [],
|
||||
shutdown: vi.fn(),
|
||||
}),
|
||||
},
|
||||
createAgent: createAgent as never,
|
||||
});
|
||||
|
||||
await manager.startSession(
|
||||
normalizeStartInput({
|
||||
config: createConfig({
|
||||
sessionId,
|
||||
compaction: {
|
||||
enabled: false,
|
||||
strategy: "basic",
|
||||
},
|
||||
}),
|
||||
initialMessages,
|
||||
initialCompactionState,
|
||||
interactive: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const prepareTurn = createAgent.mock.calls[0]?.[0]?.prepareTurn;
|
||||
expect(prepareTurn).toBeUndefined();
|
||||
expect(sessionService.persistSessionCompactionState).not.toHaveBeenCalled();
|
||||
|
||||
await expect(
|
||||
manager.updateSessionCompactionState(sessionId, initialCompactionState),
|
||||
).resolves.toEqual({ updated: true });
|
||||
expect(createAgent.mock.calls[0]?.[0]?.prepareTurn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("orders equal-length compaction updates by parsed timestamp", async () => {
|
||||
const sessionId = "inactive-session";
|
||||
const tempCwd = mkdtempSync(join(tmpdir(), "compaction-stale-"));
|
||||
try {
|
||||
const messagesPath = join(tempCwd, "messages.json");
|
||||
const sourceMessages: MessageWithMetadata[] = [
|
||||
{ role: "user", content: "source" },
|
||||
];
|
||||
writeFileSync(messagesPath, JSON.stringify(sourceMessages), "utf8");
|
||||
const current = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages: [{ role: "user", content: "current" }],
|
||||
conversationId: sessionId,
|
||||
updatedAt: "2026-01-01T00:00:00.500Z",
|
||||
});
|
||||
const incoming = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages: [{ role: "user", content: "incoming" }],
|
||||
conversationId: sessionId,
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
});
|
||||
const sessionService = {
|
||||
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
||||
listSessions: vi.fn().mockResolvedValue([
|
||||
{
|
||||
sessionId,
|
||||
provider: "mock-provider",
|
||||
model: "mock-model",
|
||||
cwd: "/tmp/project",
|
||||
workspaceRoot: "/tmp/project",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
status: "running",
|
||||
messagesPath,
|
||||
},
|
||||
]),
|
||||
readSessionCompactionState: vi.fn().mockResolvedValue(current),
|
||||
persistSessionCompactionState: vi.fn(),
|
||||
};
|
||||
const manager = new RuntimeHostUnderTest({
|
||||
distinctId,
|
||||
sessionService: sessionService as never,
|
||||
});
|
||||
|
||||
await expect(
|
||||
manager.updateSessionCompactionState(sessionId, incoming),
|
||||
).resolves.toEqual({ updated: false });
|
||||
expect(sessionService.persistSessionCompactionState).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
rmSync(tempCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("formats prompt in core and merges explicit + mention user files", async () => {
|
||||
const tempCwd = mkdtempSync(join(tmpdir(), "core-session-format-"));
|
||||
try {
|
||||
|
||||
@@ -14,7 +14,10 @@ import {
|
||||
} from "@cline/shared";
|
||||
import { setHomeDirIfUnset } from "@cline/shared/storage";
|
||||
import { isOAuthProvider } from "../../auth/provider-auth-registry";
|
||||
import { createContextCompactionPrepareTurn } from "../../extensions/context/compaction";
|
||||
import {
|
||||
createCompactionStateAwarePrepareTurn,
|
||||
createContextCompactionPrepareTurn,
|
||||
} from "../../extensions/context/compaction";
|
||||
import type { ToolExecutors } from "../../extensions/tools";
|
||||
import { DefaultToolNames } from "../../extensions/tools";
|
||||
import type { TeamEvent } from "../../extensions/tools/team";
|
||||
@@ -47,6 +50,10 @@ import {
|
||||
sumUsageTotals,
|
||||
} from "../../services/usage";
|
||||
import { enrichPromptWithMentions } from "../../services/workspace";
|
||||
import {
|
||||
projectSessionCompactionState,
|
||||
type SessionCompactionState,
|
||||
} from "../../session/models/session-compaction";
|
||||
import {
|
||||
type SessionManifest,
|
||||
SessionManifestSchema,
|
||||
@@ -171,6 +178,19 @@ function maxAccumulatedUsage(
|
||||
};
|
||||
}
|
||||
|
||||
function isIncomingCompactionStateStale(
|
||||
incoming: SessionCompactionState,
|
||||
current: SessionCompactionState | undefined,
|
||||
): boolean {
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
if (incoming.source_message_count !== current.source_message_count) {
|
||||
return incoming.source_message_count < current.source_message_count;
|
||||
}
|
||||
return Date.parse(incoming.updated_at) < Date.parse(current.updated_at);
|
||||
}
|
||||
|
||||
export interface LocalRuntimeHostOptions {
|
||||
distinctId?: string;
|
||||
sessionService: SessionBackend;
|
||||
@@ -343,6 +363,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
messages_path: messagesPath,
|
||||
});
|
||||
let resumedArtifacts: RootSessionArtifacts | undefined;
|
||||
let resumedCompactionState: SessionCompactionState | undefined;
|
||||
const isReadOnlyResumeStart =
|
||||
requestedSessionId.length > 0 &&
|
||||
initialMessages.length > 0 &&
|
||||
@@ -357,8 +378,14 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
resumedArtifacts = {
|
||||
manifestPath,
|
||||
messagesPath: existingManifest.messages_path || messagesPath,
|
||||
compactionPath: existingManifest.compaction_path,
|
||||
manifest: existingManifest,
|
||||
};
|
||||
resumedCompactionState =
|
||||
await this.invokeOptionalValue<SessionCompactionState>(
|
||||
"readSessionCompactionState",
|
||||
sessionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
const initialAggregateUsage = await this.seedAggregateUsageFromArtifacts({
|
||||
@@ -455,6 +482,65 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
|
||||
const tools = [...runtime.tools, ...(configWithProvider.extraTools ?? [])];
|
||||
const extensions = runtime.extensions ?? bootstrap.extensions;
|
||||
const explicitInitialCompactionState = startInput.initialCompactionState;
|
||||
let activeSessionRef: ActiveSession | undefined;
|
||||
const compact = createContextCompactionPrepareTurn(configWithProvider);
|
||||
const rawInitialCompactionState =
|
||||
explicitInitialCompactionState ?? resumedCompactionState;
|
||||
const initialCompactionState =
|
||||
compact && rawInitialCompactionState
|
||||
? {
|
||||
...rawInitialCompactionState,
|
||||
conversation_id:
|
||||
rawInitialCompactionState.conversation_id?.trim() || sessionId,
|
||||
}
|
||||
: undefined;
|
||||
const prepareTurn = compact
|
||||
? createCompactionStateAwarePrepareTurn({
|
||||
compact,
|
||||
getState: () => activeSessionRef?.compactionState,
|
||||
saveState: async (state) => {
|
||||
const activeSession = activeSessionRef;
|
||||
if (!activeSession) return;
|
||||
const stateForSession = {
|
||||
...state,
|
||||
conversation_id: activeSession.sessionId,
|
||||
};
|
||||
try {
|
||||
const result = await this.persistActiveSessionCompactionState(
|
||||
activeSession,
|
||||
stateForSession,
|
||||
);
|
||||
if (!result.updated) {
|
||||
configWithProvider.logger?.debug?.(
|
||||
"Skipped stale session compaction state",
|
||||
{
|
||||
sessionId: activeSession.sessionId,
|
||||
sourceMessageCount: stateForSession.source_message_count,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
configWithProvider.logger?.error?.(
|
||||
"Failed to persist session compaction state",
|
||||
{ sessionId: activeSession.sessionId, error },
|
||||
);
|
||||
captureSdkError(configWithProvider.telemetry, {
|
||||
component: "core",
|
||||
operation: "session.persist_compaction_state",
|
||||
severity: "warn",
|
||||
handled: true,
|
||||
error,
|
||||
context: {
|
||||
sessionId: activeSession.sessionId,
|
||||
providerId: configWithProvider.providerId,
|
||||
modelId: configWithProvider.modelId,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const agentConfig = {
|
||||
sessionId,
|
||||
@@ -472,7 +558,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
systemPrompt: configWithProvider.systemPrompt,
|
||||
maxIterations: configWithProvider.maxIterations,
|
||||
execution: configWithProvider.execution,
|
||||
prepareTurn: createContextCompactionPrepareTurn(configWithProvider),
|
||||
prepareTurn,
|
||||
tools,
|
||||
hooks: bootstrap.hooks,
|
||||
extensions,
|
||||
@@ -616,6 +702,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
aborting: false,
|
||||
interactive: input.interactive === true,
|
||||
persistedMessages: initialMessages,
|
||||
compactionState: initialCompactionState,
|
||||
activeTeamRunIds: new Set<string>(),
|
||||
pendingTeamRunUpdates: [],
|
||||
teamRunWaiters: [],
|
||||
@@ -625,6 +712,25 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
submitAndExitObserved: false,
|
||||
lastInteractiveTurnFinishReason: undefined,
|
||||
};
|
||||
activeSessionRef = active;
|
||||
if (
|
||||
active.compactionState &&
|
||||
!this.isCompactionStateForSession(
|
||||
active.sessionId,
|
||||
active.compactionState,
|
||||
active,
|
||||
)
|
||||
) {
|
||||
active.config.logger?.log?.(
|
||||
"Ignoring session compaction state for a different conversation",
|
||||
{
|
||||
severity: "warn",
|
||||
sessionId: active.sessionId,
|
||||
conversationId: active.compactionState.conversation_id,
|
||||
},
|
||||
);
|
||||
active.compactionState = undefined;
|
||||
}
|
||||
this.sessions.set(sessionId, active);
|
||||
this.emitStatus(sessionId, "running");
|
||||
if (initialMessages.length > 0 && !resumedArtifacts) {
|
||||
@@ -635,6 +741,15 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
initialMessages,
|
||||
active.config.systemPrompt,
|
||||
);
|
||||
if (active.compactionState) {
|
||||
const result = await this.persistActiveSessionCompactionState(
|
||||
active,
|
||||
active.compactionState,
|
||||
);
|
||||
if (!result.updated) {
|
||||
active.compactionState = undefined;
|
||||
}
|
||||
}
|
||||
if (!startInput.prompt?.trim()) {
|
||||
await this.updateStatus(active, "completed", 0);
|
||||
}
|
||||
@@ -928,6 +1043,155 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
return { updated: result?.updated === true };
|
||||
}
|
||||
|
||||
async updateSessionCompactionState(
|
||||
sessionId: string,
|
||||
state: SessionCompactionState,
|
||||
): Promise<{ updated: boolean }> {
|
||||
const target = sessionId.trim();
|
||||
if (!target) return { updated: false };
|
||||
const activeSession = this.sessions.get(target);
|
||||
const sessionRecord = activeSession
|
||||
? undefined
|
||||
: await this.getSession(target);
|
||||
const existing = activeSession ?? sessionRecord;
|
||||
if (!existing) return { updated: false };
|
||||
if (
|
||||
!(await this.canPersistCompactionState(
|
||||
target,
|
||||
state,
|
||||
activeSession,
|
||||
sessionRecord,
|
||||
))
|
||||
) {
|
||||
return { updated: false };
|
||||
}
|
||||
if (activeSession) {
|
||||
return await this.persistActiveSessionCompactionState(
|
||||
activeSession,
|
||||
state,
|
||||
);
|
||||
}
|
||||
const current = await this.invokeOptionalValue<SessionCompactionState>(
|
||||
"readSessionCompactionState",
|
||||
target,
|
||||
);
|
||||
if (isIncomingCompactionStateStale(state, current)) {
|
||||
return { updated: false };
|
||||
}
|
||||
await this.invoke<void>("persistSessionCompactionState", target, state);
|
||||
return { updated: true };
|
||||
}
|
||||
|
||||
async readSessionCompactionState(
|
||||
sessionId: string,
|
||||
): Promise<SessionCompactionState | undefined> {
|
||||
const target = sessionId.trim();
|
||||
if (!target) return undefined;
|
||||
const activeSession = this.sessions.get(target);
|
||||
if (activeSession) {
|
||||
for (;;) {
|
||||
const pendingWrite = activeSession.compactionStateWriteQueue;
|
||||
if (!pendingWrite) {
|
||||
return activeSession.compactionState;
|
||||
}
|
||||
await pendingWrite.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
return await this.invokeOptionalValue<SessionCompactionState>(
|
||||
"readSessionCompactionState",
|
||||
target,
|
||||
);
|
||||
}
|
||||
|
||||
private isCompactionStateForSession(
|
||||
sessionId: string,
|
||||
state: SessionCompactionState,
|
||||
activeSession?: ActiveSession,
|
||||
sessionRecord?: SessionRecord,
|
||||
): boolean {
|
||||
const conversationId = state.conversation_id?.trim();
|
||||
if (!conversationId) {
|
||||
return true;
|
||||
}
|
||||
if (conversationId === sessionId) {
|
||||
return true;
|
||||
}
|
||||
const expectedConversationId =
|
||||
activeSession?.agent.getConversationId()?.trim() ||
|
||||
sessionRecord?.conversationId?.trim();
|
||||
return expectedConversationId
|
||||
? conversationId === expectedConversationId
|
||||
: false;
|
||||
}
|
||||
|
||||
private async canPersistCompactionState(
|
||||
sessionId: string,
|
||||
state: SessionCompactionState,
|
||||
activeSession?: ActiveSession,
|
||||
sessionRecord?: SessionRecord,
|
||||
): Promise<boolean> {
|
||||
if (!state.conversation_id?.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!this.isCompactionStateForSession(
|
||||
sessionId,
|
||||
state,
|
||||
activeSession,
|
||||
sessionRecord,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const sourceMessages =
|
||||
activeSession?.agent.getMessages() ??
|
||||
(await this.readSessionMessages(sessionId));
|
||||
return projectSessionCompactionState(state, sourceMessages) !== undefined;
|
||||
}
|
||||
|
||||
private async persistActiveSessionCompactionState(
|
||||
session: ActiveSession,
|
||||
state: SessionCompactionState,
|
||||
): Promise<{ updated: boolean }> {
|
||||
if (
|
||||
!(await this.canPersistCompactionState(session.sessionId, state, session))
|
||||
) {
|
||||
return { updated: false };
|
||||
}
|
||||
return await this.enqueueCompactionStateWrite(session, async () => {
|
||||
if (isIncomingCompactionStateStale(state, session.compactionState)) {
|
||||
return { updated: false };
|
||||
}
|
||||
await this.invoke<void>(
|
||||
"persistSessionCompactionState",
|
||||
session.sessionId,
|
||||
state,
|
||||
);
|
||||
session.compactionState = state;
|
||||
return { updated: true };
|
||||
});
|
||||
}
|
||||
|
||||
private async enqueueCompactionStateWrite<T>(
|
||||
session: ActiveSession,
|
||||
action: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const previous = session.compactionStateWriteQueue ?? Promise.resolve();
|
||||
const run = previous.catch(() => undefined).then(action);
|
||||
const tracked = run.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
session.compactionStateWriteQueue = tracked;
|
||||
try {
|
||||
return await run;
|
||||
} finally {
|
||||
if (session.compactionStateWriteQueue === tracked) {
|
||||
session.compactionStateWriteQueue = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async readSessionMessages(
|
||||
sessionId: string,
|
||||
): Promise<LlmsProviders.Message[]> {
|
||||
|
||||
@@ -13,7 +13,11 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("readPersistedMessagesFile", () => {
|
||||
it("strips wrapped user_input envelopes from user history messages", async () => {
|
||||
it("returns persisted messages verbatim, wrappers included", async () => {
|
||||
// The user_input wrapper records which mode each message was sent in
|
||||
// and session restarts re-seed through this read path, so stripping
|
||||
// here would destroy that history a little more on every restart.
|
||||
// Display surfaces format for themselves via formatDisplayUserInput.
|
||||
const dir = await mkdtemp(join(tmpdir(), "runtime-host-support-"));
|
||||
tempDirs.push(dir);
|
||||
const messagesPath = join(dir, "messages.json");
|
||||
@@ -33,7 +37,7 @@ describe("readPersistedMessagesFile", () => {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: '<user_input mode="plan">inspect repo</user_input>',
|
||||
text: '<user_input mode="plan"><mode_notice>The user switched from act mode to plan mode before sending this message.</mode_notice>\ninspect repo</user_input>',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -43,12 +47,14 @@ describe("readPersistedMessagesFile", () => {
|
||||
|
||||
const messages = await readPersistedMessagesFile(messagesPath);
|
||||
|
||||
expect(messages[0]?.content).toBe("spawn a team of agents");
|
||||
expect(messages[0]?.content).toBe(
|
||||
'<user_input mode="act">spawn a team of agents</user_input>',
|
||||
);
|
||||
expect(messages[1]?.content).toBe("Working on it.");
|
||||
expect(messages[2]?.content).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: "inspect repo",
|
||||
text: '<user_input mode="plan"><mode_notice>The user switched from act mode to plan mode before sending this message.</mode_notice>\ninspect repo</user_input>',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import type * as LlmsProviders from "@cline/llms";
|
||||
import { formatDisplayUserInput } from "@cline/shared";
|
||||
import type { HookEventPayload } from "../../hooks";
|
||||
import type { CoreSessionEvent } from "../../types/events";
|
||||
import type {
|
||||
@@ -44,6 +43,13 @@ export class RuntimeHostEventBus {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the persisted messages verbatim. User messages keep their
|
||||
// runtime-generated <user_input mode="..."> wrappers and <mode_notice>
|
||||
// elements: they are the durable record of which mode each message was sent
|
||||
// in, and session restarts re-seed new sessions through this read path, so
|
||||
// stripping here would launder that history off disk (and out of the model's
|
||||
// context) a little more on every restart. Display surfaces are responsible
|
||||
// for their own formatting via formatDisplayUserInput.
|
||||
export async function readPersistedMessagesFile(
|
||||
messagesPath?: string | null,
|
||||
): Promise<LlmsProviders.Message[]> {
|
||||
@@ -54,12 +60,12 @@ export async function readPersistedMessagesFile(
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (Array.isArray(parsed)) {
|
||||
return sanitizeDisplayMessages(parsed as LlmsProviders.Message[]);
|
||||
return parsed as LlmsProviders.Message[];
|
||||
}
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
const messages = (parsed as { messages?: unknown }).messages;
|
||||
if (Array.isArray(messages)) {
|
||||
return sanitizeDisplayMessages(messages as LlmsProviders.Message[]);
|
||||
return messages as LlmsProviders.Message[];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
@@ -68,38 +74,6 @@ export async function readPersistedMessagesFile(
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeDisplayMessage(
|
||||
message: LlmsProviders.Message,
|
||||
): LlmsProviders.Message {
|
||||
if (message.role !== "user") {
|
||||
return message;
|
||||
}
|
||||
if (typeof message.content === "string") {
|
||||
return {
|
||||
...message,
|
||||
content: formatDisplayUserInput(message.content),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content: message.content.map((part) => {
|
||||
if (part.type !== "text" || typeof part.text !== "string") {
|
||||
return part;
|
||||
}
|
||||
return {
|
||||
...part,
|
||||
text: formatDisplayUserInput(part.text),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeDisplayMessages(
|
||||
messages: LlmsProviders.Message[],
|
||||
): LlmsProviders.Message[] {
|
||||
return messages.map(sanitizeDisplayMessage);
|
||||
}
|
||||
|
||||
export function cloneAccumulatedUsage(
|
||||
usage: SessionAccumulatedUsage | undefined,
|
||||
): SessionAccumulatedUsage | undefined {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import type { HookEventPayload } from "../../hooks";
|
||||
import type { CheckpointEntry } from "../../hooks/checkpoint-hooks";
|
||||
import type { ProviderSettings } from "../../services/llms/provider-settings";
|
||||
import type { SessionCompactionState } from "../../session/models/session-compaction";
|
||||
import type { SessionManifest } from "../../session/models/session-manifest";
|
||||
import type { SessionSource } from "../../types/common";
|
||||
import type { CoreSessionConfig } from "../../types/config";
|
||||
@@ -104,6 +105,7 @@ export interface StartSessionInput {
|
||||
interactive?: boolean;
|
||||
sessionMetadata?: Record<string, unknown>;
|
||||
initialMessages?: LlmsProviders.Message[];
|
||||
initialCompactionState?: SessionCompactionState;
|
||||
userImages?: string[];
|
||||
userFiles?: string[];
|
||||
/**
|
||||
@@ -308,6 +310,13 @@ export interface RuntimeHost {
|
||||
title?: string | null;
|
||||
},
|
||||
): Promise<{ updated: boolean }>;
|
||||
updateSessionCompactionState(
|
||||
sessionId: string,
|
||||
state: SessionCompactionState,
|
||||
): Promise<{ updated: boolean }>;
|
||||
readSessionCompactionState(
|
||||
sessionId: string,
|
||||
): Promise<SessionCompactionState | undefined>;
|
||||
readSessionMessages(sessionId: string): Promise<LlmsProviders.Message[]>;
|
||||
dispatchHookEvent(payload: HookEventPayload): Promise<void>;
|
||||
subscribe(
|
||||
|
||||
@@ -564,7 +564,7 @@ describe("SessionRuntime message preparation", () => {
|
||||
]);
|
||||
const textParts = result?.messages?.flatMap((message) =>
|
||||
message.content.flatMap((part) =>
|
||||
part.type === "text" ? [part.text] : [],
|
||||
typeof part !== "string" && part.type === "text" ? [part.text] : [],
|
||||
),
|
||||
);
|
||||
expect(textParts).toEqual(["original", "builder-added"]);
|
||||
@@ -1426,7 +1426,10 @@ describe("SessionRuntime real AgentRuntime smoke", () => {
|
||||
expect(
|
||||
modelRequests[1]?.some((message) =>
|
||||
message.content.some(
|
||||
(part) => part.type === "text" && part.text === EMPTY_CONTENT_TEXT,
|
||||
(part) =>
|
||||
typeof part !== "string" &&
|
||||
part.type === "text" &&
|
||||
part.text === EMPTY_CONTENT_TEXT,
|
||||
),
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
@@ -79,6 +79,13 @@ export class SessionArtifacts {
|
||||
);
|
||||
}
|
||||
|
||||
public sessionCompactionPath(sessionId: string): string {
|
||||
return join(
|
||||
this.sessionArtifactsDir(sessionId),
|
||||
`${sessionId}.compaction.json`,
|
||||
);
|
||||
}
|
||||
|
||||
public sessionManifestPath(sessionId: string, ensureDir = false): string {
|
||||
const base = ensureDir
|
||||
? this.ensureSessionArtifactsDir(sessionId)
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createSessionCompactionState,
|
||||
parseSessionCompactionState,
|
||||
projectSessionCompactionState,
|
||||
} from "./session-compaction";
|
||||
|
||||
describe("session compaction state", () => {
|
||||
it("rejects fallback boundary keys when a message role contains the delimiter", () => {
|
||||
expect(() =>
|
||||
createSessionCompactionState({
|
||||
sourceMessages: [
|
||||
{
|
||||
role: "user:custom",
|
||||
content: "invalid role",
|
||||
} as never,
|
||||
],
|
||||
compactedMessages: [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
],
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
}),
|
||||
).toThrow("Message role cannot contain ':'");
|
||||
});
|
||||
|
||||
it("rejects projection when the canonical prefix was edited before the boundary", () => {
|
||||
const sourceMessages = [
|
||||
{ id: "u1", role: "user" as const, content: "original detail" },
|
||||
{ id: "a1", role: "assistant" as const, content: "answer" },
|
||||
];
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages: [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
],
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const editedPrefix = [
|
||||
{ ...sourceMessages[0], content: "redacted detail" },
|
||||
sourceMessages[1],
|
||||
{ id: "u2", role: "user" as const, content: "tail" },
|
||||
];
|
||||
|
||||
expect(projectSessionCompactionState(state, editedPrefix)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("projects compacted state when the canonical prefix matches exactly", () => {
|
||||
const sourceMessages = [
|
||||
{ id: "u1", role: "user" as const, content: "original detail" },
|
||||
{ id: "a1", role: "assistant" as const, content: "answer" },
|
||||
];
|
||||
const compactedMessages = [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
];
|
||||
const tail = { id: "u2", role: "user" as const, content: "tail" };
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages,
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(
|
||||
projectSessionCompactionState(state, [...sourceMessages, tail]),
|
||||
).toEqual([...compactedMessages, tail]);
|
||||
});
|
||||
|
||||
it("projects after persisted source messages are reloaded from JSON", () => {
|
||||
const sourceMessages = [
|
||||
{
|
||||
id: "a1",
|
||||
role: "assistant" as const,
|
||||
content: "answer",
|
||||
metadata: { b: 2, a: 1 },
|
||||
metrics: { inputTokens: 10, outputTokens: 5 },
|
||||
},
|
||||
];
|
||||
const compactedMessages = [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
];
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages,
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const reloadedMessages = JSON.parse(JSON.stringify(sourceMessages));
|
||||
|
||||
expect(projectSessionCompactionState(state, reloadedMessages)).toEqual(
|
||||
compactedMessages,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects projection when canonical message metadata changed", () => {
|
||||
const sourceMessages = [
|
||||
{
|
||||
id: "a1",
|
||||
role: "assistant" as const,
|
||||
content: "answer",
|
||||
metadata: { stable: true },
|
||||
},
|
||||
];
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages: [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
],
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(
|
||||
projectSessionCompactionState(state, [
|
||||
{ ...sourceMessages[0], metadata: { stable: false } },
|
||||
]),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("projects when resumed user input was display-normalized from persisted history", () => {
|
||||
const sourceMessages = [
|
||||
{
|
||||
id: "u1",
|
||||
role: "user" as const,
|
||||
content: '<user_input mode="act">hello</user_input>',
|
||||
},
|
||||
{
|
||||
id: "u2",
|
||||
role: "user" as const,
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: '<user_input mode="act">inspect</user_input>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: "a1", role: "assistant" as const, content: "answer" },
|
||||
];
|
||||
const resumedMessages = [
|
||||
{ ...sourceMessages[0], content: "hello" },
|
||||
{
|
||||
...sourceMessages[1],
|
||||
content: [{ type: "text" as const, text: "inspect" }],
|
||||
},
|
||||
sourceMessages[2],
|
||||
{ id: "u3", role: "user" as const, content: "tail" },
|
||||
];
|
||||
const compactedMessages = [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
];
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages,
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(projectSessionCompactionState(state, resumedMessages)).toEqual([
|
||||
...compactedMessages,
|
||||
resumedMessages[3],
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects anchor-free sidecars even when the source count is zero", () => {
|
||||
const state = parseSessionCompactionState({
|
||||
version: 1,
|
||||
updated_at: "2026-01-01T00:00:00.000Z",
|
||||
source_message_count: 0,
|
||||
messages: [
|
||||
{ id: "summary", role: "user" as const, content: "unanchored" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(state).toBeDefined();
|
||||
if (!state) {
|
||||
throw new Error("expected parsed compaction state");
|
||||
}
|
||||
expect(
|
||||
projectSessionCompactionState(state, [
|
||||
{ id: "u1", role: "user", content: "canonical" },
|
||||
]),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("projects legacy sidecars when the boundary key matches", () => {
|
||||
const sourceMessages = [
|
||||
{ id: "u1", role: "user" as const, content: "original detail" },
|
||||
{ id: "a1", role: "assistant" as const, content: "answer" },
|
||||
];
|
||||
const tail = { id: "u2", role: "user" as const, content: "tail" };
|
||||
const state = parseSessionCompactionState({
|
||||
version: 1,
|
||||
updated_at: "2026-01-01T00:00:00.000Z",
|
||||
source_message_count: sourceMessages.length,
|
||||
source_last_message_key: "id:a1",
|
||||
messages: [{ id: "summary", role: "user" as const, content: "summary" }],
|
||||
});
|
||||
|
||||
expect(state).toBeDefined();
|
||||
if (!state) {
|
||||
throw new Error("expected parsed compaction state");
|
||||
}
|
||||
expect(
|
||||
projectSessionCompactionState(state, [...sourceMessages, tail]),
|
||||
).toEqual([{ id: "summary", role: "user", content: "summary" }, tail]);
|
||||
});
|
||||
|
||||
it("rejects malformed sidecar timestamps", () => {
|
||||
const state = parseSessionCompactionState({
|
||||
version: 1,
|
||||
updated_at: "not-a-date",
|
||||
source_message_count: 1,
|
||||
source_prefix_hash: "sha256:test",
|
||||
messages: [{ id: "summary", role: "user" as const, content: "summary" }],
|
||||
});
|
||||
|
||||
expect(state).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
formatDisplayUserInput,
|
||||
type MessageWithMetadata,
|
||||
} from "@cline/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
function isMessageWithMetadata(value: unknown): value is MessageWithMetadata {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const candidate = value as Partial<MessageWithMetadata>;
|
||||
if (candidate.role !== "user" && candidate.role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typeof candidate.content === "string" || Array.isArray(candidate.content)
|
||||
);
|
||||
}
|
||||
|
||||
const MessageWithMetadataSchema = z.custom<MessageWithMetadata>(
|
||||
isMessageWithMetadata,
|
||||
);
|
||||
|
||||
export const SessionCompactionStateSchema = z.object({
|
||||
version: z.literal(1),
|
||||
updated_at: z.string().datetime(),
|
||||
conversation_id: z.string().min(1).optional(),
|
||||
source_message_count: z.number().int().nonnegative(),
|
||||
source_prefix_hash: z.string().min(1).optional(),
|
||||
source_last_message_key: z.string().min(1).optional(),
|
||||
messages: z.array(MessageWithMetadataSchema),
|
||||
system_prompt: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SessionCompactionState = z.infer<
|
||||
typeof SessionCompactionStateSchema
|
||||
>;
|
||||
|
||||
function cloneMessages(
|
||||
messages: readonly MessageWithMetadata[],
|
||||
): MessageWithMetadata[] {
|
||||
return JSON.parse(JSON.stringify(messages)) as MessageWithMetadata[];
|
||||
}
|
||||
|
||||
function normalizeMessageForSourceHash(
|
||||
message: MessageWithMetadata,
|
||||
): MessageWithMetadata {
|
||||
if (message.role !== "user") {
|
||||
return message;
|
||||
}
|
||||
if (typeof message.content === "string") {
|
||||
return {
|
||||
...message,
|
||||
content: formatDisplayUserInput(message.content),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content: message.content.map((part) =>
|
||||
part.type === "text"
|
||||
? { ...part, text: formatDisplayUserInput(part.text) }
|
||||
: part,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function assertBoundaryRole(role: MessageWithMetadata["role"]): void {
|
||||
if (role.includes(":")) {
|
||||
throw new TypeError(
|
||||
"Message role cannot contain ':' in compaction boundary keys",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the persisted message shape in a fixed top-level field order. Nested
|
||||
// objects keep their persisted JSON order because transcript writes are append-only.
|
||||
function sourceMessageHashInput(message: MessageWithMetadata): unknown[] {
|
||||
const normalized = normalizeMessageForSourceHash(message);
|
||||
assertBoundaryRole(normalized.role);
|
||||
return [
|
||||
["role", normalized.role],
|
||||
["content", normalized.content],
|
||||
["id", normalized.id ?? null],
|
||||
["agent", normalized.agent ?? null],
|
||||
["sessionId", normalized.sessionId ?? null],
|
||||
["metadata", normalized.metadata ?? null],
|
||||
["modelInfo", normalized.modelInfo ?? null],
|
||||
["metrics", normalized.metrics ?? null],
|
||||
["ts", normalized.ts ?? null],
|
||||
];
|
||||
}
|
||||
|
||||
function messageBoundaryKey(message: MessageWithMetadata | undefined): string {
|
||||
if (!message) {
|
||||
return "";
|
||||
}
|
||||
const normalized = normalizeMessageForSourceHash(message);
|
||||
if (typeof normalized.id === "string" && normalized.id.trim()) {
|
||||
return `id:${normalized.id.trim()}`;
|
||||
}
|
||||
assertBoundaryRole(normalized.role);
|
||||
if (typeof normalized.ts === "number" && Number.isFinite(normalized.ts)) {
|
||||
return `ts:${normalized.role}:${normalized.ts}`;
|
||||
}
|
||||
return `content:${normalized.role}:${JSON.stringify(normalized.content)}`;
|
||||
}
|
||||
|
||||
// These anchors are persisted in session sidecars. Changing the format is safe
|
||||
// for saved transcripts, but invalidates existing compaction sidecars.
|
||||
function sourcePrefixHash(
|
||||
messages: readonly MessageWithMetadata[],
|
||||
count = messages.length,
|
||||
): string {
|
||||
const hash = createHash("sha256");
|
||||
hash.update("cline-session-compaction-source-v1\n");
|
||||
hash.update(`${count}\n`);
|
||||
for (const message of messages.slice(0, count)) {
|
||||
hash.update(JSON.stringify(sourceMessageHashInput(message)));
|
||||
hash.update("\n");
|
||||
}
|
||||
return `sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
export function createSessionCompactionState(input: {
|
||||
sourceMessages: readonly MessageWithMetadata[];
|
||||
compactedMessages: readonly MessageWithMetadata[];
|
||||
conversationId?: string;
|
||||
systemPrompt?: string;
|
||||
updatedAt?: string;
|
||||
}): SessionCompactionState {
|
||||
const lastSourceMessage = input.sourceMessages.at(-1);
|
||||
const sourceLastMessageKey = messageBoundaryKey(lastSourceMessage);
|
||||
return SessionCompactionStateSchema.parse({
|
||||
version: 1,
|
||||
updated_at: input.updatedAt ?? new Date().toISOString(),
|
||||
...(input.conversationId?.trim()
|
||||
? { conversation_id: input.conversationId.trim() }
|
||||
: {}),
|
||||
source_message_count: input.sourceMessages.length,
|
||||
source_prefix_hash: sourcePrefixHash(input.sourceMessages),
|
||||
...(sourceLastMessageKey
|
||||
? { source_last_message_key: sourceLastMessageKey }
|
||||
: {}),
|
||||
messages: cloneMessages(input.compactedMessages),
|
||||
...(input.systemPrompt !== undefined
|
||||
? { system_prompt: input.systemPrompt }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function projectSessionCompactionState(
|
||||
state: SessionCompactionState,
|
||||
sourceMessages: readonly MessageWithMetadata[],
|
||||
): MessageWithMetadata[] | undefined {
|
||||
const hasEnoughSourceMessages =
|
||||
state.source_message_count <= sourceMessages.length;
|
||||
if (!hasEnoughSourceMessages) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hasMatchingSourcePrefix =
|
||||
!!state.source_prefix_hash &&
|
||||
sourcePrefixHash(sourceMessages, state.source_message_count) ===
|
||||
state.source_prefix_hash;
|
||||
const boundary = sourceMessages[state.source_message_count - 1];
|
||||
const hasMatchingLegacyBoundary =
|
||||
!state.source_prefix_hash &&
|
||||
state.source_message_count > 0 &&
|
||||
!!state.source_last_message_key &&
|
||||
messageBoundaryKey(boundary) === state.source_last_message_key;
|
||||
const canProjectState = hasMatchingSourcePrefix || hasMatchingLegacyBoundary;
|
||||
if (!canProjectState) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
...cloneMessages(state.messages),
|
||||
...cloneMessages(sourceMessages.slice(state.source_message_count)),
|
||||
];
|
||||
}
|
||||
|
||||
export function parseSessionCompactionState(
|
||||
value: unknown,
|
||||
): SessionCompactionState | undefined {
|
||||
const parsed = SessionCompactionStateSchema.safeParse(value);
|
||||
return parsed.success ? parsed.data : undefined;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export const SessionManifestSchema = z.object({
|
||||
prompt: z.string().optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
messages_path: z.string().min(1).optional(),
|
||||
compaction_path: z.string().min(1).optional(),
|
||||
});
|
||||
|
||||
export type SessionManifest = z.infer<typeof SessionManifestSchema>;
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface CreateRootSessionWithArtifactsInput {
|
||||
export interface RootSessionArtifacts {
|
||||
manifestPath: string;
|
||||
messagesPath: string;
|
||||
compactionPath?: string;
|
||||
manifest: SessionManifest;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { SqliteSessionStore } from "../../services/storage/sqlite-session-store";
|
||||
import { SessionSource } from "../../types/common";
|
||||
import { createSessionCompactionState } from "../models/session-compaction";
|
||||
import { FileSessionService } from "../services/file-session-service";
|
||||
import { CoreSessionService } from "../services/session-service";
|
||||
|
||||
@@ -32,6 +39,160 @@ describe("UnifiedSessionPersistenceService", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("persists compaction state as a separate session artifact", async () => {
|
||||
const sessionsDir = mkdtempSync(join(tmpdir(), "compaction-artifact-"));
|
||||
tempDirs.push(sessionsDir);
|
||||
const service = new FileSessionService(sessionsDir);
|
||||
const sessionId = "session-with-compaction";
|
||||
const artifacts = await service.createRootSessionWithArtifacts({
|
||||
sessionId,
|
||||
source: SessionSource.CLI,
|
||||
pid: process.pid,
|
||||
interactive: true,
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet",
|
||||
cwd: "/tmp/project",
|
||||
workspaceRoot: "/tmp/project",
|
||||
enableTools: true,
|
||||
enableSpawn: true,
|
||||
enableTeams: false,
|
||||
startedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const sourceMessages = [
|
||||
{ id: "u1", role: "user" as const, content: "full transcript" },
|
||||
];
|
||||
const compactedMessages = [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
];
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages,
|
||||
conversationId: "conv-1",
|
||||
updatedAt: "2026-01-01T00:00:01.000Z",
|
||||
});
|
||||
expect(
|
||||
JSON.parse(readFileSync(artifacts.manifestPath, "utf8")),
|
||||
).not.toHaveProperty("compaction_path");
|
||||
expect(existsSync(artifacts.compactionPath ?? "")).toBe(false);
|
||||
|
||||
await service.persistSessionMessages(sessionId, sourceMessages);
|
||||
await service.persistSessionCompactionState(sessionId, state);
|
||||
|
||||
expect(
|
||||
JSON.parse(readFileSync(artifacts.manifestPath, "utf8")),
|
||||
).toHaveProperty("compaction_path", artifacts.compactionPath);
|
||||
const messagesPayload = JSON.parse(
|
||||
readFileSync(artifacts.messagesPath, "utf8"),
|
||||
) as { messages?: unknown[] };
|
||||
const compactionPayload = JSON.parse(
|
||||
readFileSync(artifacts.compactionPath ?? "", "utf8"),
|
||||
) as { messages?: unknown[]; source_message_count?: number };
|
||||
expect(messagesPayload.messages).toHaveLength(1);
|
||||
expect(compactionPayload).toMatchObject({
|
||||
source_message_count: 1,
|
||||
messages: compactedMessages,
|
||||
});
|
||||
await expect(
|
||||
service.readSessionCompactionState(sessionId),
|
||||
).resolves.toMatchObject({
|
||||
source_message_count: 1,
|
||||
messages: compactedMessages,
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes persisted compaction state without mutating canonical messages", async () => {
|
||||
const sessionsDir = mkdtempSync(join(tmpdir(), "compaction-delete-"));
|
||||
tempDirs.push(sessionsDir);
|
||||
const service = new FileSessionService(sessionsDir);
|
||||
const sessionId = "session-delete-compaction";
|
||||
const artifacts = await service.createRootSessionWithArtifacts({
|
||||
sessionId,
|
||||
source: SessionSource.CLI,
|
||||
pid: process.pid,
|
||||
interactive: true,
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet",
|
||||
cwd: "/tmp/project",
|
||||
workspaceRoot: "/tmp/project",
|
||||
enableTools: true,
|
||||
enableSpawn: true,
|
||||
enableTeams: false,
|
||||
startedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const sourceMessages = [
|
||||
{ id: "u1", role: "user" as const, content: "full transcript" },
|
||||
];
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages,
|
||||
compactedMessages: [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
],
|
||||
updatedAt: "2026-01-01T00:00:01.000Z",
|
||||
});
|
||||
|
||||
await service.persistSessionMessages(sessionId, sourceMessages);
|
||||
await service.persistSessionCompactionState(sessionId, state);
|
||||
expect(existsSync(artifacts.compactionPath ?? "")).toBe(true);
|
||||
await service.deleteSessionCompactionState(sessionId);
|
||||
|
||||
expect(existsSync(artifacts.messagesPath)).toBe(true);
|
||||
expect(existsSync(artifacts.compactionPath ?? "")).toBe(false);
|
||||
expect(
|
||||
JSON.parse(readFileSync(artifacts.manifestPath, "utf8")),
|
||||
).not.toHaveProperty("compaction_path");
|
||||
await expect(
|
||||
service.readSessionCompactionState(sessionId),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("adds compaction path to old manifests only when sidecar is written", async () => {
|
||||
const sessionsDir = mkdtempSync(join(tmpdir(), "compaction-old-manifest-"));
|
||||
tempDirs.push(sessionsDir);
|
||||
const service = new FileSessionService(sessionsDir);
|
||||
const sessionId = "session-old-manifest";
|
||||
const artifacts = await service.createRootSessionWithArtifacts({
|
||||
sessionId,
|
||||
source: SessionSource.CLI,
|
||||
pid: process.pid,
|
||||
interactive: true,
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet",
|
||||
cwd: "/tmp/project",
|
||||
workspaceRoot: "/tmp/project",
|
||||
enableTools: true,
|
||||
enableSpawn: true,
|
||||
enableTeams: false,
|
||||
startedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(artifacts.manifestPath, "utf8"),
|
||||
) as {
|
||||
compaction_path?: string;
|
||||
};
|
||||
delete manifest.compaction_path;
|
||||
writeFileSync(
|
||||
artifacts.manifestPath,
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
const state = createSessionCompactionState({
|
||||
sourceMessages: [
|
||||
{ id: "u1", role: "user" as const, content: "full transcript" },
|
||||
],
|
||||
compactedMessages: [
|
||||
{ id: "summary", role: "user" as const, content: "summary" },
|
||||
],
|
||||
updatedAt: "2026-01-01T00:00:01.000Z",
|
||||
});
|
||||
|
||||
await service.persistSessionCompactionState(sessionId, state);
|
||||
|
||||
expect(existsSync(artifacts.compactionPath ?? "")).toBe(true);
|
||||
expect(
|
||||
JSON.parse(readFileSync(artifacts.manifestPath, "utf8")),
|
||||
).toHaveProperty("compaction_path", artifacts.compactionPath);
|
||||
});
|
||||
|
||||
sqliteIt(
|
||||
"reconciles dead running sessions into failed manifests with terminal markers",
|
||||
async () => {
|
||||
@@ -550,4 +711,55 @@ describe("UnifiedSessionPersistenceService", () => {
|
||||
expect(existsSync(join(sessionsDir, sessionId))).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
sqliteIt(
|
||||
"deletes a session when compaction sidecar cleanup fails",
|
||||
async () => {
|
||||
const dbDir = mkdtempSync(join(tmpdir(), "delete-sidecar-fail-db-"));
|
||||
const sessionsDir = mkdtempSync(
|
||||
join(tmpdir(), "delete-sidecar-fail-sessions-"),
|
||||
);
|
||||
tempDirs.push(dbDir, sessionsDir);
|
||||
|
||||
const store = new SqliteSessionStore({ sessionsDir: dbDir });
|
||||
stores.push(store);
|
||||
const service = new CoreSessionService(store, {
|
||||
sessionArtifactsDir: sessionsDir,
|
||||
});
|
||||
const sessionId = "sidecar-delete-fail-session";
|
||||
await service.createRootSessionWithArtifacts({
|
||||
sessionId,
|
||||
source: SessionSource.CLI,
|
||||
pid: process.pid,
|
||||
interactive: false,
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4-6",
|
||||
cwd: "/tmp/project",
|
||||
workspaceRoot: "/tmp/project",
|
||||
enableTools: true,
|
||||
enableSpawn: false,
|
||||
enableTeams: false,
|
||||
prompt: "delete me",
|
||||
startedAt: "2026-04-10T19:00:00.000Z",
|
||||
});
|
||||
const manifestStore = (
|
||||
service as unknown as {
|
||||
manifestStore: {
|
||||
deleteSessionCompactionState: (sessionId: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
).manifestStore;
|
||||
const deleteSidecar = vi
|
||||
.spyOn(manifestStore, "deleteSessionCompactionState")
|
||||
.mockRejectedValue(new Error("sidecar busy"));
|
||||
|
||||
const result = await service.deleteSession(sessionId);
|
||||
|
||||
expect(result).toEqual({ deleted: true });
|
||||
await expect(service.listSessions(10)).resolves.not.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ sessionId })]),
|
||||
);
|
||||
expect(deleteSidecar).toHaveBeenCalledWith(sessionId);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
SessionPersistenceAdapter,
|
||||
StoredMessageWithMetadata,
|
||||
} from "../../types/session";
|
||||
import type { SessionCompactionState } from "../models/session-compaction";
|
||||
import type { SessionRow } from "../models/session-row";
|
||||
import { SessionManifestStore } from "../stores/session-manifest-store";
|
||||
import { TeamChildSessionManager } from "../team";
|
||||
@@ -109,6 +110,8 @@ export class UnifiedSessionPersistenceService {
|
||||
providedId.length > 0 ? providedId : `${Date.now()}_${nanoid(5)}`;
|
||||
const messagesPath =
|
||||
this.manifestStore.artifacts.sessionMessagesPath(sessionId);
|
||||
const compactionPath =
|
||||
this.manifestStore.artifacts.sessionCompactionPath(sessionId);
|
||||
const manifestPath =
|
||||
this.manifestStore.artifacts.sessionManifestPath(sessionId);
|
||||
const metadata = resolveMetadataWithTitle({
|
||||
@@ -172,7 +175,7 @@ export class UnifiedSessionPersistenceService {
|
||||
startedAt,
|
||||
);
|
||||
this.manifestStore.writeSessionManifest(manifestPath, manifest);
|
||||
return { manifestPath, messagesPath, manifest };
|
||||
return { manifestPath, messagesPath, compactionPath, manifest };
|
||||
}
|
||||
|
||||
async updateSessionStatus(
|
||||
@@ -320,6 +323,23 @@ export class UnifiedSessionPersistenceService {
|
||||
);
|
||||
}
|
||||
|
||||
async readSessionCompactionState(
|
||||
sessionId: string,
|
||||
): Promise<SessionCompactionState | undefined> {
|
||||
return await this.manifestStore.readSessionCompactionState(sessionId);
|
||||
}
|
||||
|
||||
async persistSessionCompactionState(
|
||||
sessionId: string,
|
||||
state: SessionCompactionState,
|
||||
): Promise<void> {
|
||||
await this.manifestStore.persistSessionCompactionState(sessionId, state);
|
||||
}
|
||||
|
||||
async deleteSessionCompactionState(sessionId: string): Promise<void> {
|
||||
await this.manifestStore.deleteSessionCompactionState(sessionId);
|
||||
}
|
||||
|
||||
applySubagentStatus(
|
||||
subSessionId: string,
|
||||
event: HookEventPayload,
|
||||
@@ -547,6 +567,7 @@ export class UnifiedSessionPersistenceService {
|
||||
children.map(async (child) => {
|
||||
await deleteCheckpointRefs(child.cwd, child.sessionId);
|
||||
unlinkIfExists(child.messagesPath);
|
||||
await this.deleteSessionCompactionStateIfExists(child.sessionId);
|
||||
unlinkIfExists(
|
||||
this.manifestStore.artifacts.sessionManifestPath(
|
||||
child.sessionId,
|
||||
@@ -561,6 +582,7 @@ export class UnifiedSessionPersistenceService {
|
||||
await deleteCheckpointRefs(row.cwd, id);
|
||||
|
||||
unlinkIfExists(row.messagesPath);
|
||||
await this.deleteSessionCompactionStateIfExists(id);
|
||||
unlinkIfExists(this.manifestStore.artifacts.sessionManifestPath(id, false));
|
||||
if (row.isSubagent) {
|
||||
this.manifestStore.artifacts.removeSessionDirIfEmpty(id);
|
||||
@@ -579,4 +601,12 @@ export class UnifiedSessionPersistenceService {
|
||||
}
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
private async deleteSessionCompactionStateIfExists(
|
||||
sessionId: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.manifestStore.deleteSessionCompactionState(sessionId);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdir, open, rename, rm } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
async function fsyncBestEffort(path: string): Promise<void> {
|
||||
let handle: Awaited<ReturnType<typeof open>> | undefined;
|
||||
try {
|
||||
handle = await open(path, "r");
|
||||
await handle.sync();
|
||||
} catch {
|
||||
// Directory fsync is not available on all platforms/filesystems.
|
||||
} finally {
|
||||
if (handle !== undefined) {
|
||||
try {
|
||||
await handle.close();
|
||||
} catch {
|
||||
// Best-effort durability only.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeFileAtomic(
|
||||
path: string,
|
||||
contents: string,
|
||||
): Promise<void> {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
||||
let handle: Awaited<ReturnType<typeof open>> | undefined;
|
||||
try {
|
||||
handle = await open(tempPath, "wx");
|
||||
await handle.writeFile(contents, "utf8");
|
||||
await handle.sync();
|
||||
await handle.close();
|
||||
handle = undefined;
|
||||
await rename(tempPath, path);
|
||||
await fsyncBestEffort(dirname(path));
|
||||
} catch (error) {
|
||||
if (handle !== undefined) {
|
||||
try {
|
||||
await handle.close();
|
||||
} catch {
|
||||
// Preserve the original write error.
|
||||
}
|
||||
}
|
||||
try {
|
||||
await rm(tempPath, { force: true });
|
||||
} catch {
|
||||
// Preserve the original write error.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { readFile, rm } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import type * as LlmsProviders from "@cline/llms";
|
||||
import type { BasicLogger } from "@cline/shared";
|
||||
@@ -20,10 +21,25 @@ import type {
|
||||
SessionPersistenceAdapter,
|
||||
StoredMessageWithMetadata,
|
||||
} from "../../types/session";
|
||||
import {
|
||||
parseSessionCompactionState,
|
||||
type SessionCompactionState,
|
||||
SessionCompactionStateSchema,
|
||||
} from "../models/session-compaction";
|
||||
import {
|
||||
type SessionManifest,
|
||||
SessionManifestSchema,
|
||||
} from "../models/session-manifest";
|
||||
import { writeFileAtomic } from "./atomic-file";
|
||||
|
||||
function isNotFoundError(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"code" in error &&
|
||||
error.code === "ENOENT"
|
||||
);
|
||||
}
|
||||
|
||||
export class SessionManifestStore {
|
||||
readonly artifacts: SessionArtifacts;
|
||||
@@ -135,6 +151,66 @@ export class SessionManifestStore {
|
||||
}
|
||||
}
|
||||
|
||||
private resolveCompactionPath(sessionId: string): string {
|
||||
const { manifest } = this.readManifestFile(sessionId);
|
||||
return (
|
||||
manifest?.compaction_path?.trim() ||
|
||||
this.artifacts.sessionCompactionPath(sessionId)
|
||||
);
|
||||
}
|
||||
|
||||
private updateCompactionPath(sessionId: string, path: string | undefined): void {
|
||||
const manifestFile = this.readManifestFile(sessionId);
|
||||
if (!manifestFile.manifest) {
|
||||
return;
|
||||
}
|
||||
if (manifestFile.manifest.compaction_path === path) {
|
||||
return;
|
||||
}
|
||||
this.writeSessionManifest(manifestFile.path, {
|
||||
...manifestFile.manifest,
|
||||
compaction_path: path,
|
||||
});
|
||||
}
|
||||
|
||||
async readSessionCompactionState(
|
||||
sessionId: string,
|
||||
): Promise<SessionCompactionState | undefined> {
|
||||
const path = this.resolveCompactionPath(sessionId);
|
||||
try {
|
||||
return parseSessionCompactionState(
|
||||
JSON.parse(await readFile(path, "utf8")) as unknown,
|
||||
);
|
||||
} catch (error) {
|
||||
if (isNotFoundError(error)) {
|
||||
return undefined;
|
||||
}
|
||||
this.logger?.debug("Ignoring invalid session compaction state", {
|
||||
sessionId,
|
||||
path,
|
||||
error,
|
||||
recovery:
|
||||
"Canonical history is unchanged; deleting the sidecar is safe.",
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async persistSessionCompactionState(
|
||||
sessionId: string,
|
||||
state: SessionCompactionState,
|
||||
): Promise<void> {
|
||||
const path = this.resolveCompactionPath(sessionId);
|
||||
const payload = SessionCompactionStateSchema.parse(state);
|
||||
await writeFileAtomic(path, `${JSON.stringify(payload, null, 2)}\n`);
|
||||
this.updateCompactionPath(sessionId, path);
|
||||
}
|
||||
|
||||
async deleteSessionCompactionState(sessionId: string): Promise<void> {
|
||||
await rm(this.resolveCompactionPath(sessionId), { force: true });
|
||||
this.updateCompactionPath(sessionId, undefined);
|
||||
}
|
||||
|
||||
appendStaleSessionHookLog(
|
||||
detectedAt: string,
|
||||
sessionId: string,
|
||||
|
||||
@@ -79,6 +79,13 @@ export interface CoreCompactionSummarizerConfig {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
/**
|
||||
* Optional pre-resolved model metadata for the summarizer. Supplying either
|
||||
* this or `knownModels` lets agentic compaction budget summary input against
|
||||
* the summarizer model's actual context window instead of falling back to the
|
||||
* active model's window.
|
||||
*/
|
||||
modelInfo?: ModelInfo;
|
||||
knownModels?: Record<string, ModelInfo>;
|
||||
providerConfig?: ProviderConfig;
|
||||
maxOutputTokens?: number;
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AgentFinishReason } from "@cline/shared";
|
||||
import type { SessionAccumulatedUsage } from "../runtime/host/runtime-host";
|
||||
import type { BuiltRuntime } from "../runtime/orchestration/session-runtime";
|
||||
import type { SessionRuntime } from "../runtime/orchestration/session-runtime-orchestrator";
|
||||
import type { SessionCompactionState } from "../session/models/session-compaction";
|
||||
import type { SessionRow } from "../session/models/session-row";
|
||||
import type { RootSessionArtifacts } from "../session/services/session-service";
|
||||
import type { SessionSource, SessionStatus } from "./common";
|
||||
@@ -26,6 +27,8 @@ export type ActiveSession = {
|
||||
aborting: boolean;
|
||||
interactive: boolean;
|
||||
persistedMessages?: LlmsProviders.MessageWithMetadata[];
|
||||
compactionState?: SessionCompactionState;
|
||||
compactionStateWriteQueue?: Promise<void>;
|
||||
activeTeamRunIds: Set<string>;
|
||||
pendingTeamRunUpdates: TeamRunUpdate[];
|
||||
teamRunWaiters: Array<() => void>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cline/llms",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -113,6 +113,7 @@ describe("models-dev-catalog", () => {
|
||||
});
|
||||
|
||||
it("uses input limits as the model request context window", () => {
|
||||
expect(resolveMaxInputTokens(undefined)).toBe(128_000);
|
||||
expect(
|
||||
resolveMaxInputTokens({
|
||||
context: 400_000,
|
||||
@@ -274,7 +275,7 @@ describe("models-dev-catalog", () => {
|
||||
id: "claude-defaults",
|
||||
name: "claude-defaults",
|
||||
contextWindow: undefined,
|
||||
maxInputTokens: 4096,
|
||||
maxInputTokens: 128_000,
|
||||
maxTokens: 4096,
|
||||
capabilities: ["tools"],
|
||||
pricing: {
|
||||
@@ -291,7 +292,7 @@ describe("models-dev-catalog", () => {
|
||||
id: "claude-older",
|
||||
name: "claude-older",
|
||||
contextWindow: undefined,
|
||||
maxInputTokens: 4096,
|
||||
maxInputTokens: 128_000,
|
||||
maxTokens: 4096,
|
||||
capabilities: ["tools"],
|
||||
pricing: {
|
||||
|
||||
@@ -37,7 +37,7 @@ interface ModelsDevProviderPayload {
|
||||
export type ModelsDevPayload = Record<string, ModelsDevProviderPayload>;
|
||||
export type ModelsDevProviderKeyMap = Record<string, string>;
|
||||
|
||||
const DEFAULT_MAX_INPUT_TOKENS = 4096;
|
||||
const DEFAULT_MAX_INPUT_TOKENS = 128_000;
|
||||
const DEFAULT_MAX_TOKENS = 4096;
|
||||
|
||||
function parseReleaseDate(value: string | undefined): number {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
version: number;
|
||||
providers: Record<string, Record<string, ModelInfo>>;
|
||||
} = {
|
||||
version: 1783097653299,
|
||||
version: 1783386091437,
|
||||
providers: {
|
||||
aihubmix: {
|
||||
"glm-5.2": {
|
||||
@@ -1229,14 +1229,21 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 128000,
|
||||
capabilities: ["images", "files", "tools", "reasoning", "prompt-cache"],
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 2,
|
||||
output: 10,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 2.5,
|
||||
},
|
||||
releaseDate: "2026-06-30",
|
||||
releaseDate: "2026-06-29",
|
||||
family: "claude-sonnet",
|
||||
},
|
||||
"claude-fable-5": {
|
||||
@@ -1245,14 +1252,21 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 128000,
|
||||
capabilities: ["images", "files", "tools", "reasoning", "prompt-cache"],
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 10,
|
||||
output: 50,
|
||||
cacheRead: 1,
|
||||
cacheWrite: 12.5,
|
||||
},
|
||||
releaseDate: "2026-06-09",
|
||||
releaseDate: "2026-06-07",
|
||||
family: "claude-fable",
|
||||
},
|
||||
"claude-opus-4-8": {
|
||||
@@ -1261,7 +1275,14 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 128000,
|
||||
capabilities: ["images", "files", "tools", "reasoning", "prompt-cache"],
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 5,
|
||||
output: 25,
|
||||
@@ -1277,14 +1298,21 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 128000,
|
||||
capabilities: ["images", "files", "tools", "reasoning", "prompt-cache"],
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 5,
|
||||
output: 25,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 6.25,
|
||||
},
|
||||
releaseDate: "2026-04-16",
|
||||
releaseDate: "2026-04-14",
|
||||
family: "claude-opus",
|
||||
},
|
||||
"claude-sonnet-4-6": {
|
||||
@@ -1292,12 +1320,13 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
name: "Claude Sonnet 4.6",
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 64000,
|
||||
maxTokens: 128000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -1321,6 +1350,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -1330,7 +1360,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 6.25,
|
||||
},
|
||||
releaseDate: "2026-02-05",
|
||||
releaseDate: "2026-02-04",
|
||||
family: "claude-opus",
|
||||
},
|
||||
"claude-opus-4-5": {
|
||||
@@ -1344,6 +1374,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -1367,6 +1398,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -1376,7 +1408,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 6.25,
|
||||
},
|
||||
releaseDate: "2025-11-01",
|
||||
releaseDate: "2025-11-24",
|
||||
family: "claude-opus",
|
||||
},
|
||||
"claude-haiku-4-5": {
|
||||
@@ -1390,6 +1422,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -1413,6 +1446,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -1428,14 +1462,15 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"claude-sonnet-4-5": {
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5 (latest)",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 64000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -1451,14 +1486,15 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"claude-sonnet-4-5-20250929": {
|
||||
id: "claude-sonnet-4-5-20250929",
|
||||
name: "Claude Sonnet 4.5",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 64000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -1471,144 +1507,6 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
releaseDate: "2025-09-29",
|
||||
family: "claude-sonnet",
|
||||
},
|
||||
"claude-opus-4-1": {
|
||||
id: "claude-opus-4-1",
|
||||
name: "Claude Opus 4.1 (latest)",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 32000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 15,
|
||||
output: 75,
|
||||
cacheRead: 1.5,
|
||||
cacheWrite: 18.75,
|
||||
},
|
||||
releaseDate: "2025-08-05",
|
||||
family: "claude-opus",
|
||||
},
|
||||
"claude-opus-4-1-20250805": {
|
||||
id: "claude-opus-4-1-20250805",
|
||||
name: "Claude Opus 4.1",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 32000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 15,
|
||||
output: 75,
|
||||
cacheRead: 1.5,
|
||||
cacheWrite: 18.75,
|
||||
},
|
||||
releaseDate: "2025-08-05",
|
||||
family: "claude-opus",
|
||||
},
|
||||
"claude-opus-4-0": {
|
||||
id: "claude-opus-4-0",
|
||||
name: "Claude Opus 4 (latest)",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 32000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 15,
|
||||
output: 75,
|
||||
cacheRead: 1.5,
|
||||
cacheWrite: 18.75,
|
||||
},
|
||||
releaseDate: "2025-05-22",
|
||||
family: "claude-opus",
|
||||
},
|
||||
"claude-opus-4-20250514": {
|
||||
id: "claude-opus-4-20250514",
|
||||
name: "Claude Opus 4",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 32000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 15,
|
||||
output: 75,
|
||||
cacheRead: 1.5,
|
||||
cacheWrite: 18.75,
|
||||
},
|
||||
releaseDate: "2025-05-22",
|
||||
family: "claude-opus",
|
||||
},
|
||||
"claude-sonnet-4-0": {
|
||||
id: "claude-sonnet-4-0",
|
||||
name: "Claude Sonnet 4 (latest)",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 64000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 3,
|
||||
output: 15,
|
||||
cacheRead: 0.3,
|
||||
cacheWrite: 3.75,
|
||||
},
|
||||
releaseDate: "2025-05-22",
|
||||
family: "claude-sonnet",
|
||||
},
|
||||
"claude-sonnet-4-20250514": {
|
||||
id: "claude-sonnet-4-20250514",
|
||||
name: "Claude Sonnet 4",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 64000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 3,
|
||||
output: 15,
|
||||
cacheRead: 0.3,
|
||||
cacheWrite: 3.75,
|
||||
},
|
||||
releaseDate: "2025-05-22",
|
||||
family: "claude-sonnet",
|
||||
},
|
||||
},
|
||||
baseten: {
|
||||
"zai-org/GLM-5.2": {
|
||||
@@ -3100,6 +2998,30 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
releaseDate: "2025-10-15",
|
||||
family: "claude-haiku",
|
||||
},
|
||||
"jp.anthropic.claude-haiku-4-5-20251001-v1:0": {
|
||||
id: "jp.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
name: "Claude Haiku 4.5 (JP)",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 64000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 1,
|
||||
output: 5,
|
||||
cacheRead: 0.1,
|
||||
cacheWrite: 1.25,
|
||||
},
|
||||
releaseDate: "2025-10-15",
|
||||
family: "claude-haiku",
|
||||
},
|
||||
"us.anthropic.claude-haiku-4-5-20251001-v1:0": {
|
||||
id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
name: "Claude Haiku 4.5 (US)",
|
||||
@@ -4070,7 +3992,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
id: "cline-pass/glm-5.2",
|
||||
contextWindow: 1048576,
|
||||
maxInputTokens: 1048576,
|
||||
maxTokens: 32768,
|
||||
maxTokens: 131072,
|
||||
capabilities: [
|
||||
"tools",
|
||||
"reasoning",
|
||||
@@ -4079,9 +4001,9 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.93,
|
||||
output: 3,
|
||||
cacheRead: 0.18,
|
||||
input: 0.9086,
|
||||
output: 2.8556,
|
||||
cacheRead: 0.16874,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-06-13",
|
||||
@@ -4100,11 +4022,12 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.105,
|
||||
output: 0.28,
|
||||
cacheRead: 0,
|
||||
cacheRead: 0.028,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-04-22",
|
||||
@@ -5696,6 +5619,27 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
releaseDate: "2025-08-05",
|
||||
family: "gpt-oss",
|
||||
},
|
||||
"openai/gpt-oss-20b": {
|
||||
id: "openai/gpt-oss-20b",
|
||||
name: "GPT OSS 20B",
|
||||
contextWindow: 131072,
|
||||
maxInputTokens: 131072,
|
||||
maxTokens: 32768,
|
||||
capabilities: [
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.1,
|
||||
output: 0.5,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2025-08-05",
|
||||
family: "gpt-oss",
|
||||
},
|
||||
"zai-org/GLM-4.5": {
|
||||
id: "zai-org/GLM-4.5",
|
||||
name: "GLM-4.5",
|
||||
@@ -12190,6 +12134,49 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
},
|
||||
},
|
||||
openrouter: {
|
||||
"tencent/hy3": {
|
||||
id: "tencent/hy3",
|
||||
name: "Hy3",
|
||||
contextWindow: 202752,
|
||||
maxInputTokens: 202752,
|
||||
maxTokens: 131072,
|
||||
capabilities: [
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.14,
|
||||
output: 0.58,
|
||||
cacheRead: 0.035,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-07-06",
|
||||
family: "hy3",
|
||||
},
|
||||
"tencent/hy3:free": {
|
||||
id: "tencent/hy3:free",
|
||||
name: "Hy3 (free)",
|
||||
contextWindow: 262144,
|
||||
maxInputTokens: 262144,
|
||||
maxTokens: 262144,
|
||||
capabilities: [
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
],
|
||||
pricing: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-07-06",
|
||||
family: "hy3",
|
||||
},
|
||||
"poolside/laguna-xs-2.1": {
|
||||
id: "poolside/laguna-xs-2.1",
|
||||
name: "Laguna XS 2.1",
|
||||
@@ -12243,6 +12230,29 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
releaseDate: "2026-06-30",
|
||||
family: "claude-sonnet",
|
||||
},
|
||||
"nex-agi/nex-n2-mini": {
|
||||
id: "nex-agi/nex-n2-mini",
|
||||
name: "Nex-N2-Mini",
|
||||
contextWindow: 262144,
|
||||
maxInputTokens: 262144,
|
||||
maxTokens: 262144,
|
||||
capabilities: [
|
||||
"images",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.025,
|
||||
output: 0.1,
|
||||
cacheRead: 0.0025,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-06-24",
|
||||
family: "agi",
|
||||
},
|
||||
"sakana/fugu-ultra": {
|
||||
id: "sakana/fugu-ultra",
|
||||
name: "Fugu Ultra",
|
||||
@@ -12309,7 +12319,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
name: "GLM-5.2",
|
||||
contextWindow: 1048576,
|
||||
maxInputTokens: 1048576,
|
||||
maxTokens: 32768,
|
||||
maxTokens: 131072,
|
||||
capabilities: [
|
||||
"tools",
|
||||
"reasoning",
|
||||
@@ -12318,9 +12328,9 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.93,
|
||||
output: 3,
|
||||
cacheRead: 0.18,
|
||||
input: 0.9086,
|
||||
output: 2.8556,
|
||||
cacheRead: 0.16874,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-06-13",
|
||||
@@ -13153,11 +13163,12 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.105,
|
||||
output: 0.28,
|
||||
cacheRead: 0,
|
||||
cacheRead: 0.028,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-04-22",
|
||||
@@ -13728,8 +13739,8 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"temperature",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.085,
|
||||
output: 0.4,
|
||||
input: 0.08,
|
||||
output: 0.45,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -14131,11 +14142,12 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.385,
|
||||
output: 2.45,
|
||||
cacheRead: 0,
|
||||
cacheRead: 0.111,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-02-15",
|
||||
@@ -14418,11 +14430,12 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.375,
|
||||
output: 2.025,
|
||||
cacheRead: 0,
|
||||
cacheRead: 0.203,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
releaseDate: "2026-01",
|
||||
@@ -15789,7 +15802,7 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
},
|
||||
"openai/gpt-oss-20b": {
|
||||
id: "openai/gpt-oss-20b",
|
||||
name: "gpt-oss-20b",
|
||||
name: "GPT OSS 20B",
|
||||
contextWindow: 131072,
|
||||
maxInputTokens: 131072,
|
||||
maxTokens: 131072,
|
||||
@@ -21187,14 +21200,15 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"anthropic/claude-sonnet-4.5": {
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 64000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
@@ -21560,29 +21574,6 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
releaseDate: "2025-08-07",
|
||||
family: "gpt-nano",
|
||||
},
|
||||
"anthropic/claude-opus-4.1": {
|
||||
id: "anthropic/claude-opus-4.1",
|
||||
name: "Claude Opus 4.1",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 32000,
|
||||
capabilities: [
|
||||
"images",
|
||||
"files",
|
||||
"tools",
|
||||
"reasoning",
|
||||
"temperature",
|
||||
"prompt-cache",
|
||||
],
|
||||
pricing: {
|
||||
input: 15,
|
||||
output: 75,
|
||||
cacheRead: 1.5,
|
||||
cacheWrite: 18.75,
|
||||
},
|
||||
releaseDate: "2025-08-05",
|
||||
family: "claude-opus",
|
||||
},
|
||||
"openai/gpt-oss-120b": {
|
||||
id: "openai/gpt-oss-120b",
|
||||
name: "GPT OSS 120B",
|
||||
@@ -21610,7 +21601,12 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
contextWindow: 131072,
|
||||
maxInputTokens: 122880,
|
||||
maxTokens: 8192,
|
||||
capabilities: ["tools", "reasoning", "temperature"],
|
||||
capabilities: [
|
||||
"tools",
|
||||
"reasoning",
|
||||
"structured_output",
|
||||
"temperature",
|
||||
],
|
||||
pricing: {
|
||||
input: 0.05,
|
||||
output: 0.2,
|
||||
@@ -21876,8 +21872,8 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
"anthropic/claude-sonnet-4": {
|
||||
id: "anthropic/claude-sonnet-4",
|
||||
name: "Claude Sonnet 4",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
contextWindow: 1000000,
|
||||
maxInputTokens: 1000000,
|
||||
maxTokens: 64000,
|
||||
capabilities: [
|
||||
"images",
|
||||
@@ -22617,6 +22613,22 @@ export const GENERATED_PROVIDER_MODELS: {
|
||||
releaseDate: "2024-05-13",
|
||||
family: "gpt",
|
||||
},
|
||||
"anthropic/claude-3-haiku": {
|
||||
id: "anthropic/claude-3-haiku",
|
||||
name: "Claude Haiku 3",
|
||||
contextWindow: 200000,
|
||||
maxInputTokens: 200000,
|
||||
maxTokens: 4096,
|
||||
capabilities: ["images", "tools", "temperature", "prompt-cache"],
|
||||
pricing: {
|
||||
input: 0.25,
|
||||
output: 1.25,
|
||||
cacheRead: 0.03,
|
||||
cacheWrite: 0.3,
|
||||
},
|
||||
releaseDate: "2024-03-13",
|
||||
family: "claude-haiku",
|
||||
},
|
||||
"openai/gpt-4-turbo": {
|
||||
id: "openai/gpt-4-turbo",
|
||||
name: "GPT-4 Turbo",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/sdk",
|
||||
"description": "Cline SDK - user-facing alias for @cline/core",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cline/shared",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.58",
|
||||
"description": "Shared utilities, types, and schemas for Cline packages",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -438,9 +438,11 @@ export interface AgentRuntimeConfig {
|
||||
request: ToolApprovalRequest,
|
||||
) => Promise<ToolApprovalResult> | ToolApprovalResult;
|
||||
/**
|
||||
* Optional host-owned context pipeline that can rewrite the transcript
|
||||
* before each model request. When it returns messages, the runtime replaces
|
||||
* its in-memory transcript so compaction persists into the final run result.
|
||||
* Optional host-owned request projection hook invoked before each model call.
|
||||
*
|
||||
* Returned messages affect only the provider request for the current call.
|
||||
* They do not replace the canonical runtime transcript, are not persisted as
|
||||
* session history, and are not reflected in AgentRunResult.messages.
|
||||
*/
|
||||
prepareTurn?: (
|
||||
context: AgentRuntimePrepareTurnContext,
|
||||
|
||||
@@ -800,8 +800,14 @@ export interface AgentConfig {
|
||||
*/
|
||||
logger?: BasicLogger;
|
||||
/**
|
||||
* Optional callback that can rewrite the turn input before each model call.
|
||||
* This is the primary seam for host-owned context pipelines.
|
||||
* Optional request projection hook invoked before each model call.
|
||||
*
|
||||
* Returned messages affect only the provider request for the current call.
|
||||
* They do not replace the canonical runtime transcript, are not persisted as
|
||||
* session history, and are not reflected in AgentRunResult.messages.
|
||||
*
|
||||
* Hosts that need durable redaction or normalization must apply it before a
|
||||
* message enters the canonical transcript.
|
||||
*/
|
||||
prepareTurn?: (
|
||||
context: AgentPrepareTurnContext,
|
||||
|
||||
@@ -384,6 +384,8 @@ export type HubCommandName =
|
||||
| "session.restore"
|
||||
| "session.delete"
|
||||
| "session.update"
|
||||
| "session.compaction.get"
|
||||
| "session.compaction.update"
|
||||
| "session.pending_prompts"
|
||||
| "session.update_pending_prompt"
|
||||
| "session.remove_pending_prompt"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user