@bojackduy/opencode-loopd 1.9.7 → 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 +234 -5
  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();
@@ -2103,6 +2103,115 @@ function createRealHost(client, directory) {
2103
2103
  }
2104
2104
  };
2105
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
+ }
2106
2215
  async function withTimeout(promise, timeoutMs, operation) {
2107
2216
  let timer;
2108
2217
  try {
@@ -4337,6 +4446,9 @@ var PLUGIN_ID = "opencode-loopd.server";
4337
4446
  var server = async ({ client, directory }, pluginOptions) => {
4338
4447
  const defaults = parsePluginDefaults(pluginOptions);
4339
4448
  const host = createRealHost(client, directory);
4449
+ return createServerHooks(directory, host, defaults);
4450
+ };
4451
+ function createServerHooks(directory, host, defaults) {
4340
4452
  const goalService = createGoalService(host);
4341
4453
  const worker = createControlWorker({
4342
4454
  directory,
@@ -4465,7 +4577,7 @@ var server = async ({ client, directory }, pluginOptions) => {
4465
4577
  scheduleWorker.stop();
4466
4578
  }
4467
4579
  };
4468
- };
4580
+ }
4469
4581
  function parsePluginDefaults(options) {
4470
4582
  const agent = typeof options?.defaultAgent === "string" ? options.defaultAgent.trim() : "";
4471
4583
  const model = typeof options?.defaultModel === "string" ? options.defaultModel.trim() : "";
@@ -4476,10 +4588,127 @@ function parsePluginDefaults(options) {
4476
4588
  defaultChecks: checks.length > 0 ? checks : undefined
4477
4589
  };
4478
4590
  }
4479
- var v2 = define({
4591
+ var v2 = {
4480
4592
  id: PLUGIN_ID,
4481
- async setup() {}
4482
- });
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
+ }
4483
4712
  var plugin_default = {
4484
4713
  id: PLUGIN_ID,
4485
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.7",
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",