cursor-opencode-provider 0.6.6 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/language-model.d.ts +4 -0
- package/dist/language-model.js +83 -7
- package/dist/pricing-data.d.ts +38 -0
- package/dist/pricing-data.js +42 -4
- package/dist/protocol/messages.js +14 -1
- package/dist/protocol/tool-call-bridge.d.ts +29 -1
- package/dist/protocol/tool-call-bridge.js +186 -6
- package/dist/session.d.ts +6 -0
- package/dist/session.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -419,7 +419,9 @@ OpenCode
|
|
|
419
419
|
|
|
420
420
|
### Injected system guidance
|
|
421
421
|
|
|
422
|
-
The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
|
|
422
|
+
The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, preferring OpenCode `todowrite` / `todoread` over Cursor TodoWrite without inviting catalog-comparison narration, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
|
|
423
|
+
|
|
424
|
+
Native Cursor todo display updates (`update_todos_tool_call` / `read_todos_tool_call`) are mirrored into `todowrite` / `todoread` when those host tools are advertised. The provider retains the last known full list per OpenCode session — refreshed by bridged writes, direct host `todowrite` calls, and `todoread` results, surviving Run close, checkpoint resume, and later turns — so `merge: true` updates that omit Cursor's completed list still become a host replace-all snapshot (matched by id, then by content across id spaces). This snapshot is in-memory only and never enters the prompt, RequestContext, or persisted restart state, so Cursor's prompt-cache prefix is unaffected.
|
|
423
425
|
|
|
424
426
|
If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`src/language-model.ts`](src/language-model.ts) and its focused coverage in [`test/prompt-history.test.ts`](test/prompt-history.test.ts).
|
|
425
427
|
|
|
@@ -492,7 +494,7 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
492
494
|
- **Cursor's checkpoint controls displayed totals** — live continuation captures show `TurnEnded` counters can decrease between turns and can represent aggregate model work rather than final context occupancy. When a checkpoint contains `ConversationStateStructure.token_details`, the provider makes AI SDK input + output equal Cursor's `used_tokens`; at `TurnEnded`, output/reasoning remain based on `TurnEnded`, while uncached/cache-read/cache-write input are proportionally normalized to the remaining context input. OpenCode TUI/GUI replace each assistant message's tokens (TUI requires `tokens.output > 0`) and add cost per step, so mid-turn tool boundaries emit the last-known occupancy snapshot rather than a delta or a billed zero. Those snapshots set `output=1` so the footer updates and `providerMetadata.copilot.totalNanoAiu: 0` so OpenCode's getUsage reports $0 instead of charging occupancy as a new prompt. A Run without fresh token details retains the previous checkpoint snapshot and marks `providerMetadata.cursor.context` as `source: "checkpoint-previous-turn", stale: true`. If no checkpoint has ever supplied token details, standard usage is zero and context metadata is absent, so `TurnEnded` is never misrepresented as occupancy; its exact values remain under `providerMetadata.cursor.*Raw`. The full known snapshot includes `usedTokens`, `maxTokens`, percentage, category breakdown, source, and staleness. Cursor consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
|
|
493
495
|
- **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` built from OpenCode project context (workspace env, `AGENTS.md` / `instructions`, `.opencode` agents/skills/plugins, git, layout, plus `.claude`/`.agents` skill fallbacks). Expensive workspace state (rules, environment, repository, git, and layout) is frozen per Cursor conversation and persisted for restart recovery. Tools/MCP descriptors, skills, subagents, and plugin metadata are rediscovered and overlaid each Run; unchanged overlays reuse byte-identical encoded context, while enabling/disabling a capability becomes visible on the next turn. Concurrent first Runs share one base build, and compaction/reset clears it. Its canonical root is also used by the [injected system guidance](#injected-system-guidance). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. Cursor-only cloud/sandbox marketplace surfaces are omitted. `env.workspace_paths` / `process_working_directory` stay on the real git workspace; Cursor's metadata root (`project_folder`, MCP `workspace_project_dir`, terminals/transcripts) is advertised under `<host-cache>/projects/<slug>/` (default `~/.cache/opencode/projects/…`) so dumps like `agent-tools/` do not land in the repo. OpenCode remains the permission authority: its coarse allow/ask/deny configuration is not fabricated into Cursor's unrelated allow/block instruction-list messages.
|
|
494
496
|
- **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped into that server's `mcp_descriptors` / `provider_identifier` (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Cursor's MCP-state exec probe is answered from the same advertised descriptors before the actual tool request, using the full canonical tool-definition identity required by native `get_mcp_tools`; exec still reconstructs the full OpenCode tool id.
|
|
495
|
-
- **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call`
|
|
497
|
+
- **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` → `todowrite`, `read_todos_tool_call` → `todoread`, `create_plan_tool_call` → `todowrite`); the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. All 37 Cursor CLI exec request/result pairs are inventoried by field and name. Known-but-unsupported variants are soft-denied on the held Run with a populated typed error/`rejected`/`allowlisted:false` result (or `throw` when the CLI has no typed error shape) so the turn continues; unknown future request fields still fail the Run explicitly rather than receiving a guessed response that could deadlock it.
|
|
496
498
|
- **Progress-only fragments can get one continuation** — if a real tool-enabled turn ends with a short progress fragment ("Checking the workspace", "Looking into it now") and no host tool call, the provider opens one follow-up Run with a synthetic user nudge so the model can call a listed tool or finish the answer. Complete answers that merely start with those verbs are not continued. Title/compaction turns (`allowTools=false`) never continue. If the continuation fails, the original `turn_ended` is still reported as a successful stop.
|
|
497
499
|
- **Tool availability is per OpenCode agent** — Cursor can request native capabilities such as Task even when a child or restricted OpenCode agent did not advertise the corresponding host tool. The provider prompts Cursor with the exact current catalog and checks every decoded host-tool exec request against it. An unavailable request is answered on Cursor's correlated typed result channel and is never emitted as OpenCode's `invalid` tool.
|
|
498
500
|
- **Host web tools use collision-safe aliases** — Cursor sees `custom_websearch` / `custom_webfetch`, and the held Run maps each alias back to an executable OpenCode tool with its schema, permission check, and correlated result intact. The plugin registers its search fallback directly as `custom_websearch`, avoiding OpenCode's reserved `websearch` id filter for third-party providers and taking precedence over MCP search providers such as Brave without host environment configuration. Exact host `webfetch` is translated the same way. Cursor's UI-bound native web interactions stay disabled because their approval replies cannot carry OpenCode tool results.
|
package/dist/language-model.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export declare function resolveRetryPolicy(options: CursorRetryOptions | undefin
|
|
|
19
19
|
export declare function connectFrameError(payload: string): CursorProviderError;
|
|
20
20
|
/** Restore the last real catalog solely for lifecycle turns such as compaction. */
|
|
21
21
|
export declare function restoreTurnToolCatalog(sessionKey: string, tools: OpencodeToolDef[]): void;
|
|
22
|
+
/** Record the authoritative host todo list for an OpenCode session. */
|
|
23
|
+
export declare function rememberMirroredTodos(sessionKey: string | undefined, todos: ReadonlyArray<Record<string, unknown>>): void;
|
|
24
|
+
/** Copy of the last known host todo list for an OpenCode session, if any. */
|
|
25
|
+
export declare function snapshotMirroredTodosBySession(sessionKey: string | undefined): Array<Record<string, unknown>> | undefined;
|
|
22
26
|
type V3Part = LanguageModelV3StreamPart;
|
|
23
27
|
export declare function createCursorLanguageModel(modelId: string, providerId: string, options: CreateCursorOptions): LanguageModelV3;
|
|
24
28
|
export declare function pumpWithRecovery(input: {
|
package/dist/language-model.js
CHANGED
|
@@ -11,7 +11,7 @@ import { buildGitDiffExecMessages } from "./protocol/git-diff.js";
|
|
|
11
11
|
import { cursorExecVariantByRequestField, describeCursorExecVariant } from "./protocol/exec-variants.js";
|
|
12
12
|
import { appendWorkspaceRootGrounding } from "./protocol/workspace-grounding.js";
|
|
13
13
|
import { progressOnlyContinuationPrompt, shouldContinueProgressOnlyTurn, } from "./protocol/progress-continuation.js";
|
|
14
|
-
import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, } from "./protocol/tool-call-bridge.js";
|
|
14
|
+
import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, snapshotMirroredTodos, snapshotMirroredTodosFromReadOutput, } from "./protocol/tool-call-bridge.js";
|
|
15
15
|
import { handleKvServerMessage } from "./protocol/kv.js";
|
|
16
16
|
import { buildAskQuestionInteractionReply, buildAsyncAskQuestionCompletion, buildCreatePlanInteractionReply, buildSwitchModeInteractionReply, handleInteractionQuery, } from "./protocol/interactions.js";
|
|
17
17
|
import { ASK_QUESTION_RESULT_FIELD, askQuestionResultFromToolOutput, askQuestionToolInput, } from "./protocol/ask-question.js";
|
|
@@ -326,6 +326,43 @@ function snapshotToolCatalog(sessionKey) {
|
|
|
326
326
|
return [];
|
|
327
327
|
return structuredClone(toolCatalogBySession.get(sessionKey) ?? []);
|
|
328
328
|
}
|
|
329
|
+
// Last known full host todo list per OpenCode session. Run-local
|
|
330
|
+
// `session.mirroredTodos` dies with the Run (turn_ended closes the session),
|
|
331
|
+
// but merges in later turns still need a base — same shape of problem as the
|
|
332
|
+
// tool catalog above, same solution. In-memory only: never enters the prompt,
|
|
333
|
+
// RequestContext, or persisted restart state, so the cache prefix is untouched.
|
|
334
|
+
// Refreshed on every todo write/read we observe; replaced wholesale, never
|
|
335
|
+
// merged, so it tracks host truth instead of accumulating guesses.
|
|
336
|
+
const mirroredTodosBySession = new Map();
|
|
337
|
+
/** Record the authoritative host todo list for an OpenCode session. */
|
|
338
|
+
export function rememberMirroredTodos(sessionKey, todos) {
|
|
339
|
+
if (!sessionKey)
|
|
340
|
+
return;
|
|
341
|
+
mirroredTodosBySession.delete(sessionKey);
|
|
342
|
+
mirroredTodosBySession.set(sessionKey, todos.map((t) => ({ ...t })));
|
|
343
|
+
while (mirroredTodosBySession.size > MAX_TURN_STATE_SESSIONS) {
|
|
344
|
+
const oldest = mirroredTodosBySession.keys().next().value;
|
|
345
|
+
if (!oldest)
|
|
346
|
+
break;
|
|
347
|
+
mirroredTodosBySession.delete(oldest);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** Copy of the last known host todo list for an OpenCode session, if any. */
|
|
351
|
+
export function snapshotMirroredTodosBySession(sessionKey) {
|
|
352
|
+
if (!sessionKey)
|
|
353
|
+
return undefined;
|
|
354
|
+
const todos = mirroredTodosBySession.get(sessionKey);
|
|
355
|
+
return todos ? todos.map((t) => ({ ...t })) : undefined;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Store a bridged/observed full todo snapshot on both the live Run session
|
|
359
|
+
* (for merges later this turn) and the per-OpenCode-session copy (for merges
|
|
360
|
+
* in later turns and across checkpoint resumes).
|
|
361
|
+
*/
|
|
362
|
+
function storeMirroredTodos(session, todos) {
|
|
363
|
+
session.mirroredTodos = todos.map((t) => ({ ...t }));
|
|
364
|
+
rememberMirroredTodos(session.openCodeSessionId, session.mirroredTodos);
|
|
365
|
+
}
|
|
329
366
|
function rememberPostCompactionRebase(sessionKey) {
|
|
330
367
|
postCompactionRebaseBySession.delete(sessionKey);
|
|
331
368
|
postCompactionRebaseBySession.add(sessionKey);
|
|
@@ -573,6 +610,8 @@ export async function pumpWithRecovery(input) {
|
|
|
573
610
|
if (recovery.kind === "resume") {
|
|
574
611
|
session.usageEstimate = { ...pumpedSession.usageEstimate };
|
|
575
612
|
session.editToolCalls = new Map(pumpedSession.editToolCalls);
|
|
613
|
+
// mirroredTodos rides along via rememberMirroredTodos (per-OpenCode-
|
|
614
|
+
// session, seeded in startSession) — no handoff needed here.
|
|
576
615
|
}
|
|
577
616
|
input.onSession?.(session);
|
|
578
617
|
}
|
|
@@ -977,6 +1016,10 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
977
1016
|
pending: new Map(),
|
|
978
1017
|
displayToolCalls: new Map(),
|
|
979
1018
|
editToolCalls: new Map(),
|
|
1019
|
+
// Seed from the per-OpenCode-session copy: merges in this turn (and after
|
|
1020
|
+
// checkpoint resumes/rebases, which rebuild the session here) expand
|
|
1021
|
+
// against the last observed host list, not an empty one.
|
|
1022
|
+
mirroredTodos: snapshotMirroredTodosBySession(lifecycle ? undefined : sessionKey),
|
|
980
1023
|
nextBridgedExecId: 900_000,
|
|
981
1024
|
blobs: new Map(),
|
|
982
1025
|
toolDescriptors,
|
|
@@ -1362,6 +1405,16 @@ export function deliverContinuationResults(session, trailingToolResults) {
|
|
|
1362
1405
|
continue;
|
|
1363
1406
|
return undefined;
|
|
1364
1407
|
}
|
|
1408
|
+
// A host `todoread` result is the authoritative list. Refresh the mirrored
|
|
1409
|
+
// snapshot (direct reads and bridged native reads alike) so later merges
|
|
1410
|
+
// apply onto host truth, not a stale write.
|
|
1411
|
+
if (pending.toolName === "todoread" && r.error === undefined) {
|
|
1412
|
+
const snapshot = snapshotMirroredTodosFromReadOutput(r.output);
|
|
1413
|
+
if (snapshot !== undefined) {
|
|
1414
|
+
storeMirroredTodos(session, snapshot);
|
|
1415
|
+
trace(`continuation: mirrored todoread snapshot items=${snapshot.length}`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1365
1418
|
session.usageEstimate.inputTokens += estimateTokens(r.output.length);
|
|
1366
1419
|
if (pending.bridged) {
|
|
1367
1420
|
trace(`continuation: completed bridged result execId=${r.execId} toolName=${pending.toolName ?? r.toolName} outLen=${r.output.length}`);
|
|
@@ -2113,7 +2166,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2113
2166
|
trace(`display tool_call_completed: SKIPPED (allowTools=false) callId=${callId}`);
|
|
2114
2167
|
}
|
|
2115
2168
|
else {
|
|
2116
|
-
const display = parseDisplayToolCall(callId, toolCall);
|
|
2169
|
+
const display = parseDisplayToolCall(callId, toolCall, session.mirroredTodos);
|
|
2117
2170
|
const advertised = advertisedToolNamesFromDescriptors(session.toolDescriptors);
|
|
2118
2171
|
const bridged = display
|
|
2119
2172
|
? resolveBridgedOpenCodeToolCall(display, advertised)
|
|
@@ -2134,6 +2187,13 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2134
2187
|
`advertised=[${advertised.join(",")}]`);
|
|
2135
2188
|
}
|
|
2136
2189
|
else {
|
|
2190
|
+
// Snapshot only Cursor todo writes (not create_plan's synthetic
|
|
2191
|
+
// "plan" prepend) so later merge patches apply onto a real list.
|
|
2192
|
+
if (bridged.toolName === "todowrite" && bridged.variant === "update_todos_tool_call") {
|
|
2193
|
+
const snapshot = snapshotMirroredTodos(bridged.args.todos);
|
|
2194
|
+
if (snapshot !== undefined)
|
|
2195
|
+
storeMirroredTodos(session, snapshot);
|
|
2196
|
+
}
|
|
2137
2197
|
const execId = session.nextBridgedExecId++;
|
|
2138
2198
|
sessionManager.registerPending(execId, session, "bridged", bridged.toolName, true);
|
|
2139
2199
|
const toolCallId = `cursor_${session.sessionId}_${execId}`;
|
|
@@ -2396,6 +2456,16 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
2396
2456
|
}
|
|
2397
2457
|
// Keep the stream open; the result arrives on the next doStream call.
|
|
2398
2458
|
sessionManager.registerPending(parsed.id, session, parsed.resultField, parsed.toolName, false, parsed.resultMetadata);
|
|
2459
|
+
// A direct host `todowrite` is a replace-all snapshot: it is the new
|
|
2460
|
+
// truth for later Cursor merge patches, which otherwise apply onto a
|
|
2461
|
+
// stale (or empty) mirrored list.
|
|
2462
|
+
if (parsed.toolName === "todowrite") {
|
|
2463
|
+
const snapshot = snapshotMirroredTodos(parsed.args.todos);
|
|
2464
|
+
if (snapshot !== undefined) {
|
|
2465
|
+
storeMirroredTodos(session, snapshot);
|
|
2466
|
+
trace(`exec: mirrored todowrite snapshot items=${snapshot.length}`);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2399
2469
|
// tc.input is already a JSON string (LanguageModelV3ToolCall.input).
|
|
2400
2470
|
trace(`exec: EMITTED tool-call toolCallId=${tc.toolCallId} toolName=${tc.toolName} inputLen=${tc.input.length}`);
|
|
2401
2471
|
emittedHostTools++;
|
|
@@ -2781,12 +2851,18 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
2781
2851
|
if (names.has("plan_enter")) {
|
|
2782
2852
|
instructions.push("- To enter plan mode, call the OpenCode `plan_enter` tool. Cursor-native SwitchMode requests for plan/spec are also accepted and answered through it.");
|
|
2783
2853
|
}
|
|
2784
|
-
else if (names.has("todowrite")) {
|
|
2785
|
-
instructions.push("- For planning task lists, call the OpenCode `todowrite` tool and/or write the plan as normal markdown.");
|
|
2786
|
-
}
|
|
2787
2854
|
if (names.has("plan_exit")) {
|
|
2788
2855
|
instructions.push("- To leave plan mode, call the OpenCode `plan_exit` tool. Cursor-native SwitchMode for any non-plan target (agent, build, chat, debug, edit, background, multitask, triage, project, …) is also accepted and answered through it; the provider then injects the Cursor CLI-shaped mode reminder for that target.");
|
|
2789
2856
|
}
|
|
2857
|
+
// Prefer host todos whenever advertised. Keep guidance terse — spelling out
|
|
2858
|
+
// Cursor TodoWrite vs TodoRead / MCP alias collisions teaches the model to
|
|
2859
|
+
// inventory catalogs instead of just calling the host tools.
|
|
2860
|
+
if (names.has("todowrite") || names.has("todoread")) {
|
|
2861
|
+
const write = names.has("todowrite") ? "`todowrite`" : undefined;
|
|
2862
|
+
const read = names.has("todoread") ? "`todoread`" : undefined;
|
|
2863
|
+
const tools = write && read ? `${write} / ${read}` : (write ?? read);
|
|
2864
|
+
instructions.push(`- For task-list create/update/complete/cancel${read ? "/read" : ""}, call OpenCode ${tools}; do not use Cursor TodoWrite, and do not narrate Cursor-vs-OpenCode todo-tool differences.`);
|
|
2865
|
+
}
|
|
2790
2866
|
if (names.has(CUSTOM_WEBSEARCH_TOOL)) {
|
|
2791
2867
|
instructions.push(`- For web searches, call \`${CUSTOM_WEBSEARCH_TOOL}\`; do not use Cursor's native WebSearch interaction.`);
|
|
2792
2868
|
}
|
|
@@ -2828,8 +2904,8 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
2828
2904
|
`OpenCode exposes exactly these executable tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`,
|
|
2829
2905
|
`Workspace root: ${JSON.stringify(workspaceRoot)}. Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them.`,
|
|
2830
2906
|
subagents.executor
|
|
2831
|
-
? "Call only tools in that exact list for ordinary host execution. Cursor-native Task/subagent requests are permitted because a compatible host executor is listed. Bridged Cursor interactions named below (AskQuestion, SwitchMode, CreatePlan, …) are not OpenCode/MCP catalog tools — raise them normally and do not narrate that they are missing."
|
|
2832
|
-
: "Call only tools in that exact list for ordinary host execution. Bridged Cursor interactions named below (AskQuestion, SwitchMode, CreatePlan, …) are not OpenCode/MCP catalog tools — raise them normally and do not narrate that they are missing. Other unlisted Cursor-native tools are not bridged; complete the work with the listed tools or explain the limitation without claiming a missing MCP tool.",
|
|
2907
|
+
? "Call only tools in that exact OpenCode list for ordinary host execution. Cursor-native Task/subagent requests are permitted because a compatible host executor is listed. Bridged Cursor interactions named below (AskQuestion, SwitchMode, CreatePlan, …) are not OpenCode/MCP catalog tools — raise them normally and do not narrate that they are missing."
|
|
2908
|
+
: "Call only tools in that exact OpenCode list for ordinary host execution. Bridged Cursor interactions named below (AskQuestion, SwitchMode, CreatePlan, …) are not OpenCode/MCP catalog tools — raise them normally and do not narrate that they are missing. Other unlisted Cursor-native tools are not bridged; complete the work with the listed tools or explain the limitation without claiming a missing MCP tool.",
|
|
2833
2909
|
...(instructions.length > 0
|
|
2834
2910
|
? ["Use these OpenCode tools instead of equivalent Cursor-native UI interactions:"]
|
|
2835
2911
|
: []),
|
package/dist/pricing-data.d.ts
CHANGED
|
@@ -125,6 +125,11 @@ export declare const CURSOR_MODEL_COSTS: {
|
|
|
125
125
|
readonly output: 3.5;
|
|
126
126
|
readonly cache_read: 0.075;
|
|
127
127
|
};
|
|
128
|
+
readonly "gemini-3.8-flash": {
|
|
129
|
+
readonly input: 0.75;
|
|
130
|
+
readonly output: 3.5;
|
|
131
|
+
readonly cache_read: 0.075;
|
|
132
|
+
};
|
|
128
133
|
readonly "glm-5.2": {
|
|
129
134
|
readonly input: 1.4;
|
|
130
135
|
readonly output: 4.4;
|
|
@@ -185,6 +190,12 @@ export declare const CURSOR_MODEL_COSTS: {
|
|
|
185
190
|
readonly output: 1.2;
|
|
186
191
|
readonly cache_read: 0.02;
|
|
187
192
|
readonly cache_write: 0.25;
|
|
193
|
+
readonly context_over_200k: {
|
|
194
|
+
readonly input: 0.4;
|
|
195
|
+
readonly output: 1.2;
|
|
196
|
+
readonly cache_read: 0.04;
|
|
197
|
+
readonly cache_write: 0.5;
|
|
198
|
+
};
|
|
188
199
|
};
|
|
189
200
|
readonly "gpt-5.6-sol": {
|
|
190
201
|
readonly input: 4;
|
|
@@ -203,6 +214,12 @@ export declare const CURSOR_MODEL_COSTS: {
|
|
|
203
214
|
readonly output: 12;
|
|
204
215
|
readonly cache_read: 0.2;
|
|
205
216
|
readonly cache_write: 2.5;
|
|
217
|
+
readonly context_over_200k: {
|
|
218
|
+
readonly input: 4;
|
|
219
|
+
readonly output: 12;
|
|
220
|
+
readonly cache_read: 0.4;
|
|
221
|
+
readonly cache_write: 5;
|
|
222
|
+
};
|
|
206
223
|
};
|
|
207
224
|
readonly "grok-4.5": {
|
|
208
225
|
readonly input: 2;
|
|
@@ -234,6 +251,11 @@ export declare const CURSOR_MODEL_COSTS: {
|
|
|
234
251
|
readonly output: 15;
|
|
235
252
|
readonly cache_read: 0.3;
|
|
236
253
|
};
|
|
254
|
+
readonly "muse-spark-1.3": {
|
|
255
|
+
readonly input: 1.25;
|
|
256
|
+
readonly output: 4.25;
|
|
257
|
+
readonly cache_read: 0.15;
|
|
258
|
+
};
|
|
237
259
|
};
|
|
238
260
|
export declare const CURSOR_MODEL_CONTEXTS: {
|
|
239
261
|
readonly "claude-fable-5": {
|
|
@@ -310,6 +332,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
|
|
|
310
332
|
readonly maxContext: 200000;
|
|
311
333
|
readonly maxContextForMaxMode: 1000000;
|
|
312
334
|
};
|
|
335
|
+
readonly "gemini-3.8-flash": {
|
|
336
|
+
readonly maxContext: 200000;
|
|
337
|
+
readonly maxContextForMaxMode: 1000000;
|
|
338
|
+
};
|
|
313
339
|
readonly "glm-5.2": {
|
|
314
340
|
readonly maxContext: 200000;
|
|
315
341
|
};
|
|
@@ -341,6 +367,7 @@ export declare const CURSOR_MODEL_CONTEXTS: {
|
|
|
341
367
|
};
|
|
342
368
|
readonly "gpt-5.6-luna": {
|
|
343
369
|
readonly maxContext: 272000;
|
|
370
|
+
readonly maxContextForMaxMode: 1000000;
|
|
344
371
|
};
|
|
345
372
|
readonly "gpt-5.6-sol": {
|
|
346
373
|
readonly maxContext: 272000;
|
|
@@ -348,6 +375,7 @@ export declare const CURSOR_MODEL_CONTEXTS: {
|
|
|
348
375
|
};
|
|
349
376
|
readonly "gpt-5.6-terra": {
|
|
350
377
|
readonly maxContext: 272000;
|
|
378
|
+
readonly maxContextForMaxMode: 1000000;
|
|
351
379
|
};
|
|
352
380
|
readonly "grok-4.5": {
|
|
353
381
|
readonly maxContext: 256000;
|
|
@@ -362,6 +390,10 @@ export declare const CURSOR_MODEL_CONTEXTS: {
|
|
|
362
390
|
readonly maxContext: 200000;
|
|
363
391
|
readonly maxContextForMaxMode: 1000000;
|
|
364
392
|
};
|
|
393
|
+
readonly "muse-spark-1.3": {
|
|
394
|
+
readonly maxContext: 300000;
|
|
395
|
+
readonly maxContextForMaxMode: 1000000;
|
|
396
|
+
};
|
|
365
397
|
};
|
|
366
398
|
export declare const CURSOR_MODEL_CAPABILITIES: {
|
|
367
399
|
readonly "claude-fable-5": {
|
|
@@ -421,6 +453,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
|
|
|
421
453
|
readonly "gemini-3.7-flash": {
|
|
422
454
|
readonly supportsImages: true;
|
|
423
455
|
};
|
|
456
|
+
readonly "gemini-3.8-flash": {
|
|
457
|
+
readonly supportsImages: true;
|
|
458
|
+
};
|
|
424
459
|
readonly "glm-5.2": {
|
|
425
460
|
readonly supportsImages: false;
|
|
426
461
|
};
|
|
@@ -469,4 +504,7 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
|
|
|
469
504
|
readonly "kimi-k3": {
|
|
470
505
|
readonly supportsImages: true;
|
|
471
506
|
};
|
|
507
|
+
readonly "muse-spark-1.3": {
|
|
508
|
+
readonly supportsImages: true;
|
|
509
|
+
};
|
|
472
510
|
};
|
package/dist/pricing-data.js
CHANGED
|
@@ -125,6 +125,11 @@ export const CURSOR_MODEL_COSTS = {
|
|
|
125
125
|
"output": 3.5,
|
|
126
126
|
"cache_read": 0.075
|
|
127
127
|
},
|
|
128
|
+
"gemini-3.8-flash": {
|
|
129
|
+
"input": 0.75,
|
|
130
|
+
"output": 3.5,
|
|
131
|
+
"cache_read": 0.075
|
|
132
|
+
},
|
|
128
133
|
"glm-5.2": {
|
|
129
134
|
"input": 1.4,
|
|
130
135
|
"output": 4.4,
|
|
@@ -184,7 +189,13 @@ export const CURSOR_MODEL_COSTS = {
|
|
|
184
189
|
"input": 0.2,
|
|
185
190
|
"output": 1.2,
|
|
186
191
|
"cache_read": 0.02,
|
|
187
|
-
"cache_write": 0.25
|
|
192
|
+
"cache_write": 0.25,
|
|
193
|
+
"context_over_200k": {
|
|
194
|
+
"input": 0.4,
|
|
195
|
+
"output": 1.2,
|
|
196
|
+
"cache_read": 0.04,
|
|
197
|
+
"cache_write": 0.5
|
|
198
|
+
}
|
|
188
199
|
},
|
|
189
200
|
"gpt-5.6-sol": {
|
|
190
201
|
"input": 4,
|
|
@@ -202,7 +213,13 @@ export const CURSOR_MODEL_COSTS = {
|
|
|
202
213
|
"input": 2,
|
|
203
214
|
"output": 12,
|
|
204
215
|
"cache_read": 0.2,
|
|
205
|
-
"cache_write": 2.5
|
|
216
|
+
"cache_write": 2.5,
|
|
217
|
+
"context_over_200k": {
|
|
218
|
+
"input": 4,
|
|
219
|
+
"output": 12,
|
|
220
|
+
"cache_read": 0.4,
|
|
221
|
+
"cache_write": 5
|
|
222
|
+
}
|
|
206
223
|
},
|
|
207
224
|
"grok-4.5": {
|
|
208
225
|
"input": 2,
|
|
@@ -233,6 +250,11 @@ export const CURSOR_MODEL_COSTS = {
|
|
|
233
250
|
"input": 3,
|
|
234
251
|
"output": 15,
|
|
235
252
|
"cache_read": 0.3
|
|
253
|
+
},
|
|
254
|
+
"muse-spark-1.3": {
|
|
255
|
+
"input": 1.25,
|
|
256
|
+
"output": 4.25,
|
|
257
|
+
"cache_read": 0.15
|
|
236
258
|
}
|
|
237
259
|
};
|
|
238
260
|
export const CURSOR_MODEL_CONTEXTS = {
|
|
@@ -310,6 +332,10 @@ export const CURSOR_MODEL_CONTEXTS = {
|
|
|
310
332
|
"maxContext": 200000,
|
|
311
333
|
"maxContextForMaxMode": 1000000
|
|
312
334
|
},
|
|
335
|
+
"gemini-3.8-flash": {
|
|
336
|
+
"maxContext": 200000,
|
|
337
|
+
"maxContextForMaxMode": 1000000
|
|
338
|
+
},
|
|
313
339
|
"glm-5.2": {
|
|
314
340
|
"maxContext": 200000
|
|
315
341
|
},
|
|
@@ -340,14 +366,16 @@ export const CURSOR_MODEL_CONTEXTS = {
|
|
|
340
366
|
"maxContextForMaxMode": 1000000
|
|
341
367
|
},
|
|
342
368
|
"gpt-5.6-luna": {
|
|
343
|
-
"maxContext": 272000
|
|
369
|
+
"maxContext": 272000,
|
|
370
|
+
"maxContextForMaxMode": 1000000
|
|
344
371
|
},
|
|
345
372
|
"gpt-5.6-sol": {
|
|
346
373
|
"maxContext": 272000,
|
|
347
374
|
"maxContextForMaxMode": 1000000
|
|
348
375
|
},
|
|
349
376
|
"gpt-5.6-terra": {
|
|
350
|
-
"maxContext": 272000
|
|
377
|
+
"maxContext": 272000,
|
|
378
|
+
"maxContextForMaxMode": 1000000
|
|
351
379
|
},
|
|
352
380
|
"grok-4.5": {
|
|
353
381
|
"maxContext": 256000
|
|
@@ -361,6 +389,10 @@ export const CURSOR_MODEL_CONTEXTS = {
|
|
|
361
389
|
"kimi-k3": {
|
|
362
390
|
"maxContext": 200000,
|
|
363
391
|
"maxContextForMaxMode": 1000000
|
|
392
|
+
},
|
|
393
|
+
"muse-spark-1.3": {
|
|
394
|
+
"maxContext": 300000,
|
|
395
|
+
"maxContextForMaxMode": 1000000
|
|
364
396
|
}
|
|
365
397
|
};
|
|
366
398
|
export const CURSOR_MODEL_CAPABILITIES = {
|
|
@@ -421,6 +453,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
|
|
|
421
453
|
"gemini-3.7-flash": {
|
|
422
454
|
"supportsImages": true
|
|
423
455
|
},
|
|
456
|
+
"gemini-3.8-flash": {
|
|
457
|
+
"supportsImages": true
|
|
458
|
+
},
|
|
424
459
|
"glm-5.2": {
|
|
425
460
|
"supportsImages": false
|
|
426
461
|
},
|
|
@@ -468,5 +503,8 @@ export const CURSOR_MODEL_CAPABILITIES = {
|
|
|
468
503
|
},
|
|
469
504
|
"kimi-k3": {
|
|
470
505
|
"supportsImages": true
|
|
506
|
+
},
|
|
507
|
+
"muse-spark-1.3": {
|
|
508
|
+
"supportsImages": true
|
|
471
509
|
}
|
|
472
510
|
};
|
|
@@ -257,11 +257,24 @@ export function createMessageTypes() {
|
|
|
257
257
|
addType(root, "PiFindToolCall", [{ id: 1, name: "args", type: "PiFindToolArgs" }]);
|
|
258
258
|
addType(root, "PiLsToolCall", [{ id: 1, name: "args", type: "PiLsToolArgs" }]);
|
|
259
259
|
// Display-only native tools Cursor may complete without an ExecServerMessage.
|
|
260
|
+
// ReadTodos mirrors UpdateTodos: args + result (see agent.v1 ReadTodosToolCall).
|
|
260
261
|
addType(root, "ReadTodosArgs", [
|
|
261
262
|
{ id: 1, name: "status_filter", type: "TodoStatus", repeated: true },
|
|
262
263
|
{ id: 2, name: "id_filter", type: "string", repeated: true },
|
|
263
264
|
]);
|
|
264
|
-
addType(root, "
|
|
265
|
+
addType(root, "ReadTodosSuccess", [
|
|
266
|
+
{ id: 1, name: "todos", type: "TodoItem", repeated: true },
|
|
267
|
+
{ id: 2, name: "total_count", type: "int32" },
|
|
268
|
+
]);
|
|
269
|
+
addType(root, "ReadTodosError", [{ id: 1, name: "error", type: "string" }]);
|
|
270
|
+
addType(root, "ReadTodosResult", [
|
|
271
|
+
{ id: 1, name: "success", type: "ReadTodosSuccess" },
|
|
272
|
+
{ id: 2, name: "error", type: "ReadTodosError" },
|
|
273
|
+
], [{ name: "result", fields: ["success", "error"] }]);
|
|
274
|
+
addType(root, "ReadTodosToolCall", [
|
|
275
|
+
{ id: 1, name: "args", type: "ReadTodosArgs" },
|
|
276
|
+
{ id: 2, name: "result", type: "ReadTodosResult" },
|
|
277
|
+
]);
|
|
265
278
|
addType(root, "AwaitArgs", [
|
|
266
279
|
{ id: 1, name: "task_id", type: "string" },
|
|
267
280
|
{ id: 2, name: "block_until_ms", type: "uint32" },
|
|
@@ -19,10 +19,38 @@ export type BridgedOpenCodeToolCall = {
|
|
|
19
19
|
callId: string;
|
|
20
20
|
variant: string;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a full todo snapshot for `mirroredTodos` storage. Returns
|
|
24
|
+
* undefined when the input is not a todo array (nothing to learn). Drops the
|
|
25
|
+
* synthetic "plan" id minted for create_plan mirrors so later merges never
|
|
26
|
+
* resurrect it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function snapshotMirroredTodos(todos: unknown): Array<Record<string, unknown>> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Refresh the mirrored snapshot from a host `todoread` tool result. The host
|
|
31
|
+
* list is authoritative; returns undefined when the output is not recognizable
|
|
32
|
+
* todo JSON (prose, caps, errors) so a stale-but-useful snapshot survives.
|
|
33
|
+
*/
|
|
34
|
+
export declare function snapshotMirroredTodosFromReadOutput(output: string): Array<Record<string, unknown>> | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Overlay Cursor merge patches onto a previously mirrored full todo list.
|
|
37
|
+
* Preserves prior order; appends newly introduced ids in patch order.
|
|
38
|
+
*
|
|
39
|
+
* Matching is by id first, then by exact (trimmed) content. The content
|
|
40
|
+
* fallback covers mixed flows where the prior snapshot came from a direct
|
|
41
|
+
* host `todowrite` (positional ids) while the patch references Cursor-side
|
|
42
|
+
* ids for the same tasks. Patch items with neither an id/content match nor
|
|
43
|
+
* their own content are skipped — a status-only update for an unknown item
|
|
44
|
+
* cannot be mapped onto the host list.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applyTodoMerge(prior: ReadonlyArray<Record<string, unknown>>, patch: unknown): Array<Record<string, unknown>>;
|
|
22
47
|
/**
|
|
23
48
|
* Decode a Cursor ToolCall oneof into a display tool call we can bridge.
|
|
49
|
+
*
|
|
50
|
+
* @param priorMirroredTodos Last full todo snapshot mirrored this Run; used to
|
|
51
|
+
* expand Cursor `merge: true` updates that omit ToolCallCompleted.success.todos.
|
|
24
52
|
*/
|
|
25
|
-
export declare function parseDisplayToolCall(callId: string, toolCall: Record<string, unknown> | undefined): DisplayToolCall | undefined;
|
|
53
|
+
export declare function parseDisplayToolCall(callId: string, toolCall: Record<string, unknown> | undefined, priorMirroredTodos?: ReadonlyArray<Record<string, unknown>>): DisplayToolCall | undefined;
|
|
26
54
|
/**
|
|
27
55
|
* Pick an advertised OpenCode tool for a display call, remapping args as needed.
|
|
28
56
|
* Returns undefined when nothing compatible is advertised this turn.
|
|
@@ -46,11 +46,13 @@ const VARIANT_TO_OPENCODE = {
|
|
|
46
46
|
const TOOL_CALL_VARIANTS = Object.keys(VARIANT_TO_OPENCODE);
|
|
47
47
|
// ToolCallCompleted is a notification, not an execution request. Only mirror
|
|
48
48
|
// client-visible state whose authoritative final value is already present in
|
|
49
|
-
// the completed payload
|
|
49
|
+
// the completed payload (or reconstructable from a prior mirrored snapshot for
|
|
50
|
+
// Cursor todo merges). Data-returning, interactive, and side-effecting calls
|
|
50
51
|
// must use an exec/interaction request channel where Cursor can receive their
|
|
51
52
|
// actual result.
|
|
52
53
|
const DISPLAY_STATE_MIRROR_VARIANTS = new Set([
|
|
53
54
|
"update_todos_tool_call",
|
|
55
|
+
"read_todos_tool_call",
|
|
54
56
|
"create_plan_tool_call",
|
|
55
57
|
]);
|
|
56
58
|
function asRecord(v) {
|
|
@@ -97,14 +99,169 @@ function mapTodos(raw) {
|
|
|
97
99
|
};
|
|
98
100
|
});
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Normalize a full todo snapshot for `mirroredTodos` storage. Returns
|
|
104
|
+
* undefined when the input is not a todo array (nothing to learn). Drops the
|
|
105
|
+
* synthetic "plan" id minted for create_plan mirrors so later merges never
|
|
106
|
+
* resurrect it.
|
|
107
|
+
*/
|
|
108
|
+
export function snapshotMirroredTodos(todos) {
|
|
109
|
+
if (!Array.isArray(todos))
|
|
110
|
+
return undefined;
|
|
111
|
+
return mapTodos(todos).filter((t) => {
|
|
112
|
+
const id = typeof t.id === "string" ? t.id : "";
|
|
113
|
+
return id.length > 0 && id !== "plan";
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Refresh the mirrored snapshot from a host `todoread` tool result. The host
|
|
118
|
+
* list is authoritative; returns undefined when the output is not recognizable
|
|
119
|
+
* todo JSON (prose, caps, errors) so a stale-but-useful snapshot survives.
|
|
120
|
+
*/
|
|
121
|
+
export function snapshotMirroredTodosFromReadOutput(output) {
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(output);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const list = Array.isArray(parsed) ? parsed : (asRecord(parsed)?.todos ?? undefined);
|
|
130
|
+
if (!Array.isArray(list))
|
|
131
|
+
return undefined;
|
|
132
|
+
return snapshotMirroredTodos(list);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Overlay Cursor merge patches onto a previously mirrored full todo list.
|
|
136
|
+
* Preserves prior order; appends newly introduced ids in patch order.
|
|
137
|
+
*
|
|
138
|
+
* Matching is by id first, then by exact (trimmed) content. The content
|
|
139
|
+
* fallback covers mixed flows where the prior snapshot came from a direct
|
|
140
|
+
* host `todowrite` (positional ids) while the patch references Cursor-side
|
|
141
|
+
* ids for the same tasks. Patch items with neither an id/content match nor
|
|
142
|
+
* their own content are skipped — a status-only update for an unknown item
|
|
143
|
+
* cannot be mapped onto the host list.
|
|
144
|
+
*/
|
|
145
|
+
export function applyTodoMerge(prior, patch) {
|
|
146
|
+
const rawPatch = Array.isArray(patch) ? patch : [];
|
|
147
|
+
const mappedPatch = mapTodos(patch);
|
|
148
|
+
const byId = new Map();
|
|
149
|
+
const byContent = new Map();
|
|
150
|
+
// Patch ids already folded into a prior record via content match: never
|
|
151
|
+
// re-emit them as new items in the output phase.
|
|
152
|
+
const consumedPatchIds = new Set();
|
|
153
|
+
// Patch ids accepted as genuinely new content: the only patch ids the
|
|
154
|
+
// output phase may append. Orphans skipped above (unknown id, no content)
|
|
155
|
+
// must not leak back in here.
|
|
156
|
+
const appendedPatchIds = new Set();
|
|
157
|
+
for (const item of prior) {
|
|
158
|
+
const id = typeof item.id === "string" && item.id.length > 0 ? item.id : "";
|
|
159
|
+
if (!id)
|
|
160
|
+
continue;
|
|
161
|
+
const normalized = {
|
|
162
|
+
id,
|
|
163
|
+
content: typeof item.content === "string" ? item.content : "",
|
|
164
|
+
status: typeof item.status === "string" ? item.status : mapTodoStatus(item.status),
|
|
165
|
+
priority: typeof item.priority === "string" && item.priority ? item.priority : "medium",
|
|
166
|
+
};
|
|
167
|
+
byId.set(id, normalized);
|
|
168
|
+
const key = normalized.content.trim();
|
|
169
|
+
if (key && !byContent.has(key))
|
|
170
|
+
byContent.set(key, normalized);
|
|
171
|
+
}
|
|
172
|
+
const overlay = (target, item, raw) => {
|
|
173
|
+
// Only overwrite fields the patch actually carries. mapTodos fills
|
|
174
|
+
// defaults ("" content, "pending" status, "medium" priority) that must not
|
|
175
|
+
// clobber a prior item when Cursor sends an id+status-only patch.
|
|
176
|
+
const next = { ...target, id: target.id };
|
|
177
|
+
if (typeof raw.content === "string" && raw.content.length > 0) {
|
|
178
|
+
next.content = raw.content;
|
|
179
|
+
}
|
|
180
|
+
if (raw.status !== undefined && raw.status !== null) {
|
|
181
|
+
next.status = String(item.status);
|
|
182
|
+
}
|
|
183
|
+
if (typeof raw.priority === "string" && raw.priority) {
|
|
184
|
+
next.priority = raw.priority;
|
|
185
|
+
}
|
|
186
|
+
return next;
|
|
187
|
+
};
|
|
188
|
+
for (let index = 0; index < mappedPatch.length; index++) {
|
|
189
|
+
const item = mappedPatch[index];
|
|
190
|
+
const raw = asRecord(rawPatch[index]) ?? {};
|
|
191
|
+
const id = String(item.id);
|
|
192
|
+
const byIdMatch = byId.get(id);
|
|
193
|
+
if (byIdMatch) {
|
|
194
|
+
const merged = overlay(byIdMatch, item, raw);
|
|
195
|
+
byId.set(id, merged);
|
|
196
|
+
// Keep the content index pointing at the fresh record so later
|
|
197
|
+
// content-fallback matches never merge onto a stale copy.
|
|
198
|
+
const oldKey = typeof byIdMatch.content === "string" ? byIdMatch.content.trim() : "";
|
|
199
|
+
const newKey = typeof merged.content === "string" ? merged.content.trim() : "";
|
|
200
|
+
if (oldKey && byContent.get(oldKey) === byIdMatch)
|
|
201
|
+
byContent.delete(oldKey);
|
|
202
|
+
if (newKey && !byContent.has(newKey))
|
|
203
|
+
byContent.set(newKey, merged);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const contentKey = typeof raw.content === "string" && raw.content.trim().length > 0
|
|
207
|
+
? raw.content.trim()
|
|
208
|
+
: "";
|
|
209
|
+
const byContentMatch = contentKey ? byContent.get(contentKey) : undefined;
|
|
210
|
+
if (byContentMatch) {
|
|
211
|
+
// Same task, different id space (host positional ids vs Cursor ids).
|
|
212
|
+
// Keep the prior id so snapshot identity stays stable and order is
|
|
213
|
+
// preserved; adopt the patch's status/priority/content.
|
|
214
|
+
const merged = overlay(byContentMatch, item, raw);
|
|
215
|
+
byId.set(String(byContentMatch.id), merged);
|
|
216
|
+
if (contentKey)
|
|
217
|
+
byContent.set(contentKey, merged);
|
|
218
|
+
consumedPatchIds.add(id);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (!contentKey)
|
|
222
|
+
continue;
|
|
223
|
+
byId.set(id, item);
|
|
224
|
+
byContent.set(contentKey, item);
|
|
225
|
+
appendedPatchIds.add(id);
|
|
226
|
+
}
|
|
227
|
+
const out = [];
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
for (const item of prior) {
|
|
230
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
231
|
+
if (!id || seen.has(id))
|
|
232
|
+
continue;
|
|
233
|
+
const next = byId.get(id);
|
|
234
|
+
if (next) {
|
|
235
|
+
out.push(next);
|
|
236
|
+
seen.add(id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const item of mappedPatch) {
|
|
240
|
+
const id = String(item.id);
|
|
241
|
+
// Id-matched items are already emitted under the prior id; content-matched
|
|
242
|
+
// items were folded into the prior record (not appended under the foreign
|
|
243
|
+
// patch id); orphans were skipped outright. Only genuinely new content
|
|
244
|
+
// reaches the host list.
|
|
245
|
+
if (seen.has(id) || consumedPatchIds.has(id))
|
|
246
|
+
continue;
|
|
247
|
+
if (!appendedPatchIds.has(id))
|
|
248
|
+
continue;
|
|
249
|
+
out.push(item);
|
|
250
|
+
seen.add(id);
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
100
254
|
function unwrapArgs(variantPayload) {
|
|
101
255
|
const nested = asRecord(variantPayload.args);
|
|
102
256
|
return nested ?? variantPayload;
|
|
103
257
|
}
|
|
104
258
|
/**
|
|
105
259
|
* Decode a Cursor ToolCall oneof into a display tool call we can bridge.
|
|
260
|
+
*
|
|
261
|
+
* @param priorMirroredTodos Last full todo snapshot mirrored this Run; used to
|
|
262
|
+
* expand Cursor `merge: true` updates that omit ToolCallCompleted.success.todos.
|
|
106
263
|
*/
|
|
107
|
-
export function parseDisplayToolCall(callId, toolCall) {
|
|
264
|
+
export function parseDisplayToolCall(callId, toolCall, priorMirroredTodos) {
|
|
108
265
|
if (!toolCall || !callId)
|
|
109
266
|
return undefined;
|
|
110
267
|
const variant = findToolVariant(toolCall);
|
|
@@ -136,10 +293,16 @@ export function parseDisplayToolCall(callId, toolCall) {
|
|
|
136
293
|
const success = asRecord(result?.success);
|
|
137
294
|
const completedTodos = Array.isArray(success?.todos) ? success.todos : undefined;
|
|
138
295
|
const isMerge = variant === "update_todos_tool_call" && args.merge === true;
|
|
139
|
-
// OpenCode todowrite replaces the whole list.
|
|
140
|
-
//
|
|
296
|
+
// OpenCode todowrite replaces the whole list. Prefer the completed payload;
|
|
297
|
+
// otherwise a non-merge args list; otherwise merge against the Run snapshot.
|
|
141
298
|
const sourceTodos = completedTodos ?? (!isMerge ? args.todos : undefined);
|
|
142
|
-
const
|
|
299
|
+
const canMergeFromPrior = sourceTodos === undefined &&
|
|
300
|
+
isMerge &&
|
|
301
|
+
Array.isArray(priorMirroredTodos) &&
|
|
302
|
+
priorMirroredTodos.length > 0;
|
|
303
|
+
const todos = canMergeFromPrior
|
|
304
|
+
? applyTodoMerge(priorMirroredTodos, args.todos)
|
|
305
|
+
: mapTodos(sourceTodos);
|
|
143
306
|
if (variant === "create_plan_tool_call") {
|
|
144
307
|
const overview = typeof args.overview === "string" ? args.overview.trim() : "";
|
|
145
308
|
const plan = typeof args.plan === "string" ? args.plan.trim() : "";
|
|
@@ -158,7 +321,21 @@ export function parseDisplayToolCall(callId, toolCall) {
|
|
|
158
321
|
variant,
|
|
159
322
|
preferredToolName: "todowrite",
|
|
160
323
|
args: { todos },
|
|
161
|
-
bridgeable: variant === "create_plan_tool_call"
|
|
324
|
+
bridgeable: variant === "create_plan_tool_call"
|
|
325
|
+
? todos.length > 0
|
|
326
|
+
: Array.isArray(sourceTodos) || canMergeFromPrior,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// Cursor TodoRead is the same display-only channel as TodoWrite (agent.v1
|
|
330
|
+
// read_todos_tool_call). OpenCode todoread takes an empty body; host filters
|
|
331
|
+
// are not part of the canonical schema, so drop status_filter / id_filter.
|
|
332
|
+
if (variant === "read_todos_tool_call") {
|
|
333
|
+
return {
|
|
334
|
+
callId,
|
|
335
|
+
variant,
|
|
336
|
+
preferredToolName: "todoread",
|
|
337
|
+
args: {},
|
|
338
|
+
bridgeable: true,
|
|
162
339
|
};
|
|
163
340
|
}
|
|
164
341
|
if (variant === "edit_tool_call") {
|
|
@@ -295,6 +472,9 @@ export function resolveBridgedOpenCodeToolCall(display, advertised) {
|
|
|
295
472
|
todos: mapTodos(display.args.todos ?? mapped.args.todos),
|
|
296
473
|
};
|
|
297
474
|
}
|
|
475
|
+
if (toolName === "todoread") {
|
|
476
|
+
mapped.args = {};
|
|
477
|
+
}
|
|
298
478
|
return {
|
|
299
479
|
toolName: mapped.toolName,
|
|
300
480
|
args: mapped.args,
|
package/dist/session.d.ts
CHANGED
|
@@ -147,6 +147,12 @@ export type CursorSession = {
|
|
|
147
147
|
* or when we bridge the completed display call into an OpenCode tool-call.
|
|
148
148
|
*/
|
|
149
149
|
displayToolCalls: Map<string, Record<string, unknown>>;
|
|
150
|
+
/**
|
|
151
|
+
* Last full todo snapshot successfully mirrored into OpenCode `todowrite`.
|
|
152
|
+
* Used to apply Cursor `merge: true` display updates that omit the final
|
|
153
|
+
* completed list so host todos still receive a replace-all snapshot.
|
|
154
|
+
*/
|
|
155
|
+
mirroredTodos?: Array<Record<string, unknown>>;
|
|
150
156
|
/**
|
|
151
157
|
* Legacy edit calls whose authoritative exec path is still in progress.
|
|
152
158
|
* Cursor implements these as read -> whole-file write; retaining the path
|
package/dist/session.js
CHANGED
|
@@ -387,6 +387,7 @@ export class SessionManager {
|
|
|
387
387
|
session.pumpOwner = null;
|
|
388
388
|
session.pumpActive = false;
|
|
389
389
|
session.displayToolCalls?.clear();
|
|
390
|
+
session.mirroredTodos = undefined;
|
|
390
391
|
session.blobs?.clear();
|
|
391
392
|
this.sessions.delete(session);
|
|
392
393
|
if (session.openCodeSessionId &&
|