@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.
Files changed (3) hide show
  1. package/dist/server.js +270 -18
  2. package/dist/tui.js +163 -1
  3. 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 { define } from "@opencode-ai/plugin/v2/promise";
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: { parts: [{ type: "text", text: message }] }
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 (input.parentAgent)
2379
- goal.parentAgent = input.parentAgent;
2380
- if (input.parentModel)
2381
- goal.parentModel = input.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 = define({
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.9.6",
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",