@bojackduy/opencode-loopd 1.9.6 → 1.10.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/server.js +270 -18
- package/dist/tui.js +163 -1
- package/package.json +2 -1
package/dist/server.js
CHANGED
|
@@ -487,7 +487,7 @@ var CURRENT_VERSION = 6, LOCK_STALE_MS = 1e4;
|
|
|
487
487
|
var init_state_repository = () => {};
|
|
488
488
|
|
|
489
489
|
// src/server/plugin.ts
|
|
490
|
-
import {
|
|
490
|
+
import { tool as v1Tool } from "@opencode-ai/plugin/tool";
|
|
491
491
|
|
|
492
492
|
// src/application/control-worker.ts
|
|
493
493
|
init_state_repository();
|
|
@@ -1334,7 +1334,7 @@ function createLoopEngine(options) {
|
|
|
1334
1334
|
revision: blockedState.revision
|
|
1335
1335
|
});
|
|
1336
1336
|
if (shouldNotifyBlocked) {
|
|
1337
|
-
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" stopped: ${limitResult.reason} (child did not wrap up). Status: blocked. Last progress: ${goal.lastProgress?.summary || "none"}
|
|
1337
|
+
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" stopped: ${limitResult.reason} (child did not wrap up). Status: blocked. Last progress: ${goal.lastProgress?.summary || "none"}.`, goal.parentAgent);
|
|
1338
1338
|
}
|
|
1339
1339
|
return true;
|
|
1340
1340
|
}
|
|
@@ -1473,7 +1473,7 @@ function createLoopEngine(options) {
|
|
|
1473
1473
|
revision: newState.revision
|
|
1474
1474
|
});
|
|
1475
1475
|
if (shouldNotify) {
|
|
1476
|
-
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" blocked after ${runtime.consecutiveFailures + 1} failures. Last error: ${message}
|
|
1476
|
+
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" blocked after ${runtime.consecutiveFailures + 1} failures. Last error: ${message}.`, goal.parentAgent);
|
|
1477
1477
|
}
|
|
1478
1478
|
}
|
|
1479
1479
|
return true;
|
|
@@ -1635,7 +1635,7 @@ function createLoopEngine(options) {
|
|
|
1635
1635
|
costUsed: stoppedGoal.costUsed
|
|
1636
1636
|
});
|
|
1637
1637
|
if (shouldNotify) {
|
|
1638
|
-
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" stopped: ${reason}. Worker aborted, status: budget_limited. Resume with resume_goal to continue spending
|
|
1638
|
+
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" stopped: ${reason}. Worker aborted, status: budget_limited. Resume with resume_goal to continue spending.`, goal.parentAgent);
|
|
1639
1639
|
}
|
|
1640
1640
|
return true;
|
|
1641
1641
|
}
|
|
@@ -1745,7 +1745,7 @@ function createLoopEngine(options) {
|
|
|
1745
1745
|
workerSessionID: goal.workerSessionID,
|
|
1746
1746
|
count: unknownRuntime?.unknownStatusCount
|
|
1747
1747
|
});
|
|
1748
|
-
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" worker is unreachable after ${unknownRuntime?.unknownStatusCount ?? unknownStatusThreshold} status checks. The goal remains active; use inspect_background_goal, nudge_goal, pause_goal, or resume_goal to recover it
|
|
1748
|
+
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" worker is unreachable after ${unknownRuntime?.unknownStatusCount ?? unknownStatusThreshold} status checks. The goal remains active; use inspect_background_goal, nudge_goal, pause_goal, or resume_goal to recover it.`, goal.parentAgent);
|
|
1749
1749
|
}
|
|
1750
1750
|
continue;
|
|
1751
1751
|
}
|
|
@@ -1840,7 +1840,7 @@ function createLoopEngine(options) {
|
|
|
1840
1840
|
stuckSeconds
|
|
1841
1841
|
});
|
|
1842
1842
|
const quietMinutes = Math.max(1, Math.floor(stuckSeconds / 60));
|
|
1843
|
-
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" worker is idle but its turn will not confirm (unconfirmed for ${quietMinutes}m, no activity). The goal remains active; use inspect_background_goal to look, nudge_goal to re-prompt, or pause_goal to stop it
|
|
1843
|
+
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" worker is idle but its turn will not confirm (unconfirmed for ${quietMinutes}m, no activity). The goal remains active; use inspect_background_goal to look, nudge_goal to re-prompt, or pause_goal to stop it.`, goal.parentAgent);
|
|
1844
1844
|
continue;
|
|
1845
1845
|
}
|
|
1846
1846
|
if (status === "idle") {
|
|
@@ -1879,7 +1879,7 @@ function createLoopEngine(options) {
|
|
|
1879
1879
|
runID: runtime.activeRunID,
|
|
1880
1880
|
stuckSeconds
|
|
1881
1881
|
});
|
|
1882
|
-
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" worker may be stuck: no activity for ${Math.floor(stuckSeconds / 60)}m while reporting ${status}, lease expired. The goal remains active; use inspect_background_goal to look, nudge_goal to re-prompt, or pause_goal to stop it
|
|
1882
|
+
await host.notifyOwner(goal.ownerSessionID, `Loop goal "${goal.name}" worker may be stuck: no activity for ${Math.floor(stuckSeconds / 60)}m while reporting ${status}, lease expired. The goal remains active; use inspect_background_goal to look, nudge_goal to re-prompt, or pause_goal to stop it.`, goal.parentAgent);
|
|
1883
1883
|
}
|
|
1884
1884
|
}
|
|
1885
1885
|
}
|
|
@@ -2070,15 +2070,27 @@ function createRealHost(client, directory) {
|
|
|
2070
2070
|
await client.session.compact({ sessionID });
|
|
2071
2071
|
} catch {}
|
|
2072
2072
|
},
|
|
2073
|
-
async notifyOwner(ownerSessionID, message) {
|
|
2073
|
+
async notifyOwner(ownerSessionID, message, agent) {
|
|
2074
2074
|
if (shouldDedupParentNotify(ownerSessionID, message)) {
|
|
2075
2075
|
await logServerEvent(directory, "parent.notify.deduped", { ownerSessionID, preview: message.slice(0, 160) });
|
|
2076
2076
|
return;
|
|
2077
2077
|
}
|
|
2078
|
+
let resolvedAgent = agent?.trim() || undefined;
|
|
2079
|
+
if (!resolvedAgent) {
|
|
2080
|
+
try {
|
|
2081
|
+
const sess = await client.session.get({ path: { id: ownerSessionID } });
|
|
2082
|
+
const liveAgent = sess?.data?.agent;
|
|
2083
|
+
if (typeof liveAgent === "string" && liveAgent.trim())
|
|
2084
|
+
resolvedAgent = liveAgent.trim();
|
|
2085
|
+
} catch {}
|
|
2086
|
+
}
|
|
2078
2087
|
try {
|
|
2088
|
+
const body = { parts: [{ type: "text", text: message }] };
|
|
2089
|
+
if (resolvedAgent)
|
|
2090
|
+
body.agent = resolvedAgent;
|
|
2079
2091
|
const result = await withTimeout(client.session.promptAsync({
|
|
2080
2092
|
path: { id: ownerSessionID },
|
|
2081
|
-
body
|
|
2093
|
+
body
|
|
2082
2094
|
}), 1e4, "OpenCode parent notify");
|
|
2083
2095
|
if (result?.error) {
|
|
2084
2096
|
await logServerEvent(directory, "parent.notify.failed", { ownerSessionID, detail: describeError(result.error) });
|
|
@@ -2091,6 +2103,115 @@ function createRealHost(client, directory) {
|
|
|
2091
2103
|
}
|
|
2092
2104
|
};
|
|
2093
2105
|
}
|
|
2106
|
+
function createV2Host(context, statuses) {
|
|
2107
|
+
const directory = context.location.directory;
|
|
2108
|
+
return {
|
|
2109
|
+
async createWorker({ parentID, title, agent, model }) {
|
|
2110
|
+
const session = await context.session.create({
|
|
2111
|
+
title,
|
|
2112
|
+
agent,
|
|
2113
|
+
model: model ? { id: model.modelID, providerID: model.providerID } : undefined,
|
|
2114
|
+
location: { directory },
|
|
2115
|
+
metadata: { "loopd.parentID": parentID }
|
|
2116
|
+
});
|
|
2117
|
+
statuses.set(session.id, "idle");
|
|
2118
|
+
await logServerEvent(directory, "worker.created", { parentID, workerSessionID: session.id, title });
|
|
2119
|
+
return session.id;
|
|
2120
|
+
},
|
|
2121
|
+
async promptWorker({ sessionID, prompt, messageID, model, agent }) {
|
|
2122
|
+
if (agent)
|
|
2123
|
+
await context.session.switchAgent({ sessionID, agent });
|
|
2124
|
+
if (model) {
|
|
2125
|
+
await context.session.switchModel({
|
|
2126
|
+
sessionID,
|
|
2127
|
+
model: { id: model.modelID, providerID: model.providerID }
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
const result = await context.session.prompt({
|
|
2131
|
+
sessionID,
|
|
2132
|
+
id: messageID,
|
|
2133
|
+
text: prompt
|
|
2134
|
+
});
|
|
2135
|
+
statuses.set(sessionID, "busy");
|
|
2136
|
+
await logServerEvent(directory, "worker.prompted", { sessionID });
|
|
2137
|
+
return { messageID: result.id };
|
|
2138
|
+
},
|
|
2139
|
+
async readSession(sessionID) {
|
|
2140
|
+
try {
|
|
2141
|
+
const session = await context.session.get({ sessionID });
|
|
2142
|
+
const model = session.model ? { providerID: session.model.providerID, modelID: session.model.id } : undefined;
|
|
2143
|
+
if (!session.agent && !model)
|
|
2144
|
+
return;
|
|
2145
|
+
return { agent: session.agent, model };
|
|
2146
|
+
} catch {
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
},
|
|
2150
|
+
async sessionStatus(sessionID) {
|
|
2151
|
+
return statuses.get(sessionID) ?? "unknown";
|
|
2152
|
+
},
|
|
2153
|
+
async abortSession(sessionID) {
|
|
2154
|
+
try {
|
|
2155
|
+
await context.session.interrupt({ sessionID });
|
|
2156
|
+
statuses.set(sessionID, "idle");
|
|
2157
|
+
} catch {}
|
|
2158
|
+
},
|
|
2159
|
+
async readMessages(sessionID, limit = 10) {
|
|
2160
|
+
try {
|
|
2161
|
+
const messages = await context.session.context({ sessionID });
|
|
2162
|
+
let parentMessageID;
|
|
2163
|
+
return messages.flatMap((message) => {
|
|
2164
|
+
if (message.type === "user") {
|
|
2165
|
+
parentMessageID = message.id;
|
|
2166
|
+
return [{
|
|
2167
|
+
role: "user",
|
|
2168
|
+
content: message.text,
|
|
2169
|
+
timestamp: new Date(message.time.created).toISOString(),
|
|
2170
|
+
messageID: message.id
|
|
2171
|
+
}];
|
|
2172
|
+
}
|
|
2173
|
+
if (message.type !== "assistant")
|
|
2174
|
+
return [];
|
|
2175
|
+
const created = message.time.created;
|
|
2176
|
+
const completed = message.time.completed;
|
|
2177
|
+
return [{
|
|
2178
|
+
role: "assistant",
|
|
2179
|
+
content: message.content.filter((part) => part.type === "text").map((part) => part.text).join(`
|
|
2180
|
+
`),
|
|
2181
|
+
timestamp: new Date(completed ?? created).toISOString(),
|
|
2182
|
+
messageID: message.id,
|
|
2183
|
+
parentMessageID,
|
|
2184
|
+
completedAt: completed ? new Date(completed).toISOString() : undefined,
|
|
2185
|
+
tokens: message.tokens ? {
|
|
2186
|
+
input: message.tokens.input,
|
|
2187
|
+
output: message.tokens.output,
|
|
2188
|
+
reasoning: message.tokens.reasoning,
|
|
2189
|
+
cacheRead: message.tokens.cache.read,
|
|
2190
|
+
cacheWrite: message.tokens.cache.write
|
|
2191
|
+
} : undefined,
|
|
2192
|
+
cost: message.cost,
|
|
2193
|
+
durationMs: completed && completed >= created ? completed - created : undefined
|
|
2194
|
+
}];
|
|
2195
|
+
}).slice(-limit);
|
|
2196
|
+
} catch {
|
|
2197
|
+
return [];
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
async compactSession(sessionID) {
|
|
2201
|
+
try {
|
|
2202
|
+
await context.session.command({ sessionID, name: "compact", text: "" });
|
|
2203
|
+
} catch {}
|
|
2204
|
+
},
|
|
2205
|
+
async notifyOwner(ownerSessionID, message) {
|
|
2206
|
+
try {
|
|
2207
|
+
await context.session.prompt({ sessionID: ownerSessionID, text: message });
|
|
2208
|
+
await logServerEvent(directory, "parent.notified", { ownerSessionID, preview: message.slice(0, 160) });
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
await logServerEvent(directory, "parent.notify.failed", { ownerSessionID, detail: describeError(error) });
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2094
2215
|
async function withTimeout(promise, timeoutMs, operation) {
|
|
2095
2216
|
let timer;
|
|
2096
2217
|
try {
|
|
@@ -2361,6 +2482,17 @@ function createGoalService(host) {
|
|
|
2361
2482
|
return withGoalOperation(id, () => startUnlocked(directory, input, id));
|
|
2362
2483
|
}
|
|
2363
2484
|
async function startUnlocked(directory, input, id) {
|
|
2485
|
+
let parentAgent = input.parentAgent;
|
|
2486
|
+
let parentModel = input.parentModel;
|
|
2487
|
+
if ((!parentAgent || !parentModel) && host.readSession) {
|
|
2488
|
+
try {
|
|
2489
|
+
const identity = await host.readSession(input.ownerSessionID);
|
|
2490
|
+
if (!parentAgent && identity?.agent)
|
|
2491
|
+
parentAgent = identity.agent;
|
|
2492
|
+
if (!parentModel && identity?.model)
|
|
2493
|
+
parentModel = `${identity.model.providerID}/${identity.model.modelID}`;
|
|
2494
|
+
} catch {}
|
|
2495
|
+
}
|
|
2364
2496
|
const goal = createGoal({
|
|
2365
2497
|
id,
|
|
2366
2498
|
name: input.name,
|
|
@@ -2375,10 +2507,10 @@ function createGoalService(host) {
|
|
|
2375
2507
|
});
|
|
2376
2508
|
if (typeof input.costBudget === "number")
|
|
2377
2509
|
goal.costBudget = input.costBudget;
|
|
2378
|
-
if (
|
|
2379
|
-
goal.parentAgent =
|
|
2380
|
-
if (
|
|
2381
|
-
goal.parentModel =
|
|
2510
|
+
if (parentAgent)
|
|
2511
|
+
goal.parentAgent = parentAgent;
|
|
2512
|
+
if (parentModel)
|
|
2513
|
+
goal.parentModel = parentModel;
|
|
2382
2514
|
const artifactDir = goalArtifactDir(directory, id);
|
|
2383
2515
|
goal.config.artifactDir = artifactDir;
|
|
2384
2516
|
if (!goal.config.progressFile)
|
|
@@ -4314,6 +4446,9 @@ var PLUGIN_ID = "opencode-loopd.server";
|
|
|
4314
4446
|
var server = async ({ client, directory }, pluginOptions) => {
|
|
4315
4447
|
const defaults = parsePluginDefaults(pluginOptions);
|
|
4316
4448
|
const host = createRealHost(client, directory);
|
|
4449
|
+
return createServerHooks(directory, host, defaults);
|
|
4450
|
+
};
|
|
4451
|
+
function createServerHooks(directory, host, defaults) {
|
|
4317
4452
|
const goalService = createGoalService(host);
|
|
4318
4453
|
const worker = createControlWorker({
|
|
4319
4454
|
directory,
|
|
@@ -4432,7 +4567,7 @@ var server = async ({ client, directory }, pluginOptions) => {
|
|
|
4432
4567
|
});
|
|
4433
4568
|
}
|
|
4434
4569
|
const message = parsed.status === "complete" ? `Loop goal "${goal.name}" completed: ${parsed.summary || ""}. Evidence: ${parsed.evidence || ""}. Artifacts: ${goal.config.artifactDir || "n/a"}.` : `Loop goal "${goal.name}" blocked: ${parsed.reason || ""}. Needed: ${parsed.needed || ""}.`;
|
|
4435
|
-
await host.notifyOwner(goal.ownerSessionID, message);
|
|
4570
|
+
await host.notifyOwner(goal.ownerSessionID, message, goal.parentAgent);
|
|
4436
4571
|
} catch {}
|
|
4437
4572
|
}
|
|
4438
4573
|
},
|
|
@@ -4442,7 +4577,7 @@ var server = async ({ client, directory }, pluginOptions) => {
|
|
|
4442
4577
|
scheduleWorker.stop();
|
|
4443
4578
|
}
|
|
4444
4579
|
};
|
|
4445
|
-
}
|
|
4580
|
+
}
|
|
4446
4581
|
function parsePluginDefaults(options) {
|
|
4447
4582
|
const agent = typeof options?.defaultAgent === "string" ? options.defaultAgent.trim() : "";
|
|
4448
4583
|
const model = typeof options?.defaultModel === "string" ? options.defaultModel.trim() : "";
|
|
@@ -4453,10 +4588,127 @@ function parsePluginDefaults(options) {
|
|
|
4453
4588
|
defaultChecks: checks.length > 0 ? checks : undefined
|
|
4454
4589
|
};
|
|
4455
4590
|
}
|
|
4456
|
-
var v2 =
|
|
4591
|
+
var v2 = {
|
|
4457
4592
|
id: PLUGIN_ID,
|
|
4458
|
-
async setup() {
|
|
4459
|
-
|
|
4593
|
+
async setup(context) {
|
|
4594
|
+
const directory = context.location.directory;
|
|
4595
|
+
const statuses = new Map;
|
|
4596
|
+
const host = createV2Host(context, statuses);
|
|
4597
|
+
const hooks = createServerHooks(directory, host, parsePluginDefaults(context.options));
|
|
4598
|
+
const registrations = [];
|
|
4599
|
+
const eventController = new AbortController;
|
|
4600
|
+
let eventTask = Promise.resolve();
|
|
4601
|
+
let disposed = false;
|
|
4602
|
+
const cleanup = async () => {
|
|
4603
|
+
if (disposed)
|
|
4604
|
+
return;
|
|
4605
|
+
disposed = true;
|
|
4606
|
+
eventController.abort();
|
|
4607
|
+
await eventTask;
|
|
4608
|
+
try {
|
|
4609
|
+
await Promise.allSettled(registrations.reverse().map((registration) => registration.dispose()));
|
|
4610
|
+
} finally {
|
|
4611
|
+
await hooks.dispose?.();
|
|
4612
|
+
}
|
|
4613
|
+
};
|
|
4614
|
+
try {
|
|
4615
|
+
registrations.push(await context.tool.transform((editor) => {
|
|
4616
|
+
for (const [id, definition] of Object.entries(hooks.tool ?? {})) {
|
|
4617
|
+
editor.add(toV2Tool(id, definition, directory));
|
|
4618
|
+
}
|
|
4619
|
+
}));
|
|
4620
|
+
registrations.push(await context.tool.hook("execute.before", async (input) => {
|
|
4621
|
+
await hooks["tool.execute.before"]?.({
|
|
4622
|
+
tool: input.tool,
|
|
4623
|
+
sessionID: input.sessionID,
|
|
4624
|
+
callID: input.id
|
|
4625
|
+
}, { args: input.input });
|
|
4626
|
+
}));
|
|
4627
|
+
registrations.push(await context.tool.hook("execute.after", async (input) => {
|
|
4628
|
+
const output = input.status === "completed" ? input.result : { content: JSON.stringify(input.error) };
|
|
4629
|
+
await hooks["tool.execute.after"]?.({
|
|
4630
|
+
tool: input.tool,
|
|
4631
|
+
sessionID: input.sessionID,
|
|
4632
|
+
callID: input.id,
|
|
4633
|
+
args: input.input
|
|
4634
|
+
}, {
|
|
4635
|
+
title: "",
|
|
4636
|
+
output: typeof output.content === "string" ? output.content : JSON.stringify(output.content ?? ""),
|
|
4637
|
+
metadata: output.metadata ?? {}
|
|
4638
|
+
});
|
|
4639
|
+
}));
|
|
4640
|
+
eventTask = consumeV2Events(context, eventController.signal, statuses, hooks).catch(async (error) => {
|
|
4641
|
+
if (!eventController.signal.aborted) {
|
|
4642
|
+
await logServerEvent(directory, "events.failed", { detail: describeError(error) });
|
|
4643
|
+
}
|
|
4644
|
+
});
|
|
4645
|
+
return cleanup;
|
|
4646
|
+
} catch (error) {
|
|
4647
|
+
await cleanup();
|
|
4648
|
+
throw error;
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
};
|
|
4652
|
+
function toV2Tool(id, definition, directory) {
|
|
4653
|
+
return {
|
|
4654
|
+
name: id,
|
|
4655
|
+
description: definition.description,
|
|
4656
|
+
input: v1Tool.schema.object(definition.args),
|
|
4657
|
+
async execute(input, context) {
|
|
4658
|
+
const result = await definition.execute(input, {
|
|
4659
|
+
sessionID: context.sessionID,
|
|
4660
|
+
agent: context.agent,
|
|
4661
|
+
messageID: context.messageID,
|
|
4662
|
+
directory,
|
|
4663
|
+
worktree: directory,
|
|
4664
|
+
abort: new AbortController().signal,
|
|
4665
|
+
metadata() {},
|
|
4666
|
+
async ask() {}
|
|
4667
|
+
});
|
|
4668
|
+
if (typeof result === "string")
|
|
4669
|
+
return { content: result };
|
|
4670
|
+
return {
|
|
4671
|
+
content: result.output,
|
|
4672
|
+
metadata: {
|
|
4673
|
+
...result.metadata,
|
|
4674
|
+
...result.title ? { title: result.title } : {}
|
|
4675
|
+
}
|
|
4676
|
+
};
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
}
|
|
4680
|
+
async function consumeV2Events(context, signal, statuses, hooks) {
|
|
4681
|
+
for await (const event of context.event.subscribe({ signal })) {
|
|
4682
|
+
const data = "data" in event && event.data && typeof event.data === "object" ? event.data : {};
|
|
4683
|
+
const sessionID = typeof data.sessionID === "string" ? data.sessionID : undefined;
|
|
4684
|
+
if (sessionID) {
|
|
4685
|
+
if (event.type === "session.status")
|
|
4686
|
+
statuses.set(sessionID, data.status?.type ?? "unknown");
|
|
4687
|
+
else if (event.type === "session.idle")
|
|
4688
|
+
statuses.set(sessionID, "idle");
|
|
4689
|
+
else if (event.type === "session.execution.started")
|
|
4690
|
+
statuses.set(sessionID, "busy");
|
|
4691
|
+
}
|
|
4692
|
+
await hooks.event?.({ event: normalizeV2Event(event) });
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
function normalizeV2Event(event) {
|
|
4696
|
+
const properties = event?.data && typeof event.data === "object" ? event.data : {};
|
|
4697
|
+
if (event?.type === "session.compaction.ended")
|
|
4698
|
+
return { type: "session.compacted", properties };
|
|
4699
|
+
if (event?.type === "session.execution.failed")
|
|
4700
|
+
return { type: "session.error", properties };
|
|
4701
|
+
if (event?.type === "session.message.content.updated") {
|
|
4702
|
+
return {
|
|
4703
|
+
type: "message.part.updated",
|
|
4704
|
+
properties: {
|
|
4705
|
+
sessionID: properties.sessionID,
|
|
4706
|
+
part: { messageID: properties.messageID }
|
|
4707
|
+
}
|
|
4708
|
+
};
|
|
4709
|
+
}
|
|
4710
|
+
return { type: event?.type, properties };
|
|
4711
|
+
}
|
|
4460
4712
|
var plugin_default = {
|
|
4461
4713
|
id: PLUGIN_ID,
|
|
4462
4714
|
server,
|
package/dist/tui.js
CHANGED
|
@@ -2373,9 +2373,171 @@ var tui = async (api) => {
|
|
|
2373
2373
|
});
|
|
2374
2374
|
api.lifecycle.onDispose(() => {});
|
|
2375
2375
|
};
|
|
2376
|
+
function adaptThemeV2(theme) {
|
|
2377
|
+
const t = theme ?? {};
|
|
2378
|
+
const text = t.text ?? {};
|
|
2379
|
+
const fb = text.feedback ?? {};
|
|
2380
|
+
const bg = t.background ?? {};
|
|
2381
|
+
const surface = bg.surface ?? {};
|
|
2382
|
+
const diff = t.diff ?? {};
|
|
2383
|
+
const diffText = diff.text ?? {};
|
|
2384
|
+
const diffBg = diff.background ?? {};
|
|
2385
|
+
const diffHi = diff.highlight ?? {};
|
|
2386
|
+
const diffLn = diff.lineNumber ?? {};
|
|
2387
|
+
const syntax = t.syntax ?? {};
|
|
2388
|
+
const md = t.markdown ?? {};
|
|
2389
|
+
const dv = (v, fallback) => typeof v === "string" ? v : fallback;
|
|
2390
|
+
const base = dv(text.default, "#ffffff");
|
|
2391
|
+
const muted = dv(text.subdued, "#888888");
|
|
2392
|
+
return {
|
|
2393
|
+
text: base,
|
|
2394
|
+
textMuted: muted,
|
|
2395
|
+
primary: base,
|
|
2396
|
+
accent: base,
|
|
2397
|
+
success: dv(fb.success?.default, "#22c55e"),
|
|
2398
|
+
warning: dv(fb.warning?.default, "#eab308"),
|
|
2399
|
+
error: dv(fb.error?.default, "#ef4444"),
|
|
2400
|
+
info: dv(fb.info?.default, base),
|
|
2401
|
+
background: dv(bg.default, "#000000"),
|
|
2402
|
+
backgroundPanel: dv(surface.overlay, dv(bg.default, "#000000")),
|
|
2403
|
+
backgroundElement: dv(surface.offset, dv(bg.default, "#000000")),
|
|
2404
|
+
diffAdded: dv(diffText.added, base),
|
|
2405
|
+
diffRemoved: dv(diffText.removed, base),
|
|
2406
|
+
diffContext: dv(diffText.context, muted),
|
|
2407
|
+
diffAddedBg: dv(diffBg.added, dv(bg.default, "#000000")),
|
|
2408
|
+
diffRemovedBg: dv(diffBg.removed, dv(bg.default, "#000000")),
|
|
2409
|
+
diffContextBg: dv(diffBg.context, dv(bg.default, "#000000")),
|
|
2410
|
+
diffHighlightAdded: dv(diffHi.added, base),
|
|
2411
|
+
diffHighlightRemoved: dv(diffHi.removed, base),
|
|
2412
|
+
diffLineNumber: dv(diffLn.text, muted),
|
|
2413
|
+
diffAddedLineNumberBg: dv(diffLn.background?.added, dv(bg.default, "#000000")),
|
|
2414
|
+
diffRemovedLineNumberBg: dv(diffLn.background?.removed, dv(bg.default, "#000000")),
|
|
2415
|
+
syntaxComment: dv(syntax.comment, muted),
|
|
2416
|
+
syntaxKeyword: dv(syntax.keyword, base),
|
|
2417
|
+
syntaxFunction: dv(syntax.function, base),
|
|
2418
|
+
syntaxVariable: dv(syntax.variable, base),
|
|
2419
|
+
syntaxString: dv(syntax.string, base),
|
|
2420
|
+
syntaxNumber: dv(syntax.number, base),
|
|
2421
|
+
syntaxType: dv(syntax.type, base),
|
|
2422
|
+
syntaxOperator: dv(syntax.operator, base),
|
|
2423
|
+
syntaxPunctuation: dv(syntax.punctuation, muted),
|
|
2424
|
+
markdownText: dv(md.text, base),
|
|
2425
|
+
markdownHeading: dv(md.heading, base),
|
|
2426
|
+
markdownLink: dv(md.link, base),
|
|
2427
|
+
markdownLinkText: dv(md.linkText, base),
|
|
2428
|
+
markdownCode: dv(md.code, base),
|
|
2429
|
+
markdownBlockQuote: dv(md.blockQuote, muted),
|
|
2430
|
+
markdownEmph: dv(md.emphasis, base),
|
|
2431
|
+
markdownStrong: dv(md.strong, base),
|
|
2432
|
+
markdownListItem: dv(md.listItem, base)
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
var v2setup = (ctx) => {
|
|
2436
|
+
const directory = ctx.location?.directory ?? ctx.data.location.default().directory;
|
|
2437
|
+
let dialogOpen = false;
|
|
2438
|
+
const closeDialog = () => {
|
|
2439
|
+
dialogOpen = false;
|
|
2440
|
+
ctx.ui.dialog.clear();
|
|
2441
|
+
};
|
|
2442
|
+
const facade = {
|
|
2443
|
+
theme: {
|
|
2444
|
+
get current() {
|
|
2445
|
+
return adaptThemeV2(ctx.theme);
|
|
2446
|
+
}
|
|
2447
|
+
},
|
|
2448
|
+
mode: {
|
|
2449
|
+
push: (name) => ctx.keymap.mode.push(name)
|
|
2450
|
+
},
|
|
2451
|
+
renderer: ctx.renderer,
|
|
2452
|
+
client: ctx.client,
|
|
2453
|
+
event: {
|
|
2454
|
+
on: (name, callback) => {
|
|
2455
|
+
if (name === "session.idle")
|
|
2456
|
+
return ctx.data.on("session.idle", callback);
|
|
2457
|
+
if (name === "session.status") {
|
|
2458
|
+
const unsubs = [ctx.data.on("session.execution.started", callback), ctx.data.on("session.execution.succeeded", callback)];
|
|
2459
|
+
return () => void unsubs.forEach((un) => un());
|
|
2460
|
+
}
|
|
2461
|
+
if (name === "session.error")
|
|
2462
|
+
return ctx.data.on("session.execution.failed", callback);
|
|
2463
|
+
if (name === "session.compacted")
|
|
2464
|
+
return ctx.data.on("session.compaction.ended", callback);
|
|
2465
|
+
return ctx.data.on(name, callback);
|
|
2466
|
+
}
|
|
2467
|
+
},
|
|
2468
|
+
ui: {
|
|
2469
|
+
dialog: {
|
|
2470
|
+
clear: closeDialog,
|
|
2471
|
+
get open() {
|
|
2472
|
+
return dialogOpen;
|
|
2473
|
+
},
|
|
2474
|
+
replace: (render) => {
|
|
2475
|
+
dialogOpen = true;
|
|
2476
|
+
ctx.ui.dialog.show(render);
|
|
2477
|
+
},
|
|
2478
|
+
setSize: (_size) => ctx.ui.dialog.set({
|
|
2479
|
+
size: "xlarge"
|
|
2480
|
+
})
|
|
2481
|
+
}
|
|
2482
|
+
},
|
|
2483
|
+
route: {
|
|
2484
|
+
navigate: (name, params) => {
|
|
2485
|
+
if (name === "session")
|
|
2486
|
+
ctx.ui.router.navigate({
|
|
2487
|
+
type: "session",
|
|
2488
|
+
sessionID: params?.sessionID
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
const open = () => {
|
|
2494
|
+
const previousFocus = ctx.renderer.currentFocusedRenderable;
|
|
2495
|
+
dialogOpen = true;
|
|
2496
|
+
ctx.ui.dialog.show(() => _$createComponent2(LoopDashboard, {
|
|
2497
|
+
api: facade,
|
|
2498
|
+
directory
|
|
2499
|
+
}), () => {
|
|
2500
|
+
dialogOpen = false;
|
|
2501
|
+
});
|
|
2502
|
+
ctx.ui.dialog.set({
|
|
2503
|
+
size: "xlarge"
|
|
2504
|
+
});
|
|
2505
|
+
previousFocus?.blur();
|
|
2506
|
+
};
|
|
2507
|
+
const command = "opencode.loopd.dashboard";
|
|
2508
|
+
let layerRegistered = false;
|
|
2509
|
+
const unclaimSlot = ctx.ui.slot({
|
|
2510
|
+
append: "app",
|
|
2511
|
+
render: () => {
|
|
2512
|
+
if (!layerRegistered) {
|
|
2513
|
+
layerRegistered = true;
|
|
2514
|
+
ctx.keymap.layer(() => ({
|
|
2515
|
+
commands: [{
|
|
2516
|
+
id: command,
|
|
2517
|
+
title: "Loop Dashboard",
|
|
2518
|
+
group: "Loop",
|
|
2519
|
+
palette: true,
|
|
2520
|
+
slash: {
|
|
2521
|
+
name: "loop"
|
|
2522
|
+
},
|
|
2523
|
+
bind: "<leader>o",
|
|
2524
|
+
run: open
|
|
2525
|
+
}],
|
|
2526
|
+
bindings: [command]
|
|
2527
|
+
}));
|
|
2528
|
+
}
|
|
2529
|
+
return null;
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
return () => {
|
|
2533
|
+
closeDialog();
|
|
2534
|
+
unclaimSlot();
|
|
2535
|
+
};
|
|
2536
|
+
};
|
|
2376
2537
|
var plugin_default = {
|
|
2377
2538
|
id: PLUGIN_ID,
|
|
2378
|
-
tui
|
|
2539
|
+
tui,
|
|
2540
|
+
setup: v2setup
|
|
2379
2541
|
};
|
|
2380
2542
|
export {
|
|
2381
2543
|
plugin_default as default
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@bojackduy/opencode-loopd",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.10.0",
|
|
5
5
|
"description": "Codex-inspired background goal engine for OpenCode — autonomous subagents, engine-driven loop, child worker sessions and modal TUI dashboard. Like Claude Code loop for OpenCode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "AGPL-3.0-or-later",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@opencode-ai/plugin": "^1.18.30",
|
|
83
|
+
"@opencode/plugin": "^2.0.11",
|
|
83
84
|
"@opentui/core": "^0.4.1",
|
|
84
85
|
"@opentui/solid": "^0.4.1",
|
|
85
86
|
"@types/bun": "^1.3.14",
|