@khalilgharbaoui/opencode-claude-code-plugin 0.58.0 → 0.60.0
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/dist/index.d.ts +29 -4
- package/dist/index.js +555 -84
- package/dist/index.js.map +1 -1
- package/dist/tui.js +72 -0
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +20 -4
package/dist/index.js
CHANGED
|
@@ -55,6 +55,7 @@ var V2_TOOLS = {
|
|
|
55
55
|
name: "subagent",
|
|
56
56
|
input: (input) => defined({
|
|
57
57
|
agent: input.subagent_type ?? input.agent,
|
|
58
|
+
model: input.model,
|
|
58
59
|
description: input.description,
|
|
59
60
|
prompt: input.prompt,
|
|
60
61
|
sessionID: input.task_id ?? input.sessionID,
|
|
@@ -272,6 +273,8 @@ var MAX_TOASTED_MESSAGES = 200;
|
|
|
272
273
|
var toastBurstLimit = 3;
|
|
273
274
|
var toastBurstWindowMs = 2e3;
|
|
274
275
|
var activeSink = null;
|
|
276
|
+
var activeNotifier = null;
|
|
277
|
+
var bufferedNotices = [];
|
|
275
278
|
var bufferedEntries = [];
|
|
276
279
|
var droppedBeforeSink = 0;
|
|
277
280
|
var toastedMessages = /* @__PURE__ */ new Set();
|
|
@@ -296,6 +299,13 @@ function setLogSink(sink) {
|
|
|
296
299
|
});
|
|
297
300
|
}
|
|
298
301
|
}
|
|
302
|
+
function setToastNotifier(notifier) {
|
|
303
|
+
activeNotifier = notifier;
|
|
304
|
+
if (!notifier) return;
|
|
305
|
+
const pending2 = bufferedNotices;
|
|
306
|
+
bufferedNotices = [];
|
|
307
|
+
for (const entry of pending2) maybeToast(entry, notifier);
|
|
308
|
+
}
|
|
299
309
|
function routeToSink(entry) {
|
|
300
310
|
if (!activeSink) {
|
|
301
311
|
if (bufferedEntries.length >= MAX_BUFFERED_SINK_ENTRIES) {
|
|
@@ -314,18 +324,34 @@ function deliverToSink(entry) {
|
|
|
314
324
|
sink.log(entry);
|
|
315
325
|
} catch {
|
|
316
326
|
}
|
|
327
|
+
maybeToast(entry, (toast) => sink.toast(toast));
|
|
328
|
+
}
|
|
329
|
+
function maybeToast(entry, send) {
|
|
317
330
|
if (entry.level !== "warn" && entry.level !== "error") return;
|
|
318
331
|
if (toastedMessages.has(entry.message)) return;
|
|
319
332
|
if (toastedMessages.size < MAX_TOASTED_MESSAGES) toastedMessages.add(entry.message);
|
|
320
333
|
if (!allowToastNow()) return;
|
|
321
|
-
sendToast(
|
|
334
|
+
sendToast(send, entry);
|
|
322
335
|
}
|
|
323
|
-
function sendToast(
|
|
336
|
+
function sendToast(send, entry) {
|
|
324
337
|
try {
|
|
325
|
-
|
|
338
|
+
send(entry);
|
|
326
339
|
} catch {
|
|
327
340
|
}
|
|
328
341
|
}
|
|
342
|
+
function toastTarget() {
|
|
343
|
+
const sink = activeSink;
|
|
344
|
+
if (sink) return (entry) => sink.toast(entry);
|
|
345
|
+
return activeNotifier;
|
|
346
|
+
}
|
|
347
|
+
function routeToNotifier(entry) {
|
|
348
|
+
const notifier = activeNotifier;
|
|
349
|
+
if (notifier) {
|
|
350
|
+
maybeToast(entry, notifier);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (bufferedNotices.length < MAX_BUFFERED_SINK_ENTRIES) bufferedNotices.push(entry);
|
|
354
|
+
}
|
|
329
355
|
function allowToastNow() {
|
|
330
356
|
const now = Date.now();
|
|
331
357
|
if (now - burstWindowStart >= toastBurstWindowMs) {
|
|
@@ -347,10 +373,11 @@ function armBurstSummary() {
|
|
|
347
373
|
const suppressed = burstSuppressed;
|
|
348
374
|
burstSuppressed = 0;
|
|
349
375
|
if (suppressed <= 0) return;
|
|
350
|
-
const
|
|
351
|
-
if (!
|
|
352
|
-
const
|
|
353
|
-
|
|
376
|
+
const target = toastTarget();
|
|
377
|
+
if (!target) return;
|
|
378
|
+
const where = activeSink ? "opencode's log" : "the plugin log file";
|
|
379
|
+
const message = `${suppressed} more claude-code warning(s) were not shown; the full text is in ${where}`;
|
|
380
|
+
sendToast(target, { level: "warn", message, line: fmt("WARN", message) });
|
|
354
381
|
}, toastBurstWindowMs);
|
|
355
382
|
burstSummaryTimer.unref?.();
|
|
356
383
|
}
|
|
@@ -366,6 +393,7 @@ function emit(level, msg, data) {
|
|
|
366
393
|
routeToSink({ level, message: msg, data, line });
|
|
367
394
|
} else {
|
|
368
395
|
console.error(line);
|
|
396
|
+
if (level === "warn" || level === "error") routeToNotifier({ level, message: msg, data, line });
|
|
369
397
|
}
|
|
370
398
|
}
|
|
371
399
|
writeToFile(line);
|
|
@@ -3225,17 +3253,20 @@ function dispatchTasksFromCalls(calls, batchTasks) {
|
|
|
3225
3253
|
const out = [];
|
|
3226
3254
|
for (const call of calls) {
|
|
3227
3255
|
if (call.toolName === "task") {
|
|
3228
|
-
out.push(taskFromInput(call.toolCallId, null, call.input));
|
|
3256
|
+
if (!namesOwnModel(call.input)) out.push(taskFromInput(call.toolCallId, null, call.input));
|
|
3229
3257
|
continue;
|
|
3230
3258
|
}
|
|
3231
3259
|
if (call.toolName === "task_batch") {
|
|
3232
3260
|
for (const [index, task] of batchTasks(call.input).entries()) {
|
|
3233
|
-
out.push(taskFromInput(call.toolCallId, index, task));
|
|
3261
|
+
if (!namesOwnModel(task)) out.push(taskFromInput(call.toolCallId, index, task));
|
|
3234
3262
|
}
|
|
3235
3263
|
}
|
|
3236
3264
|
}
|
|
3237
3265
|
return out;
|
|
3238
3266
|
}
|
|
3267
|
+
function namesOwnModel(input) {
|
|
3268
|
+
return typeof input.model === "string" && input.model.trim() !== "";
|
|
3269
|
+
}
|
|
3239
3270
|
function taskFromInput(callId, batchIndex, input) {
|
|
3240
3271
|
return {
|
|
3241
3272
|
callId,
|
|
@@ -4691,6 +4722,13 @@ var TASK_INPUT_PROPERTIES = {
|
|
|
4691
4722
|
type: "string",
|
|
4692
4723
|
description: "The type of specialized agent to use for this task"
|
|
4693
4724
|
},
|
|
4725
|
+
// After the agent type, where opencode 2's own `subagent` tool puts it
|
|
4726
|
+
// (after `prompt`): first in the list it was the field the model read first.
|
|
4727
|
+
// Stripped on a host whose `task` has no such field (`applyTaskModelSupport`).
|
|
4728
|
+
model: {
|
|
4729
|
+
type: "string",
|
|
4730
|
+
description: "NEVER set this unless the user explicitly asked for a particular model or variant for this subagent. The exact opencode model reference, providerID/modelID, or providerID/modelID#variant to pick a variant. Use only an id the user gave you or one this conversation already used; never guess one. Omit it and opencode picks the model as usual."
|
|
4731
|
+
},
|
|
4694
4732
|
task_id: {
|
|
4695
4733
|
type: "string",
|
|
4696
4734
|
description: "Set this only if you mean to resume a previous task: pass the prior task_id to continue the same subagent session instead of creating a fresh one."
|
|
@@ -4713,6 +4751,64 @@ var TASK_INPUT_PROPERTIES = {
|
|
|
4713
4751
|
var TASK_INPUT_PROPERTIES_NO_BACKGROUND = Object.fromEntries(
|
|
4714
4752
|
Object.entries(TASK_INPUT_PROPERTIES).filter(([name]) => name !== "background")
|
|
4715
4753
|
);
|
|
4754
|
+
function liveTaskSupportsModel(taskParameters, dialect = "v1") {
|
|
4755
|
+
if (dialect === "v2") return true;
|
|
4756
|
+
const properties = taskParameters?.properties;
|
|
4757
|
+
if (!properties || typeof properties !== "object") return false;
|
|
4758
|
+
return "model" in properties;
|
|
4759
|
+
}
|
|
4760
|
+
function applyTaskModelSupport(tools, supported) {
|
|
4761
|
+
if (supported) return tools;
|
|
4762
|
+
const withoutModel = (schema) => {
|
|
4763
|
+
const properties = schema?.properties;
|
|
4764
|
+
if (!properties || typeof properties !== "object") return schema;
|
|
4765
|
+
return {
|
|
4766
|
+
...schema,
|
|
4767
|
+
properties: Object.fromEntries(
|
|
4768
|
+
Object.entries(properties).filter(([name]) => name !== "model")
|
|
4769
|
+
)
|
|
4770
|
+
};
|
|
4771
|
+
};
|
|
4772
|
+
return tools.map((tool) => {
|
|
4773
|
+
if (tool.name === "task") return { ...tool, inputSchema: withoutModel(tool.inputSchema) };
|
|
4774
|
+
if (tool.name !== TASK_BATCH_TOOL_NAME) return tool;
|
|
4775
|
+
const properties = tool.inputSchema.properties;
|
|
4776
|
+
const tasks = properties?.tasks;
|
|
4777
|
+
if (!tasks?.items || typeof tasks.items !== "object") return tool;
|
|
4778
|
+
return {
|
|
4779
|
+
...tool,
|
|
4780
|
+
inputSchema: {
|
|
4781
|
+
...tool.inputSchema,
|
|
4782
|
+
properties: {
|
|
4783
|
+
...properties,
|
|
4784
|
+
tasks: { ...tasks, items: withoutModel(tasks.items) }
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
};
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
function taskSchemaOffersModel(def) {
|
|
4791
|
+
const properties = def.inputSchema.properties;
|
|
4792
|
+
const schema = def.name === TASK_BATCH_TOOL_NAME ? properties?.tasks?.items : def.inputSchema;
|
|
4793
|
+
const fields = schema?.properties;
|
|
4794
|
+
return !!fields && typeof fields === "object" && "model" in fields;
|
|
4795
|
+
}
|
|
4796
|
+
function taskModelInputError(def, input, guard) {
|
|
4797
|
+
const tasks = def.name === TASK_BATCH_TOOL_NAME ? taskBatchTasks(input) : [input];
|
|
4798
|
+
const offered = taskSchemaOffersModel(def);
|
|
4799
|
+
for (const task of tasks) {
|
|
4800
|
+
if (task.model === void 0) continue;
|
|
4801
|
+
if (!offered) {
|
|
4802
|
+
return "This opencode's task tool takes no per-call `model` (opencode 1.x has none), so no tasks were started. Omit `model`.";
|
|
4803
|
+
}
|
|
4804
|
+
if (typeof task.model !== "string") {
|
|
4805
|
+
return "`model` must be a string, providerID/modelID or providerID/modelID#variant, so no tasks were started.";
|
|
4806
|
+
}
|
|
4807
|
+
const refusal = guard?.(task.model);
|
|
4808
|
+
if (refusal) return refusal;
|
|
4809
|
+
}
|
|
4810
|
+
return null;
|
|
4811
|
+
}
|
|
4716
4812
|
var DEFAULT_PROXY_TOOLS = [
|
|
4717
4813
|
{
|
|
4718
4814
|
name: "bash",
|
|
@@ -5005,7 +5101,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
5005
5101
|
const params = request.params ?? {};
|
|
5006
5102
|
const toolName = String(params.name ?? "");
|
|
5007
5103
|
const input = params.arguments ?? {};
|
|
5008
|
-
|
|
5104
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
5105
|
+
if (!tool) {
|
|
5009
5106
|
writeJson(res, {
|
|
5010
5107
|
jsonrpc: "2.0",
|
|
5011
5108
|
id: requestId,
|
|
@@ -5023,6 +5120,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
5023
5120
|
return;
|
|
5024
5121
|
}
|
|
5025
5122
|
}
|
|
5123
|
+
if (toolName === "task" || toolName === TASK_BATCH_TOOL_NAME) {
|
|
5124
|
+
const problem = taskModelInputError(tool, input, options.taskModelGuard);
|
|
5125
|
+
if (problem) {
|
|
5126
|
+
writeToolCallResult(res, requestId, { kind: "error", message: problem });
|
|
5127
|
+
return;
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5026
5130
|
const interceptor = interceptors?.get(toolName);
|
|
5027
5131
|
if (interceptor) {
|
|
5028
5132
|
let intercepted;
|
|
@@ -5781,6 +5885,112 @@ function clearLedger(sessionId) {
|
|
|
5781
5885
|
ledgers.delete(sessionId);
|
|
5782
5886
|
}
|
|
5783
5887
|
|
|
5888
|
+
// src/task-model.ts
|
|
5889
|
+
var TASK_MODEL_REFERENCE = /^([^/#]+)\/([^#]+)(?:#([^#]+))?$/;
|
|
5890
|
+
function parseTaskModelReference(value) {
|
|
5891
|
+
if (typeof value !== "string") return null;
|
|
5892
|
+
const match = TASK_MODEL_REFERENCE.exec(value.trim());
|
|
5893
|
+
if (!match) return null;
|
|
5894
|
+
const [, providerID, modelID, variant] = match;
|
|
5895
|
+
return variant ? { providerID, modelID, variant } : { providerID, modelID };
|
|
5896
|
+
}
|
|
5897
|
+
function taskModelAccountRefusal(input) {
|
|
5898
|
+
if (!input.groups || input.crossGroup) return null;
|
|
5899
|
+
const reference = parseTaskModelReference(input.model);
|
|
5900
|
+
if (!reference) return null;
|
|
5901
|
+
const target = input.providers.get(reference.providerID);
|
|
5902
|
+
if (target === void 0) return null;
|
|
5903
|
+
if (accountsShareGroup(input.sourceAccount, target, input.groups)) return null;
|
|
5904
|
+
log.notice("task model refused: its account is in another account group", {
|
|
5905
|
+
account: target,
|
|
5906
|
+
reason: "another-account-group"
|
|
5907
|
+
});
|
|
5908
|
+
return `The model "${input.model}" runs on the Claude account "${target}", which is outside this conversation's account group, so no tasks were started. A subagent is handed task text that can quote this conversation, and accountGroups keeps that text inside one group. Omit \`model\`, or name a model on an account in this conversation's group.`;
|
|
5909
|
+
}
|
|
5910
|
+
var CLAIM_TTL_MS2 = 15 * 6e4;
|
|
5911
|
+
var MAX_CLAIMS2 = 512;
|
|
5912
|
+
var MAX_SESSION_MODELS = 512;
|
|
5913
|
+
var claims2 = [];
|
|
5914
|
+
var sessionModels = /* @__PURE__ */ new Map();
|
|
5915
|
+
function pruneClaims2(now) {
|
|
5916
|
+
for (let i = claims2.length - 1; i >= 0; i--) {
|
|
5917
|
+
if (now - claims2[i].createdAt > CLAIM_TTL_MS2) claims2.splice(i, 1);
|
|
5918
|
+
}
|
|
5919
|
+
while (claims2.length > MAX_CLAIMS2) claims2.shift();
|
|
5920
|
+
}
|
|
5921
|
+
function holdSessionModel(sessionId, model) {
|
|
5922
|
+
sessionModels.delete(sessionId);
|
|
5923
|
+
sessionModels.set(sessionId, model);
|
|
5924
|
+
while (sessionModels.size > MAX_SESSION_MODELS) {
|
|
5925
|
+
const oldest = sessionModels.keys().next().value;
|
|
5926
|
+
if (oldest === void 0) break;
|
|
5927
|
+
sessionModels.delete(oldest);
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
function noteExplicitTaskModels(parentSessionId, tasks, now = Date.now()) {
|
|
5931
|
+
let noted = 0;
|
|
5932
|
+
for (const task of tasks) {
|
|
5933
|
+
const reference = parseTaskModelReference(task.model);
|
|
5934
|
+
if (!reference) continue;
|
|
5935
|
+
const resumed = typeof task.task_id === "string" ? task.task_id.trim() : "";
|
|
5936
|
+
if (resumed) {
|
|
5937
|
+
holdSessionModel(resumed, reference);
|
|
5938
|
+
noted++;
|
|
5939
|
+
continue;
|
|
5940
|
+
}
|
|
5941
|
+
if (!parentSessionId || parentSessionId === "default") continue;
|
|
5942
|
+
pruneClaims2(now);
|
|
5943
|
+
claims2.push({
|
|
5944
|
+
parentSessionId,
|
|
5945
|
+
agent: typeof task.subagent_type === "string" ? task.subagent_type : "general",
|
|
5946
|
+
prompt: typeof task.prompt === "string" ? task.prompt : "",
|
|
5947
|
+
model: reference,
|
|
5948
|
+
createdAt: now
|
|
5949
|
+
});
|
|
5950
|
+
noted++;
|
|
5951
|
+
}
|
|
5952
|
+
return noted;
|
|
5953
|
+
}
|
|
5954
|
+
function explicitTaskModelFor(sessionId) {
|
|
5955
|
+
return sessionModels.get(sessionId);
|
|
5956
|
+
}
|
|
5957
|
+
function hasExplicitTaskModelCandidate(agent, prompt, now = Date.now()) {
|
|
5958
|
+
if (!agent || prompt === void 0 || claims2.length === 0) return false;
|
|
5959
|
+
pruneClaims2(now);
|
|
5960
|
+
return claims2.some(
|
|
5961
|
+
(claim) => claim.agent === agent && promptMatchesClaim(claim.prompt, prompt)
|
|
5962
|
+
);
|
|
5963
|
+
}
|
|
5964
|
+
function claimExplicitTaskModel(input) {
|
|
5965
|
+
const held = sessionModels.get(input.sessionId);
|
|
5966
|
+
if (held) return held;
|
|
5967
|
+
if (!input.agent || input.prompt === void 0 || !input.parentSessionId) return void 0;
|
|
5968
|
+
pruneClaims2(input.now ?? Date.now());
|
|
5969
|
+
const belongsHere = (claim2) => claim2.parentSessionId === input.parentSessionId && claim2.agent === input.agent;
|
|
5970
|
+
const childPrompt = input.prompt;
|
|
5971
|
+
let index = claims2.findIndex((claim2) => belongsHere(claim2) && claim2.prompt === childPrompt);
|
|
5972
|
+
if (index === -1) {
|
|
5973
|
+
index = claims2.findIndex(
|
|
5974
|
+
(claim2) => belongsHere(claim2) && promptMatchesClaim(claim2.prompt, childPrompt)
|
|
5975
|
+
);
|
|
5976
|
+
}
|
|
5977
|
+
if (index === -1) return void 0;
|
|
5978
|
+
const [claim] = claims2.splice(index, 1);
|
|
5979
|
+
holdSessionModel(input.sessionId, claim.model);
|
|
5980
|
+
log.info("explicit task model claimed by its child session", {
|
|
5981
|
+
sessionId: input.sessionId,
|
|
5982
|
+
agent: input.agent,
|
|
5983
|
+
model: claim.model.modelID
|
|
5984
|
+
});
|
|
5985
|
+
return claim.model;
|
|
5986
|
+
}
|
|
5987
|
+
function explicitTaskModelApplies(record, modelId) {
|
|
5988
|
+
return record !== void 0 && record.modelID === modelId;
|
|
5989
|
+
}
|
|
5990
|
+
function forgetExplicitTaskModel(sessionId) {
|
|
5991
|
+
sessionModels.delete(sessionId);
|
|
5992
|
+
}
|
|
5993
|
+
|
|
5784
5994
|
// src/compression-store.ts
|
|
5785
5995
|
var MAX_COMPRESSION_ENTRIES = 32;
|
|
5786
5996
|
var compressions = /* @__PURE__ */ new Map();
|
|
@@ -6374,10 +6584,71 @@ function watchChildSession(sessionID) {
|
|
|
6374
6584
|
});
|
|
6375
6585
|
return () => answer;
|
|
6376
6586
|
}
|
|
6587
|
+
var eventRunStates = /* @__PURE__ */ new Map();
|
|
6588
|
+
var MAX_EVENT_RUN_STATES = 512;
|
|
6589
|
+
var openEventFeeds = 0;
|
|
6590
|
+
function openSessionRunEventFeed() {
|
|
6591
|
+
openEventFeeds += 1;
|
|
6592
|
+
let closed = false;
|
|
6593
|
+
return () => {
|
|
6594
|
+
if (closed) return;
|
|
6595
|
+
closed = true;
|
|
6596
|
+
openEventFeeds = Math.max(0, openEventFeeds - 1);
|
|
6597
|
+
eventRunStates.clear();
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
function noteSessionRunEvent(event) {
|
|
6601
|
+
if (openEventFeeds === 0 || !event || typeof event !== "object") return;
|
|
6602
|
+
const { type, data } = event;
|
|
6603
|
+
if (typeof type !== "string" || !data || typeof data !== "object") return;
|
|
6604
|
+
const sessionID = data.sessionID;
|
|
6605
|
+
if (typeof sessionID !== "string" || !sessionID || sessionID === "default") return;
|
|
6606
|
+
if (type === "session.deleted") {
|
|
6607
|
+
eventRunStates.delete(sessionID);
|
|
6608
|
+
return;
|
|
6609
|
+
}
|
|
6610
|
+
let next;
|
|
6611
|
+
switch (type) {
|
|
6612
|
+
case "session.execution.started":
|
|
6613
|
+
next = "busy";
|
|
6614
|
+
break;
|
|
6615
|
+
case "session.execution.succeeded":
|
|
6616
|
+
case "session.execution.failed":
|
|
6617
|
+
case "session.execution.interrupted":
|
|
6618
|
+
case "session.idle":
|
|
6619
|
+
next = "idle";
|
|
6620
|
+
break;
|
|
6621
|
+
case "session.status": {
|
|
6622
|
+
const status = data.status?.type;
|
|
6623
|
+
if (typeof status === "string") next = status === "idle" ? "idle" : "busy";
|
|
6624
|
+
break;
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
if (!next) return;
|
|
6628
|
+
eventRunStates.delete(sessionID);
|
|
6629
|
+
eventRunStates.set(sessionID, next);
|
|
6630
|
+
while (eventRunStates.size > MAX_EVENT_RUN_STATES) {
|
|
6631
|
+
const oldest = eventRunStates.keys().next().value;
|
|
6632
|
+
if (oldest === void 0) break;
|
|
6633
|
+
eventRunStates.delete(oldest);
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
function sessionRunStateFromEvents(sessionID) {
|
|
6637
|
+
if (openEventFeeds === 0) return "unknown";
|
|
6638
|
+
return eventRunStates.get(sessionID) ?? "unknown";
|
|
6639
|
+
}
|
|
6640
|
+
function busySessionsFromEvents() {
|
|
6641
|
+
if (openEventFeeds === 0) return void 0;
|
|
6642
|
+
const busy = {};
|
|
6643
|
+
for (const [sessionID, state] of eventRunStates) {
|
|
6644
|
+
if (state === "busy") busy[sessionID] = { type: "busy" };
|
|
6645
|
+
}
|
|
6646
|
+
return busy;
|
|
6647
|
+
}
|
|
6377
6648
|
async function fetchSessionRunState(sessionID, directory) {
|
|
6378
6649
|
if (!sessionID || sessionID === "default") return "unknown";
|
|
6379
6650
|
const status = opencodeClient?.session?.status;
|
|
6380
|
-
if (!status) return
|
|
6651
|
+
if (!status) return sessionRunStateFromEvents(sessionID);
|
|
6381
6652
|
try {
|
|
6382
6653
|
const dir = directory ?? await fetchSessionDirectory(sessionID);
|
|
6383
6654
|
const res = await status.call(
|
|
@@ -7732,6 +8003,28 @@ function deleteActiveProcessesForSession(sessionID) {
|
|
|
7732
8003
|
for (const key of released) clearBackgroundTasks(key);
|
|
7733
8004
|
forgetSessionChoice(sessionID);
|
|
7734
8005
|
forgetDispatchChoicesForSession(sessionID);
|
|
8006
|
+
forgetExplicitTaskModel(sessionID);
|
|
8007
|
+
return released;
|
|
8008
|
+
}
|
|
8009
|
+
var STOPPED_TURN_MESSAGE = "opencode stopped the turn while it was running its proxy tool calls";
|
|
8010
|
+
function releaseParkedProxyCallsForSession(sessionID, reason = STOPPED_TURN_MESSAGE) {
|
|
8011
|
+
if (!sessionID || sessionID === "default") return [];
|
|
8012
|
+
const released = [];
|
|
8013
|
+
for (const [key, ap] of [...activeProcesses]) {
|
|
8014
|
+
const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
|
|
8015
|
+
if (!owned) continue;
|
|
8016
|
+
if (ap.lineEmitter.listenerCount("line") > 0) continue;
|
|
8017
|
+
if (getPendingProxyCalls(key).length === 0) continue;
|
|
8018
|
+
log.info("opencode stopped the turn at a proxy tool boundary; releasing pending calls", {
|
|
8019
|
+
sessionKey: key,
|
|
8020
|
+
sessionID
|
|
8021
|
+
});
|
|
8022
|
+
void interruptTurn(ap).then((idle) => {
|
|
8023
|
+
log.info("interrupt sent for aborted turn", { sk: key, idle });
|
|
8024
|
+
});
|
|
8025
|
+
rejectAllPendingProxyCallsForSession(key, new Error(reason));
|
|
8026
|
+
released.push(key);
|
|
8027
|
+
}
|
|
7735
8028
|
return released;
|
|
7736
8029
|
}
|
|
7737
8030
|
function killAllActiveProcesses() {
|
|
@@ -8373,17 +8666,14 @@ async function fetchAsideHistory(client, sessionID, question) {
|
|
|
8373
8666
|
return [];
|
|
8374
8667
|
}
|
|
8375
8668
|
}
|
|
8376
|
-
async function
|
|
8669
|
+
async function runBtwCommand(client, input, options = {}) {
|
|
8377
8670
|
const question = input.arguments.trim();
|
|
8378
|
-
if (!question)
|
|
8379
|
-
showToast(client, { title: "btw", message: SIDE_QUESTION_USAGE, variant: "warning", duration: 6e3 });
|
|
8380
|
-
throw new BtwHandledError("/btw needs a question.");
|
|
8381
|
-
}
|
|
8671
|
+
if (!question) return "bare";
|
|
8382
8672
|
const active = await waitForAsideProcess(client, input.sessionID, options);
|
|
8383
8673
|
const transport = active?.asideTransport;
|
|
8384
8674
|
if (!active || !transport) {
|
|
8385
8675
|
if (active) log.info("btw: process has no aside transport, leaving it to the turn", { sessionID: input.sessionID });
|
|
8386
|
-
return;
|
|
8676
|
+
return "release";
|
|
8387
8677
|
}
|
|
8388
8678
|
let busy = false;
|
|
8389
8679
|
let inlineDone = false;
|
|
@@ -8444,7 +8734,7 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
8444
8734
|
}
|
|
8445
8735
|
);
|
|
8446
8736
|
}
|
|
8447
|
-
if (!busy) return;
|
|
8737
|
+
if (!busy) return "release";
|
|
8448
8738
|
const started2 = Date.now();
|
|
8449
8739
|
const outcome = await Promise.race([
|
|
8450
8740
|
inlineDelivered,
|
|
@@ -8457,18 +8747,46 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
8457
8747
|
sessionID: input.sessionID,
|
|
8458
8748
|
waitedMs: Date.now() - started2
|
|
8459
8749
|
});
|
|
8460
|
-
|
|
8750
|
+
return "inline";
|
|
8461
8751
|
}
|
|
8462
8752
|
log.info("btw: turn over, releasing the /btw message", {
|
|
8463
8753
|
sessionID: input.sessionID,
|
|
8464
8754
|
idle: outcome === "idle",
|
|
8465
8755
|
waitedMs: Date.now() - started2
|
|
8466
8756
|
});
|
|
8757
|
+
return outcome === "timeout" ? "timeout" : "release";
|
|
8758
|
+
}
|
|
8759
|
+
async function handleBtwCommand(client, input, options = {}) {
|
|
8760
|
+
const outcome = await runBtwCommand(client, input, options);
|
|
8761
|
+
if (outcome === "bare") {
|
|
8762
|
+
showToast(client, { title: "btw", message: SIDE_QUESTION_USAGE, variant: "warning", duration: 6e3 });
|
|
8763
|
+
throw new BtwHandledError("/btw needs a question.");
|
|
8764
|
+
}
|
|
8765
|
+
if (outcome === "inline") throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
|
|
8467
8766
|
if (outcome === "timeout") {
|
|
8468
8767
|
showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
|
|
8469
8768
|
throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
|
|
8470
8769
|
}
|
|
8471
8770
|
}
|
|
8771
|
+
async function askBtwThenRelease(client, input, release2, options = {}) {
|
|
8772
|
+
let outcome;
|
|
8773
|
+
try {
|
|
8774
|
+
outcome = await runBtwCommand(client, input, options);
|
|
8775
|
+
} catch (error) {
|
|
8776
|
+
log.warn("btw: the early aside failed; sending the /btw message as it is", {
|
|
8777
|
+
sessionID: input.sessionID,
|
|
8778
|
+
error: errorText(error)
|
|
8779
|
+
});
|
|
8780
|
+
outcome = "failed";
|
|
8781
|
+
}
|
|
8782
|
+
if (outcome === "inline") return outcome;
|
|
8783
|
+
try {
|
|
8784
|
+
await release2();
|
|
8785
|
+
} catch (error) {
|
|
8786
|
+
log.warn("btw: could not send the /btw message", { sessionID: input.sessionID, error: errorText(error) });
|
|
8787
|
+
}
|
|
8788
|
+
return outcome;
|
|
8789
|
+
}
|
|
8472
8790
|
|
|
8473
8791
|
// src/budget-ledger.ts
|
|
8474
8792
|
var BUDGET_MARKER = "\u258C **budget:**";
|
|
@@ -8638,7 +8956,9 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8638
8956
|
"automatic account switch skipped an account whose runtime could not be prepared",
|
|
8639
8957
|
"background dispatch refused by maxBackgroundSubagents",
|
|
8640
8958
|
"btw: a turn is running but no claude process appeared for it",
|
|
8959
|
+
"btw: could not send the /btw message",
|
|
8641
8960
|
"btw: early aside failed; the message will ask again",
|
|
8961
|
+
"btw: the early aside failed; sending the /btw message as it is",
|
|
8642
8962
|
"budget option refused: not a positive number of dollars",
|
|
8643
8963
|
"budgetWarnAt refused: expected percentages between 0 and 100",
|
|
8644
8964
|
"cannot interrupt this transport; waiting for the turn to end",
|
|
@@ -8727,7 +9047,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8727
9047
|
"no configured account could take this turn; it ends here",
|
|
8728
9048
|
"no proxyTools entry was recognised; nothing will be proxied this turn",
|
|
8729
9049
|
"no stdout after envelope write; respawning claude process to resume conversation",
|
|
8730
|
-
"opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction",
|
|
9050
|
+
"opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction, and stopped turns keep their proxy calls until the next message",
|
|
8731
9051
|
"opencode's stored OAuth token for this MCP server has expired, so the bridged server carries no credential; re-authenticate it in opencode",
|
|
8732
9052
|
"pending proxy call arrived after stream close; rejecting",
|
|
8733
9053
|
'permissionMode "plan" is enforced: claude cannot edit files or run commands, and --dangerously-skip-permissions is deliberately not passed so it stays that way. Headless Claude Code is not offered an ExitPlanMode tool, so nothing releases plan mode mid-session; approving a plan in chat does not unlock writes. Leaving plan mode means changing the config and restarting opencode.',
|
|
@@ -8771,6 +9091,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
8771
9091
|
"subagent dispatch answer was not recognised; dispatching with the defaults",
|
|
8772
9092
|
"subagent dispatch answer was not recognised; keeping the default",
|
|
8773
9093
|
"subagent dispatch form was dismissed; dispatching with the defaults",
|
|
9094
|
+
"task model refused: its account is in another account group",
|
|
8774
9095
|
"task_batch resolving with child results missing",
|
|
8775
9096
|
"the interactive transport has not been measured on this Claude Code version; it reads the TUI's screen and transcript, which a new release can change. If a turn hangs or a dialog is not handled, run /claude-code-doctor bundle and report it",
|
|
8776
9097
|
"the interactive turn did not take a forwarded user message",
|
|
@@ -14865,7 +15186,7 @@ async function resolvePlanModeQuestion(config, compactionMode, loadLiveToolInfo)
|
|
|
14865
15186
|
}
|
|
14866
15187
|
return active;
|
|
14867
15188
|
}
|
|
14868
|
-
async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
|
|
15189
|
+
async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress, callerSessionId, sourceAccount) {
|
|
14869
15190
|
const timeoutOverrides = config.proxyToolTimeoutMs;
|
|
14870
15191
|
const interceptors = /* @__PURE__ */ new Map();
|
|
14871
15192
|
if (tools.some((t) => t.name === TASK_STATUS_TOOL_NAME)) {
|
|
@@ -14895,8 +15216,25 @@ async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCom
|
|
|
14895
15216
|
}
|
|
14896
15217
|
const cap = config.maxBackgroundSubagents;
|
|
14897
15218
|
const admit = cap !== void 0 && tools.some((t) => t.name === "task" || t.name === TASK_BATCH_TOOL_NAME) ? (toolName, input) => admitBackgroundDispatch(toolName, input, cap) : void 0;
|
|
14898
|
-
const
|
|
15219
|
+
const accountProviders = accountProviderMap(config.failoverAccounts);
|
|
15220
|
+
const groups = config.accountGroups ?? null;
|
|
15221
|
+
const caller = normalizeAccountName(sourceAccount ?? config.account ?? DEFAULT_ACCOUNT);
|
|
15222
|
+
const taskModelGuard = groups && accountProviders.size > 0 ? (model) => taskModelAccountRefusal({
|
|
15223
|
+
model,
|
|
15224
|
+
sourceAccount: caller,
|
|
15225
|
+
providers: accountProviders,
|
|
15226
|
+
groups,
|
|
15227
|
+
crossGroup: config.subagentDispatchCrossGroup === true
|
|
15228
|
+
}) : void 0;
|
|
15229
|
+
const srv = await createProxyMcpServer(tools, timeoutOverrides, interceptors, {
|
|
15230
|
+
admit,
|
|
15231
|
+
taskModelGuard
|
|
15232
|
+
});
|
|
14899
15233
|
srv.calls.on("call", (call) => {
|
|
15234
|
+
if (call.toolName === "task") noteExplicitTaskModels(callerSessionId, [call.input]);
|
|
15235
|
+
else if (call.toolName === TASK_BATCH_TOOL_NAME) {
|
|
15236
|
+
noteExplicitTaskModels(callerSessionId, taskBatchTasks(call.input));
|
|
15237
|
+
}
|
|
14900
15238
|
queuePendingProxyCall(sessionKeyForCalls, call, timeoutOverrides);
|
|
14901
15239
|
});
|
|
14902
15240
|
return srv;
|
|
@@ -16864,6 +17202,26 @@ var ClaudeCodeLanguageModel = class {
|
|
|
16864
17202
|
parentSessionId: await fetchSessionParentId(affinity)
|
|
16865
17203
|
});
|
|
16866
17204
|
}
|
|
17205
|
+
/**
|
|
17206
|
+
* The model the parent named for THIS session in its own `task` call, or
|
|
17207
|
+
* undefined (h #g254). The same two lookups as `resolveDispatchChoice`, and
|
|
17208
|
+
* the same reason for the order: a held record first, and the parent fetched
|
|
17209
|
+
* only for a child whose agent and first message match a recorded request.
|
|
17210
|
+
*/
|
|
17211
|
+
async resolveExplicitTaskModel(options, affinity) {
|
|
17212
|
+
if (affinity === "default") return void 0;
|
|
17213
|
+
const held = explicitTaskModelFor(affinity);
|
|
17214
|
+
if (held) return held;
|
|
17215
|
+
const agent = this.getOpencodeAgent(options);
|
|
17216
|
+
const prompt = firstUserText(options.prompt);
|
|
17217
|
+
if (!hasExplicitTaskModelCandidate(agent, prompt)) return void 0;
|
|
17218
|
+
return claimExplicitTaskModel({
|
|
17219
|
+
sessionId: affinity,
|
|
17220
|
+
agent,
|
|
17221
|
+
prompt,
|
|
17222
|
+
parentSessionId: await fetchSessionParentId(affinity)
|
|
17223
|
+
});
|
|
17224
|
+
}
|
|
16867
17225
|
/**
|
|
16868
17226
|
* Whether the ExitPlanMode approval bridge is live for this turn: the
|
|
16869
17227
|
* operator opted in AND opencode's registry actually has the `question`
|
|
@@ -17037,13 +17395,14 @@ var ClaudeCodeLanguageModel = class {
|
|
|
17037
17395
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
17038
17396
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
17039
17397
|
*/
|
|
17040
|
-
async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
|
|
17398
|
+
async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress, callerSessionId, sourceAccount) {
|
|
17041
17399
|
return ensureProxyServer(
|
|
17042
17400
|
this.config,
|
|
17043
17401
|
tools,
|
|
17044
17402
|
sessionKeyForCalls,
|
|
17045
17403
|
interceptCompress,
|
|
17046
|
-
callerSessionId
|
|
17404
|
+
callerSessionId,
|
|
17405
|
+
sourceAccount
|
|
17047
17406
|
);
|
|
17048
17407
|
}
|
|
17049
17408
|
/**
|
|
@@ -17064,12 +17423,6 @@ var ClaudeCodeLanguageModel = class {
|
|
|
17064
17423
|
this.config.provider
|
|
17065
17424
|
);
|
|
17066
17425
|
}
|
|
17067
|
-
controlRequestBehaviorForTool(toolName) {
|
|
17068
|
-
return controlRequestBehaviorForTool(this.config, toolName);
|
|
17069
|
-
}
|
|
17070
|
-
writeControlResponse(proc, requestId, response) {
|
|
17071
|
-
writeControlResponse(proc, requestId, response);
|
|
17072
|
-
}
|
|
17073
17426
|
/**
|
|
17074
17427
|
* Handle Claude stream-json control requests (`can_use_tool`, etc.) and
|
|
17075
17428
|
* respond via stdin with a matching `control_response`.
|
|
@@ -17118,9 +17471,6 @@ var ClaudeCodeLanguageModel = class {
|
|
|
17118
17471
|
thinkingDisplay: process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0 ? "summarized" : void 0
|
|
17119
17472
|
};
|
|
17120
17473
|
}
|
|
17121
|
-
latestUserText(prompt) {
|
|
17122
|
-
return latestUserText(prompt);
|
|
17123
|
-
}
|
|
17124
17474
|
synthesizeTitle(prompt) {
|
|
17125
17475
|
return synthesizeTitle(prompt);
|
|
17126
17476
|
}
|
|
@@ -17443,17 +17793,19 @@ var ClaudeCodeLanguageModel = class {
|
|
|
17443
17793
|
const affinity = this.sessionAffinity(options);
|
|
17444
17794
|
const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
|
|
17445
17795
|
const compactionMode = this.isCompactionCall(options);
|
|
17446
|
-
const
|
|
17796
|
+
const explicitTaskModel = compactionMode ? void 0 : await this.resolveExplicitTaskModel(options, affinity);
|
|
17797
|
+
const explicitModelApplies = explicitTaskModelApplies(explicitTaskModel, this.modelId);
|
|
17798
|
+
const dispatchChoice = compactionMode || explicitModelApplies ? void 0 : await this.resolveDispatchChoice(options, affinity);
|
|
17447
17799
|
const dispatchModelId = dispatchChoice?.model ? qualifyModelName(dispatchChoice.model, this.modelId) : null;
|
|
17448
|
-
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : attempt?.modelOverride ?? dispatchModelId ?? resolveAgentModel(
|
|
17800
|
+
const effectiveModelId = compactionMode ? this.resolveCompactionModel() : attempt?.modelOverride ?? dispatchModelId ?? (explicitModelApplies ? this.modelId : resolveAgentModel(
|
|
17449
17801
|
this.getOpencodeAgent(options),
|
|
17450
17802
|
this.modelId
|
|
17451
|
-
);
|
|
17803
|
+
));
|
|
17452
17804
|
if (attempt) attempt.modelId = effectiveModelId;
|
|
17453
|
-
const reasoningEffort = compactionMode ? void 0 : dispatchChoice?.effort ?? resolveAgentEffort(
|
|
17805
|
+
const reasoningEffort = compactionMode ? void 0 : dispatchChoice?.effort ?? (explicitModelApplies && explicitTaskModel?.variant ? this.getReasoningEffort(options.providerOptions) : resolveAgentEffort(
|
|
17454
17806
|
this.getOpencodeAgent(options),
|
|
17455
17807
|
this.getReasoningEffort(options.providerOptions)
|
|
17456
|
-
);
|
|
17808
|
+
));
|
|
17457
17809
|
const promptCacheTtl = compactionMode ? void 0 : resolveAgentCacheTtl(this.getOpencodeAgent(options));
|
|
17458
17810
|
const context = [
|
|
17459
17811
|
this.config.provider,
|
|
@@ -18483,6 +18835,16 @@ var ClaudeCodeLanguageModel = class {
|
|
|
18483
18835
|
backgroundSubagentsSupported,
|
|
18484
18836
|
hostDialect
|
|
18485
18837
|
);
|
|
18838
|
+
const taskModelSupported = liveTaskSupportsModel(
|
|
18839
|
+
liveToolInfo.taskParameters,
|
|
18840
|
+
hostDialect
|
|
18841
|
+
);
|
|
18842
|
+
enrichedProxy = applyTaskModelSupport(enrichedProxy, taskModelSupported);
|
|
18843
|
+
log.info("task model gate", {
|
|
18844
|
+
supported: taskModelSupported,
|
|
18845
|
+
registryResolved: liveToolInfo.resolved,
|
|
18846
|
+
hostApi: hostDialect
|
|
18847
|
+
});
|
|
18486
18848
|
recordBackgroundSubagentGate({
|
|
18487
18849
|
supported: backgroundSubagentsSupported,
|
|
18488
18850
|
hostApi: hostDialect,
|
|
@@ -18537,7 +18899,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
18537
18899
|
combinedProxyTools,
|
|
18538
18900
|
sk,
|
|
18539
18901
|
pluginCompressEnabled,
|
|
18540
|
-
affinity
|
|
18902
|
+
affinity,
|
|
18903
|
+
sourceAccount
|
|
18541
18904
|
);
|
|
18542
18905
|
}
|
|
18543
18906
|
const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
|
|
@@ -19295,38 +19658,40 @@ var ClaudeCodeLanguageModel = class {
|
|
|
19295
19658
|
dropped: unattended.dropped
|
|
19296
19659
|
});
|
|
19297
19660
|
let partialText = false;
|
|
19298
|
-
|
|
19661
|
+
const narration = [];
|
|
19662
|
+
for (const line of unattended.lines) {
|
|
19663
|
+
try {
|
|
19664
|
+
const outer = JSON.parse(line);
|
|
19665
|
+
const msg = outer.type === "stream_event" && outer.event ? outer.event : outer;
|
|
19666
|
+
let text = "";
|
|
19667
|
+
if (msg.type === "content_block_delta" && msg.delta?.type === "text_delta") {
|
|
19668
|
+
text = msg.delta.text ?? "";
|
|
19669
|
+
partialText = true;
|
|
19670
|
+
} else if (msg.type === "assistant") {
|
|
19671
|
+
if (!partialText) text = (msg.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
|
|
19672
|
+
partialText = false;
|
|
19673
|
+
} else if (msg.type === "result") {
|
|
19674
|
+
state.unattendedTurnEnded = true;
|
|
19675
|
+
for (const entry of state.activeProcess.pendingProxyCompletions?.values() ?? []) {
|
|
19676
|
+
if (isPendingProxyCallChannelClosed(entry.call)) entry.recoveryRequired = true;
|
|
19677
|
+
}
|
|
19678
|
+
if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
|
|
19679
|
+
if (msg.is_error && msg.result) text = msg.result;
|
|
19680
|
+
}
|
|
19681
|
+
if (text) narration.push(text);
|
|
19682
|
+
} catch {
|
|
19683
|
+
}
|
|
19684
|
+
}
|
|
19685
|
+
if (narration.length > 0 || unattended.dropped > 0) {
|
|
19299
19686
|
const replayId = state.startTextBlock();
|
|
19300
19687
|
controller.enqueue({
|
|
19301
19688
|
type: "text-delta",
|
|
19302
19689
|
id: replayId,
|
|
19303
19690
|
delta: formatUnattendedReplayNote(unattended.dropped)
|
|
19304
19691
|
});
|
|
19305
|
-
for (const
|
|
19306
|
-
|
|
19307
|
-
const outer = JSON.parse(line);
|
|
19308
|
-
const msg = outer.type === "stream_event" && outer.event ? outer.event : outer;
|
|
19309
|
-
let text = "";
|
|
19310
|
-
if (msg.type === "content_block_delta" && msg.delta?.type === "text_delta") {
|
|
19311
|
-
text = msg.delta.text ?? "";
|
|
19312
|
-
partialText = true;
|
|
19313
|
-
} else if (msg.type === "assistant") {
|
|
19314
|
-
if (!partialText) text = (msg.message?.content ?? []).filter((part) => part.type === "text").map((part) => part.text ?? "").join("");
|
|
19315
|
-
partialText = false;
|
|
19316
|
-
} else if (msg.type === "result") {
|
|
19317
|
-
state.unattendedTurnEnded = true;
|
|
19318
|
-
for (const entry of state.activeProcess.pendingProxyCompletions?.values() ?? []) {
|
|
19319
|
-
if (isPendingProxyCallChannelClosed(entry.call)) entry.recoveryRequired = true;
|
|
19320
|
-
}
|
|
19321
|
-
if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
|
|
19322
|
-
if (msg.is_error && msg.result) text = msg.result;
|
|
19323
|
-
}
|
|
19324
|
-
if (text) controller.enqueue({ type: "text-delta", id: state.currentTextId ?? state.startTextBlock(), delta: text });
|
|
19325
|
-
} catch {
|
|
19326
|
-
}
|
|
19327
|
-
}
|
|
19692
|
+
for (const text of narration) controller.enqueue({ type: "text-delta", id: replayId, delta: text });
|
|
19693
|
+
state.endTextBlock();
|
|
19328
19694
|
}
|
|
19329
|
-
state.endTextBlock();
|
|
19330
19695
|
clearFallbackTimer(state);
|
|
19331
19696
|
state.hasReceivedContent = false;
|
|
19332
19697
|
}
|
|
@@ -19754,11 +20119,42 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
19754
20119
|
}
|
|
19755
20120
|
}
|
|
19756
20121
|
|
|
19757
|
-
// src/
|
|
19758
|
-
var
|
|
20122
|
+
// src/plugin-notice.ts
|
|
20123
|
+
var NOTICE_RPC_ID = "opencode-claude-code-plugin";
|
|
20124
|
+
var NOTICE_EVENT = "notice";
|
|
20125
|
+
var NOTICE_EVENT_TYPE = `rpc.${NOTICE_RPC_ID}.${NOTICE_EVENT}`;
|
|
20126
|
+
var NOTICE_RPC_DEFINITION = {
|
|
20127
|
+
id: NOTICE_RPC_ID,
|
|
20128
|
+
methods: {},
|
|
20129
|
+
events: {
|
|
20130
|
+
[NOTICE_EVENT]: {
|
|
20131
|
+
schema: {
|
|
20132
|
+
type: "object",
|
|
20133
|
+
properties: {
|
|
20134
|
+
id: { type: "string" },
|
|
20135
|
+
level: { type: "string", enum: ["warn", "error"] },
|
|
20136
|
+
message: { type: "string" }
|
|
20137
|
+
},
|
|
20138
|
+
required: ["id", "level", "message"],
|
|
20139
|
+
additionalProperties: false
|
|
20140
|
+
}
|
|
20141
|
+
}
|
|
20142
|
+
}
|
|
20143
|
+
};
|
|
19759
20144
|
var TOAST_TITLE = "claude-code";
|
|
19760
20145
|
var TOAST_DURATION_MS = 1e4;
|
|
19761
20146
|
var MAX_TOAST_CHARS = 240;
|
|
20147
|
+
function toastMessage(message) {
|
|
20148
|
+
const flat = message.replace(/\s+/g, " ").trim();
|
|
20149
|
+
if (flat.length <= MAX_TOAST_CHARS) return flat;
|
|
20150
|
+
return `${flat.slice(0, MAX_TOAST_CHARS - 1)}\u2026`;
|
|
20151
|
+
}
|
|
20152
|
+
function pluginNotice(id, level, message) {
|
|
20153
|
+
return { id, level, message: toastMessage(message) };
|
|
20154
|
+
}
|
|
20155
|
+
|
|
20156
|
+
// src/tui-log-sink.ts
|
|
20157
|
+
var LOG_SERVICE_NAME = "opencode-claude-code";
|
|
19762
20158
|
var APP_LOG_LEVEL = {
|
|
19763
20159
|
debug: "debug",
|
|
19764
20160
|
info: "info",
|
|
@@ -19766,11 +20162,6 @@ var APP_LOG_LEVEL = {
|
|
|
19766
20162
|
warn: "warn",
|
|
19767
20163
|
error: "error"
|
|
19768
20164
|
};
|
|
19769
|
-
function toastMessage(message) {
|
|
19770
|
-
const flat = message.replace(/\s+/g, " ").trim();
|
|
19771
|
-
if (flat.length <= MAX_TOAST_CHARS) return flat;
|
|
19772
|
-
return `${flat.slice(0, MAX_TOAST_CHARS - 1)}\u2026`;
|
|
19773
|
-
}
|
|
19774
20165
|
function appLogBody(entry) {
|
|
19775
20166
|
return {
|
|
19776
20167
|
service: LOG_SERVICE_NAME,
|
|
@@ -19820,6 +20211,9 @@ function registerOpencodeLogSink(client) {
|
|
|
19820
20211
|
return true;
|
|
19821
20212
|
}
|
|
19822
20213
|
|
|
20214
|
+
// src/v2.ts
|
|
20215
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
20216
|
+
|
|
19823
20217
|
// src/v2-client.ts
|
|
19824
20218
|
function formatAgentTypeList(agents) {
|
|
19825
20219
|
const lines = agents.flatMap((agent) => {
|
|
@@ -20109,25 +20503,90 @@ function deletedSessionIdV2(event) {
|
|
|
20109
20503
|
const id = event.data?.sessionID;
|
|
20110
20504
|
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
20111
20505
|
}
|
|
20112
|
-
|
|
20506
|
+
function stoppedSessionIdV2(event) {
|
|
20507
|
+
if (!event || event.type !== "session.execution.interrupted") return void 0;
|
|
20508
|
+
if (event.data?.reason !== "user") return void 0;
|
|
20509
|
+
const id = event.data?.sessionID;
|
|
20510
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
20511
|
+
}
|
|
20512
|
+
async function followSessionEvents(subscribe, signal) {
|
|
20513
|
+
const closeFeed = openSessionRunEventFeed();
|
|
20113
20514
|
try {
|
|
20114
20515
|
for await (const event of subscribe({ signal })) {
|
|
20115
20516
|
if (signal.aborted) break;
|
|
20116
|
-
|
|
20117
|
-
|
|
20118
|
-
|
|
20119
|
-
|
|
20120
|
-
|
|
20517
|
+
noteSessionRunEvent(event);
|
|
20518
|
+
const deleted = deletedSessionIdV2(event);
|
|
20519
|
+
if (deleted) {
|
|
20520
|
+
const released = deleteActiveProcessesForSession(deleted);
|
|
20521
|
+
if (released.length > 0) {
|
|
20522
|
+
log.info("released claude state for deleted session", { sessionID: deleted, released });
|
|
20523
|
+
}
|
|
20524
|
+
continue;
|
|
20121
20525
|
}
|
|
20526
|
+
const stopped = stoppedSessionIdV2(event);
|
|
20527
|
+
if (stopped) releaseParkedProxyCallsForSession(stopped);
|
|
20122
20528
|
}
|
|
20123
20529
|
} catch (err) {
|
|
20124
20530
|
if (!signal.aborted) {
|
|
20125
|
-
log.warn(
|
|
20126
|
-
|
|
20127
|
-
|
|
20531
|
+
log.warn(
|
|
20532
|
+
"opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction, and stopped turns keep their proxy calls until the next message",
|
|
20533
|
+
{ error: err instanceof Error ? err.message : String(err) }
|
|
20534
|
+
);
|
|
20128
20535
|
}
|
|
20536
|
+
} finally {
|
|
20537
|
+
closeFeed();
|
|
20129
20538
|
}
|
|
20130
20539
|
}
|
|
20540
|
+
var noticeEmitters = /* @__PURE__ */ new Set();
|
|
20541
|
+
var NOTICE_PROCESS_TAG = randomUUID8().slice(0, 8);
|
|
20542
|
+
var noticeCounter = 0;
|
|
20543
|
+
function emitNotice(entry) {
|
|
20544
|
+
if (entry.level !== "warn" && entry.level !== "error") return;
|
|
20545
|
+
noticeCounter += 1;
|
|
20546
|
+
const notice = pluginNotice(`${NOTICE_PROCESS_TAG}-${noticeCounter}`, entry.level, entry.message);
|
|
20547
|
+
for (const emit2 of noticeEmitters) emit2(notice);
|
|
20548
|
+
}
|
|
20549
|
+
async function registerNoticeChannel(ctx) {
|
|
20550
|
+
const rpc = ctx.rpc;
|
|
20551
|
+
if (typeof rpc?.register !== "function") return void 0;
|
|
20552
|
+
let registration;
|
|
20553
|
+
try {
|
|
20554
|
+
registration = await rpc.register(NOTICE_RPC_DEFINITION, {});
|
|
20555
|
+
} catch (err) {
|
|
20556
|
+
log.debug("opencode 2 refused the notice channel; warnings stay in the plugin log", {
|
|
20557
|
+
error: err instanceof Error ? err.message : String(err)
|
|
20558
|
+
});
|
|
20559
|
+
return void 0;
|
|
20560
|
+
}
|
|
20561
|
+
const emitter2 = (notice) => {
|
|
20562
|
+
try {
|
|
20563
|
+
void Promise.resolve(registration.events.emit(NOTICE_EVENT, { ...notice })).catch(() => void 0);
|
|
20564
|
+
} catch {
|
|
20565
|
+
}
|
|
20566
|
+
};
|
|
20567
|
+
noticeEmitters.add(emitter2);
|
|
20568
|
+
setToastNotifier(emitNotice);
|
|
20569
|
+
return {
|
|
20570
|
+
dispose: async () => {
|
|
20571
|
+
noticeEmitters.delete(emitter2);
|
|
20572
|
+
if (noticeEmitters.size === 0) setToastNotifier(null);
|
|
20573
|
+
await registration.dispose();
|
|
20574
|
+
}
|
|
20575
|
+
};
|
|
20576
|
+
}
|
|
20577
|
+
function v2BtwClient(shim) {
|
|
20578
|
+
const shimSession = shim.session;
|
|
20579
|
+
const messages = shimSession?.messages;
|
|
20580
|
+
const status = async () => ({ data: busySessionsFromEvents() ?? {} });
|
|
20581
|
+
return {
|
|
20582
|
+
session: {
|
|
20583
|
+
...typeof messages === "function" ? { messages: (options) => messages.call(shimSession, options) } : {},
|
|
20584
|
+
get status() {
|
|
20585
|
+
return busySessionsFromEvents() === void 0 ? void 0 : status;
|
|
20586
|
+
}
|
|
20587
|
+
}
|
|
20588
|
+
};
|
|
20589
|
+
}
|
|
20131
20590
|
function createV2Setup(deps) {
|
|
20132
20591
|
return async (ctx) => {
|
|
20133
20592
|
if (!isOpencodeV2Context(ctx)) {
|
|
@@ -20137,9 +20596,13 @@ function createV2Setup(deps) {
|
|
|
20137
20596
|
return () => void 0;
|
|
20138
20597
|
}
|
|
20139
20598
|
ensureProcessExitCleanup();
|
|
20140
|
-
|
|
20599
|
+
const shim = createV1ClientShim(ctx);
|
|
20600
|
+
setOpencodeClient(shim);
|
|
20141
20601
|
const directory = ctx.location?.directory;
|
|
20142
20602
|
setOpencodeProjectDirectory(isUsableDirectory(directory) ? directory : void 0);
|
|
20603
|
+
const registrations = [];
|
|
20604
|
+
const noticeChannel = await registerNoticeChannel(ctx);
|
|
20605
|
+
if (noticeChannel) registrations.push(noticeChannel);
|
|
20143
20606
|
if (deps.buildAgentRegistry) {
|
|
20144
20607
|
try {
|
|
20145
20608
|
const config = deps.loadConfig(directory ?? process.cwd());
|
|
@@ -20159,7 +20622,6 @@ function createV2Setup(deps) {
|
|
|
20159
20622
|
}
|
|
20160
20623
|
}
|
|
20161
20624
|
const planned = /* @__PURE__ */ new Map();
|
|
20162
|
-
const registrations = [];
|
|
20163
20625
|
registrations.push(
|
|
20164
20626
|
await ctx.provider.transform((editor) => {
|
|
20165
20627
|
const seed = editor.get(BASE_PROVIDER_ID);
|
|
@@ -20215,6 +20677,7 @@ function createV2Setup(deps) {
|
|
|
20215
20677
|
})
|
|
20216
20678
|
);
|
|
20217
20679
|
const prompt = ctx.session.prompt;
|
|
20680
|
+
const btwClient = v2BtwClient(shim);
|
|
20218
20681
|
if (ctx.command?.transform && typeof prompt === "function") {
|
|
20219
20682
|
const send = (input, command, delivery) => prompt.call(ctx.session, {
|
|
20220
20683
|
...input.prompt,
|
|
@@ -20232,7 +20695,15 @@ function createV2Setup(deps) {
|
|
|
20232
20695
|
editor.add({
|
|
20233
20696
|
name: "btw",
|
|
20234
20697
|
description: BTW_COMMAND_DESCRIPTION,
|
|
20235
|
-
execute: (input) =>
|
|
20698
|
+
execute: (input) => {
|
|
20699
|
+
void askBtwThenRelease(
|
|
20700
|
+
btwClient,
|
|
20701
|
+
{ command: "btw", sessionID: input.sessionID, arguments: input.prompt.text ?? "" },
|
|
20702
|
+
() => send(input, "btw", "queue"),
|
|
20703
|
+
deps.btwWaitOptions
|
|
20704
|
+
);
|
|
20705
|
+
return Promise.resolve();
|
|
20706
|
+
}
|
|
20236
20707
|
});
|
|
20237
20708
|
})
|
|
20238
20709
|
);
|
|
@@ -20253,7 +20724,7 @@ function createV2Setup(deps) {
|
|
|
20253
20724
|
}
|
|
20254
20725
|
const events = new AbortController();
|
|
20255
20726
|
if (typeof ctx.event?.subscribe === "function") {
|
|
20256
|
-
void
|
|
20727
|
+
void followSessionEvents(ctx.event.subscribe.bind(ctx.event), events.signal);
|
|
20257
20728
|
}
|
|
20258
20729
|
log.info("claude-code plugin set up for opencode 2", {
|
|
20259
20730
|
opencode: ctx.app?.version,
|