@bojackduy/opencode-loopd 1.0.0 → 1.1.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/README.md CHANGED
@@ -1,137 +1,250 @@
1
1
  # opencode-loopd
2
2
 
3
- **Background goal engine for OpenCode**run long-running tasks as autonomous child sessions while the main chat stays interactive.
3
+ **Codex-inspired background goal engine for OpenCode — autonomous subagents, engine-driven loop, and modal TUI dashboard. Like Claude Code's loop, for OpenCode.**
4
+
5
+ > Run long-running tasks as **autonomous child sessions** (subagents) while the main chat stays interactive. Create a goal with `/goal`, watch it loop in the background, and monitor everything from a keyboard-driven TUI — without blocking the parent conversation.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@bojackduy/opencode-loopd?style=flat-square&color=blue)](https://www.npmjs.com/package/@bojackduy/opencode-loopd)
8
+ [![license](https://img.shields.io/badge/license-AGPL--3.0--only-blue?style=flat-square)](LICENSE)
9
+ [![opencode plugin](https://img.shields.io/badge/opencode-plugin-9cf?style=flat-square)](https://opencode.ai)
10
+ [![TUI](https://img.shields.io/badge/TUI-OpenTUI-purple?style=flat-square)](https://github.com/anomalyco/opentui)
11
+
12
+ ![Loop Dashboard — modal TUI with goals, running indicator, and insert mode](assets/demo.png)
13
+
14
+ *Modal dashboard (`/loop` / `<leader>d`): zero chat pollution — keys are trapped inside the dialog, NORMAL vs INSERT modes, vivid per-status coloring.*
15
+
16
+ ## Why opencode-loopd
17
+
18
+ - **Background agents, not blocked chats** — an OpenCode plugin that spawns a dedicated **worker (child) session** per goal. The parent stays free to keep chatting.
19
+ - **Codex-style engine-driven loop** — idle detection → continuation steering with accumulated context (progress history, transcript tail, inbox). No prompt spam in the parent.
20
+ - **Parent ↔ child visibility** — `list/inspect/read_transcript/send_input` give the parent full observability. Bidirectional inbox lets you steer mid-run.
21
+ - **Safe by default** — per-goal artifact isolation (`.opencode/loopd/goals/<id>/`), `maxTurns`/`maxFailures`/`maxNoProgress`, force-finish → semantic `complete_goal` summary → parent notification via wake-up injection.
22
+ - **Modal TUI dashboard** — `<leader>d` or `/loop` opens a focused dialog (no leak to chat prompt). Vim-style navigation, live running indicator, per-status borders.
23
+
24
+ Keywords: `opencode` `opencode-plugin` `background-agent` `autonomous` `subagent` `loop` `goal` `tui` `codex` `claude-code` `worker`
4
25
 
5
26
  ## What it is
6
27
 
7
- opencode-loopd is an OpenCode plugin that creates background goals with dedicated worker sessions. The parent chat stays interactive while work happens behind the scenes. You can observe, steer, pause, or stop goals at any time.
28
+ opencode-loopd is an **OpenCode plugin** (server + TUI) that adds **background goals** to OpenCode. Each goal owns a **worker (child) subagent** that loops autonomously; the **loop engine** drives continuations; the **dashboard** and **owner tools** keep the parent in control. Think *Codex goals* or *Claude Code loop*, but native to OpenCode's session model.
8
29
 
9
30
  ## Install
10
31
 
11
- ### Manual
32
+ ### Plugin — simple (no installer, just the plugin)
12
33
 
13
- Add to `~/.config/opencode/opencode.jsonc`:
34
+ Add the package to **both** configs.
14
35
 
15
- ```json
36
+ **`~/.config/opencode/opencode.jsonc`** — server engine:
37
+
38
+ ```jsonc
16
39
  {
17
- "plugin": [
18
- "/Users/you/Code/opencode-loopd"
19
- ]
40
+ "plugin": ["@bojackduy/opencode-loopd"]
20
41
  }
21
42
  ```
22
43
 
23
- ### npx (recommended)
44
+ **`~/.config/opencode/tui.json`** dashboard (modal TUI):
45
+
46
+ ```jsonc
47
+ {
48
+ "plugin": ["@bojackduy/opencode-loopd"]
49
+ }
50
+ ```
51
+
52
+ Then restart OpenCode. Verify with `/loop` (palette → Loop Dashboard) or `<leader>d`.
53
+
54
+ For a local checkout:
55
+
56
+ ```jsonc
57
+ // opencode.jsonc
58
+ { "plugin": ["./path/to/opencode-loopd"] }
59
+ // tui.json
60
+ { "plugin": ["./path/to/opencode-loopd"] }
61
+ ```
62
+
63
+ ### Installer — also installs `/goal` + skill
24
64
 
25
65
  ```bash
26
66
  npx -y @bojackduy/opencode-loopd@latest
27
67
  ```
28
68
 
29
- Run the same command again to update. Then restart OpenCode.
69
+ Registers the plugin, installs the `/goal` slash command and the `loopd` skill into `~/.config/opencode/`. Run again to update. Then **restart OpenCode**.
30
70
 
31
- ### npm global
71
+ Uninstall:
72
+
73
+ ```bash
74
+ npx -y @bojackduy/opencode-loopd@latest --uninstall
75
+ ```
76
+
77
+ Or via global npm:
32
78
 
33
79
  ```bash
34
80
  npm install -g @bojackduy/opencode-loopd@latest
35
- opencode-loopd
81
+ opencode-loopd # install
82
+ opencode-loopd --uninstall # remove
36
83
  ```
37
84
 
38
85
  Then restart OpenCode.
39
86
 
87
+ ### Manual (local dev)
88
+
89
+ Add to `~/.config/opencode/opencode.jsonc`:
90
+
91
+ ```json
92
+ {
93
+ "plugin": ["@bojackduy/opencode-loopd"]
94
+ }
95
+ ```
96
+
97
+ Or for local checkout:
98
+
99
+ ```json
100
+ {
101
+ "plugin": ["/Users/you/Code/opencode-loopd"]
102
+ }
103
+ ```
104
+
105
+ `opencode-loopd` exposes:
106
+ - `opencode-loopd/server` → engine plugin
107
+ - `opencode-loopd/tui` → dashboard plugin
108
+
40
109
  ## Usage
41
110
 
42
- ### Create a goal
111
+ ### 1. Create a goal — `/goal`
43
112
 
44
113
  From any session, tell the agent:
45
114
 
46
115
  ```
47
- Create a background goal to fetch the latest AI news and save 10 items to ai-news.md
116
+ /goal fetch the latest AI news and save 10 items to ai-news.md
117
+ ```
118
+
119
+ Or be explicit:
120
+
121
+ ```
122
+ Create a background goal to find all .ts files in src/, count lines, and write the summary to line-counts.md — verify with grep -c.
48
123
  ```
49
124
 
50
- The agent will use `loopd_create_goal` to start the goal.
125
+ The agent will clarify (what/where/how to verify) and then call `loopd_create_goal`. The worker starts immediately in a hidden child session.
51
126
 
52
- ### Monitor with dashboard
127
+ ### 2. Monitor with dashboard — `/loop`
53
128
 
54
- Press `Ctrl+L` or open the command palette → "Loop Dashboard".
129
+ Press **`<leader>d`** or open the command palette → **"Loop Dashboard"** (also `/loop`).
130
+
131
+ Dashboard (NORMAL / INSERT `:`):
55
132
 
56
- Dashboard shortcuts:
57
133
  - `j/k` — move selection
58
- - `g/G` — jump to top/bottom
59
- - `o` — open child session (full transcript)
134
+ - `g/G` — top / bottom
135
+ - `o` — open child session (full transcript, native OpenCode view)
60
136
  - `p/r/R/x` — pause / resume / retry / clear selected goal
61
- - `?`toggle help
62
- - `:`enter insert mode (send message, control commands)
63
- - `Ctrl+N` — return to normal mode
137
+ - `:`insert mode → `:send <message>` to steer, `:force` / `:block` to finish manually
138
+ - `?`toggle help help stays open while you type
139
+ - `Ctrl+N` — back to NORMAL
64
140
  - `q` — close
65
141
 
66
- ### Inspect from main agent
142
+ > The dashboard traps all keys — even when open via the palette, the chat input cursor won't blink underneath.
143
+
144
+ ### 3. Inspect from the main agent (owner tools)
67
145
 
68
- The main agent can use these tools:
146
+ The parent never needs the dashboard — it can ask:
69
147
 
70
148
  ```
71
149
  list_background_goals() — status snapshot
72
- inspect_background_goal() — full detail
73
- read_goal_transcript() — child session transcript
74
- send_goal_input(goalID, msg) — send instruction to child
75
- pause_goal(goalID) pause
76
- resume_goal(goalID) — resume
77
- clear_goal(goalID) — stop and remove
150
+ inspect_background_goal() — full detail: objective, progress, blocker, runtime
151
+ read_goal_transcript() — last N worker messages (what it's doing)
152
+ send_goal_input(goalID, msg) — steer: "skip appendix PDFs"
153
+ pause_goal(goalID) / resume / clear
78
154
  ```
79
155
 
80
- ### Worker tools (child session)
156
+ ### 4. Worker tools (child session)
81
157
 
82
- The child worker has these tools:
158
+ The child sees:
83
159
 
84
- - `get_goal` — read objective, state, criteria
85
- - `report_goal_progress` — report what was done
86
- - `complete_goal` — mark done (must pass checks)
87
- - `block_goal` — mark blocked (needs user)
88
- - `question` (OpenCode builtin) — ask the user; appears in the TUI footer
160
+ - `get_goal` — read objective, contract, progress, failures
161
+ - `report_goal_progress` — after durable writes
162
+ - `complete_goal` — only when checks pass, with evidence
163
+ - `block_goal` — real blocker needing user
164
+ - `question` (OpenCode builtin) — clarification TUI blocker tab
165
+
166
+ Completion is semantic: the child writes the summary; the plugin forwards it to the parent via wake-up injection. If limits are hit, the engine first force-asks the child to `complete_goal`; only if ignored does it auto-block with a generic message.
89
167
 
90
168
  ## Architecture
91
169
 
92
170
  ```
93
- Main session (parent)
94
- owner tools: inspect, steer, pause
95
- Loopd engine
96
- continuation steering + accumulated context
97
- Child worker session
98
- goal tools: progress, complete, block, ask
171
+ ┌─────────────────────────┐
172
+ Main session (parent) │
173
+ │ owner tools │
174
+ inspect / steer / pause│
175
+ └────────────┬────────────┘
176
+ inbox child
177
+ │ wake-up ← engine
178
+ ┌───────────────────────┼───────────────────────┐
179
+ ▼ ▼ ▼
180
+ ┌─────────┐ ┌──────────┐ ┌─────────┐
181
+ │ Goal A │ │ Goal B │ │ Goal C │
182
+ │ active │ │ blocked │ │ paused │
183
+ └────┬────┘ └────┬─────┘ └────┬────┘
184
+ │ │ │
185
+ └─────────────────────┼───────────────────────┘
186
+
187
+ ┌─────────────┴─────────────┐
188
+ ▼ ▼
189
+ Loopd engine ──────────► Child worker (subagent)
190
+ (plugin server) steering get_goal / report / complete
191
+ │ lease / retry / polling │ block / question
192
+ │ force-finish / notify ▼
193
+ │ .opencode/loopd/goals/<id>/
194
+ │ progress.md / artifacts
195
+ └─────────────────────────────────┘
99
196
  ```
100
197
 
101
- - **Engine-driven loop**: child worksidle detectedengine re-prompts with accumulated context
102
- - **Parent visibility**: owner tools read child transcript, progress history, and current state
103
- - **Bidirectional messaging**: inbox system for user↔child instructions
104
- - **Safety defaults**: maxTurns=50, maxFailures=3
198
+ - **Engine-driven loop** (like Codex): `session.idle` lease + retry + polling → re-prompt child. Not a parent-driven re-prompt machine.
199
+ - **Accumulated context**: each continuation includes progress history, transcript tail, inbox messages, and artifact dir (unless objective names another dir).
200
+ - **Parent wake-up**: when the child calls `complete_goal`/`block_goal`, `tool.execute.after` injects the child's semantic summary into the owner's session — you see "Loop goal … completed: …" without polling.
105
201
 
106
- ## Example
202
+ ## Artifacts
107
203
 
108
- ```
109
- # Create a goal
110
- goal: "Find all .ts files in src/, count lines, write summary to line-counts.md"
204
+ Every goal writes under its own directory (`.opencode/loopd/goals/<goal-id>/`); `progressFile` defaults there. The dashboard and transcript never pollute the project root. If your objective says `save to ./reports/`, that wins.
111
205
 
112
- # Engine loops automatically
113
- Turn 1: child counts files → reports progress
114
- Turn 2: child sees "batch 1 done" → continues
115
- Turn 3: child finishes → calls complete_goal
206
+ ## Example multi-turn with force-finish
116
207
 
117
- # Parent inspects at any time
118
- inspect_background_goal() → shows turn count, progress, status
119
- read_goal_transcript() → shows child's work
208
+ ```
209
+ # Parent: /goal
210
+ goal: "Generate 5 files file1..5.md under artifacts/, 2 lines each — verify with glob."
211
+ maxTurns: 1
212
+
213
+ # Turn 1: child writes file1..3, reports progress
214
+ # → engine hits maxTurns, injects FINAL REPORT REQUIRED
215
+ # Turn 2 (forced): child wraps up, calls complete_goal(summary="created 5 files…", evidence="glob confirmed…")
216
+ # → engine forwards to parent: "Loop goal … completed: created 5 files…"
217
+ # → Artifacts: .opencode/loopd/goals/<id>/file*.md
120
218
  ```
121
219
 
220
+ See `assets/demo.png` above for the dashboard at rest (0 active, 7 done) with vivid status colors, running spinner, and `INSERT` badge.
221
+
122
222
  ## Configuration
123
223
 
124
- Goals accept these config options:
224
+ Goals accept:
125
225
 
126
226
  | Option | Default | Description |
127
227
  |---|---|---|
128
- | `maxTurns` | 50 | Max continuation turns |
129
- | `maxFailures` | 3 | Max consecutive failures before block |
130
- | `maxNoProgress` | 3 | Turns without progress before block |
131
- | `timeoutMs` | 300000 | Per-turn timeout (5 min) |
132
- | `compactEvery` | — | Compact child session every N turns |
133
- | `checks` | [] | Shell commands that must pass for completion |
134
- | `progressFile` | | Markdown file for transaction state |
228
+ | `maxTurns` | `50` | Max continuation turns before force-finish |
229
+ | `maxFailures` | `3` | Consecutive failures before block |
230
+ | `maxNoProgress` | `3` | Turns without progress before force-finish |
231
+ | `timeoutMs` | `300000` | Per-turn lease (5 min) |
232
+ | `compactEvery` | — | Compact child every N turns |
233
+ | `checks` | `[]` | Shell commands that must pass for `complete_goal` |
234
+ | `progressFile` | `<artifactDir>/progress.md` | Transaction state file |
235
+ | `artifactDir` | `goals/<id>/` | Auto — override only if objective names another dir |
236
+
237
+ Artifacts, logs, and state live under `.opencode/loopd/` (project-local). Locks live in `/tmp/loopd-locks/<project-hash>/` to avoid snapshot noise.
238
+
239
+ ## Dashboard tips
240
+
241
+ - Press `:` then type bare text → sent as `send_goal_input` to the selected goal.
242
+ - Bare `:force my summary --evidence "glob ok"` bypasses verification when the child is stuck.
243
+ - `o` jumps to the native OpenCode child session — full transcript, diffs, tool calls.
244
+
245
+ ## Related / SEO
246
+
247
+ OpenCode plugin for **background agents**, **autonomous subagents**, **loop**, **goal**, **TUI dashboard**, **Codex**-style continuation, **Claude Code** loop alternative, **opencode-tui**, **worker sessions**. Pairs well with `opencode-telescope` (search) and `lazyjira`/`lazyconfluence` (ATUI) from the same author.
135
248
 
136
249
  ## License
137
250
 
Binary file
package/commands/goal.md CHANGED
@@ -15,6 +15,6 @@ When you have enough to write a concrete objective:
15
15
  - `checks` — optional shell commands that must pass before the goal can be marked complete (e.g. `["npm test"]`)
16
16
  - `progressFile` — optional path to a markdown progress file
17
17
  - limits — optional `maxTurns`, `maxNoProgress`, `maxFailures`, `compactEvery`, `timeoutMs`
18
- 2. After it returns, tell the user the goal is running in the background and they can monitor it with `/loop` (or Ctrl+L).
18
+ 2. After it returns, tell the user the goal is running in the background and they can monitor it with `/loop` (or <leader>d).
19
19
 
20
20
  Important: the worker session runs autonomously — do not try to do the goal's work in this chat. This chat only creates the goal.
package/dist/server.js CHANGED
@@ -18,22 +18,22 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  // src/infrastructure/state-repository.ts
19
19
  var exports_state_repository = {};
20
20
  __export(exports_state_repository, {
21
- writeState: () => writeState,
22
- writeControlResponse: () => writeControlResponse,
23
- writeControlRequest: () => writeControlRequest,
24
- recoverStaleProcessing: () => recoverStaleProcessing,
25
- readState: () => readState,
26
- readEvents: () => readEvents,
27
- readControlResponse: () => readControlResponse,
28
- readControlRequest: () => readControlRequest,
29
- mutateState: () => mutateState,
30
- listPendingRequests: () => listPendingRequests,
31
- goalArtifactDir: () => goalArtifactDir,
32
- ensureGoalArtifactDir: () => ensureGoalArtifactDir,
33
- drainGoalInbox: () => drainGoalInbox,
34
- claimControlRequest: () => claimControlRequest,
21
+ appendEvent: () => appendEvent,
35
22
  appendGoalInbox: () => appendGoalInbox,
36
- appendEvent: () => appendEvent
23
+ claimControlRequest: () => claimControlRequest,
24
+ drainGoalInbox: () => drainGoalInbox,
25
+ ensureGoalArtifactDir: () => ensureGoalArtifactDir,
26
+ goalArtifactDir: () => goalArtifactDir,
27
+ listPendingRequests: () => listPendingRequests,
28
+ mutateState: () => mutateState,
29
+ readControlRequest: () => readControlRequest,
30
+ readControlResponse: () => readControlResponse,
31
+ readEvents: () => readEvents,
32
+ readState: () => readState,
33
+ recoverStaleProcessing: () => recoverStaleProcessing,
34
+ writeControlRequest: () => writeControlRequest,
35
+ writeControlResponse: () => writeControlResponse,
36
+ writeState: () => writeState
37
37
  });
38
38
  import { promises as fs } from "fs";
39
39
  import path from "path";
@@ -1733,7 +1733,7 @@ function goalTools(dir, goalService, hostSessionID) {
1733
1733
  workerSessionID: worker.workerSessionID,
1734
1734
  artifactDir: goal.config.artifactDir,
1735
1735
  name: args.name,
1736
- message: `Goal "${args.name}" created and started in the background. Artifacts: ${goal.config.artifactDir}. Monitor with /loop (Ctrl+L).`
1736
+ message: `Goal "${args.name}" created and started in the background. Artifacts: ${goal.config.artifactDir}. Monitor with /loop (<leader>d).`
1737
1737
  })
1738
1738
  };
1739
1739
  } catch (error) {
package/dist/tui.js CHANGED
@@ -246,7 +246,7 @@ function tokenize(input) {
246
246
  }
247
247
  function commandHelp() {
248
248
  return [
249
- "Modes: : insert \u2192 send/commands, Ctrl+N \u2192 normal, ? toggle help",
249
+ "Modes: : insert \u2192 send/commands, Ctrl+N \u2192 normal, ? toggle help, Shift+B bug report",
250
250
  "Nav: j/k move \u2502 g/G top/bottom \u2502 o open child \u2502 p/r/R/x pause/resume/retry/clear \u2502 L logs \u2502 q close",
251
251
  "Commands (insert mode, : prefix):",
252
252
  " :send <message> Send instruction to selected goal",
@@ -254,12 +254,87 @@ function commandHelp() {
254
254
  " :force <summary> --evidence <text> Force-complete (bypass checks)",
255
255
  " :block <reason> --needed <text> Force-block the selected goal",
256
256
  " :pause / :resume / :retry / :clear Quick controls (also p/r/R/x)",
257
+ " :bug / :report Open prefilled GitHub bug report",
257
258
  " :logs / :help / :q Toggle logs / help / close",
258
259
  " Tip: create goals via /goal in the parent chat (agent clarifies first)."
259
260
  ].join(`
260
261
  `);
261
262
  }
262
263
 
264
+ // src/browser.ts
265
+ import { spawn } from "child_process";
266
+ var LOOPD_ISSUES_URL = "https://github.com/bojackduy/opencode-loopd/issues/new";
267
+ function bugReportUrl(context = {}) {
268
+ const url = new URL(LOOPD_ISSUES_URL);
269
+ url.searchParams.set("title", "bug: ");
270
+ url.searchParams.set("body", [
271
+ "## What happened?",
272
+ "",
273
+ "Describe the unexpected behavior.",
274
+ "",
275
+ "## What did you expect?",
276
+ "",
277
+ "Describe the expected behavior.",
278
+ "",
279
+ "## Steps to reproduce",
280
+ "",
281
+ "1. ",
282
+ "2. ",
283
+ "3. ",
284
+ "",
285
+ "## Environment",
286
+ "",
287
+ `- opencode-loopd version: ${context.runtimeLabel ?? ""}`,
288
+ `- OS: ${process.platform}`,
289
+ "- Terminal:",
290
+ "- Installation: npm / source",
291
+ context.extra ? `
292
+ ## Goal context
293
+
294
+ ${context.extra}
295
+ ` : "",
296
+ "Do not include secrets, API tokens, or .opencode/loopd contents."
297
+ ].join(`
298
+ `));
299
+ return url.toString();
300
+ }
301
+ function openBrowserUrl(value, options = {}) {
302
+ try {
303
+ const url = normalizeBrowserUrl(value);
304
+ const command = browserCommandForUrl(url, options.platform);
305
+ const launch = options.launch ?? launchBrowserCommand;
306
+ launch(command.command, command.args);
307
+ return { status: "opened", url };
308
+ } catch (error) {
309
+ return { status: "blocked", reason: error instanceof Error ? error.message : "Could not open the browser." };
310
+ }
311
+ }
312
+ function browserCommandForUrl(value, platform = process.platform) {
313
+ const url = normalizeBrowserUrl(value);
314
+ if (platform === "darwin")
315
+ return { command: "open", args: [url] };
316
+ if (platform === "linux")
317
+ return { command: "xdg-open", args: [url] };
318
+ if (platform === "win32")
319
+ return { command: "cmd.exe", args: ["/d", "/s", "/c", `start "" "${url}"`] };
320
+ throw new Error(`Opening a browser is not supported on ${platform}.`);
321
+ }
322
+ function normalizeBrowserUrl(value) {
323
+ let url;
324
+ try {
325
+ url = new URL(value);
326
+ } catch {
327
+ throw new Error("The selected page does not have a valid browser URL.");
328
+ }
329
+ if (url.protocol !== "http:" && url.protocol !== "https:")
330
+ throw new Error("Only http and https page URLs can be opened in a browser.");
331
+ return url.toString();
332
+ }
333
+ function launchBrowserCommand(command, args) {
334
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
335
+ child.unref();
336
+ }
337
+
263
338
  // src/tui/dashboard.tsx
264
339
  import { randomUUID } from "crypto";
265
340
  var LOG_FILE = "/tmp/loopd-tui.log";
@@ -548,6 +623,18 @@ function LoopDashboard(props) {
548
623
  }
549
624
  return;
550
625
  }
626
+ if (key === "B") {
627
+ prevent(evt);
628
+ const goal = selectedGoal();
629
+ const extra = goal ? `Goal: ${goal.name} (${goal.id.slice(0, 8)}) status=${goal.status} objective=${goal.objective.slice(0, 120)}` : "No goal selected";
630
+ const url = bugReportUrl({
631
+ runtimeLabel: `opencode-loopd dashboard`,
632
+ extra
633
+ });
634
+ const res = openBrowserUrl(url);
635
+ setStatusText(res.status === "opened" ? "Opening bug report in browser\u2026" : `Could not open browser: ${res.reason} \u2014 ${url}`);
636
+ return;
637
+ }
551
638
  if (key === "q") {
552
639
  prevent(evt);
553
640
  props.api.ui.dialog.clear();
@@ -721,6 +808,18 @@ function LoopDashboard(props) {
721
808
  setStatusText("Create goals via /goal in the parent chat (agent clarifies first). Dashboard: :send to steer the worker.");
722
809
  break;
723
810
  }
811
+ case "bug":
812
+ case "report": {
813
+ const goal = selectedGoal();
814
+ const extra = goal ? `Goal: ${goal.name} (${goal.id.slice(0, 8)}) status=${goal.status} objective=${goal.objective.slice(0, 120)}` : "No goal selected";
815
+ const url = bugReportUrl({
816
+ runtimeLabel: `opencode-loopd dashboard`,
817
+ extra
818
+ });
819
+ const res = openBrowserUrl(url);
820
+ setStatusText(res.status === "opened" ? "Opening bug report in browser\u2026" : `Could not open browser: ${res.reason} \u2014 ${url}`);
821
+ break;
822
+ }
724
823
  case "logs":
725
824
  setShowLogs(!showLogs());
726
825
  break;
@@ -828,12 +927,12 @@ function LoopDashboard(props) {
828
927
  _$setProp(_el$26, "maxHeight", 14);
829
928
  _$setProp(_el$26, "overflow", "hidden");
830
929
  _$insertNode(_el$27, _el$28);
831
- _$insertNode(_el$28, _$createTextNode(`\u2501\u2501\u2501 Keys: ? toggle : insert Ctrl+N normal o open q close \u2501\u2501\u2501`));
930
+ _$insertNode(_el$28, _$createTextNode(`\u2501\u2501\u2501 Keys: ? toggle : insert Ctrl+N normal o open B bug q close \u2501\u2501\u2501`));
832
931
  _$setProp(_el$28, "style", {
833
932
  fg: "yellow",
834
933
  bold: true
835
934
  });
836
- _$insert(_el$26, _$createComponent(For, {
935
+ _$insert(_el$27, _$createComponent(For, {
837
936
  get each() {
838
937
  return commandHelp().split(`
839
938
  `);
@@ -841,16 +940,16 @@ function LoopDashboard(props) {
841
940
  children: (line) => {
842
941
  const isHeader = line.startsWith("Modes:") || line.startsWith("Nav:") || line.startsWith("Commands");
843
942
  const isCmd = line.trim().startsWith(":");
844
- return (() => {
845
- var _el$41 = _$createElement("text"), _el$42 = _$createElement("span");
846
- _$insertNode(_el$41, _el$42);
847
- _$insert(_el$42, line);
848
- _$effect((_$p) => _$setProp(_el$42, "style", {
943
+ return [`
944
+ `, (() => {
945
+ var _el$41 = _$createElement("span");
946
+ _$insert(_el$41, line);
947
+ _$effect((_$p) => _$setProp(_el$41, "style", {
849
948
  fg: isHeader ? theme().primary : isCmd ? theme().warning : theme().text,
850
949
  bold: isHeader
851
950
  }, _$p));
852
951
  return _el$41;
853
- })();
952
+ })()];
854
953
  }
855
954
  }), null);
856
955
  _$effect((_$p) => _$setProp(_el$26, "backgroundColor", theme().background, _$p));
@@ -868,31 +967,31 @@ function LoopDashboard(props) {
868
967
  },
869
968
  get fallback() {
870
969
  return (() => {
871
- var _el$43 = _$createElement("box"), _el$44 = _$createElement("text"), _el$45 = _$createElement("span"), _el$47 = _$createElement("span"), _el$49 = _$createElement("span"), _el$51 = _$createElement("text"), _el$52 = _$createElement("span"), _el$54 = _$createElement("span"), _el$56 = _$createElement("span"), _el$58 = _$createElement("span"), _el$60 = _$createElement("span"), _el$62 = _$createElement("span"), _el$64 = _$createElement("span");
970
+ var _el$42 = _$createElement("box"), _el$43 = _$createElement("text"), _el$44 = _$createElement("span"), _el$46 = _$createElement("span"), _el$48 = _$createElement("span"), _el$50 = _$createElement("text"), _el$51 = _$createElement("span"), _el$53 = _$createElement("span"), _el$55 = _$createElement("span"), _el$57 = _$createElement("span"), _el$59 = _$createElement("span"), _el$61 = _$createElement("span"), _el$63 = _$createElement("span");
971
+ _$insertNode(_el$42, _el$43);
972
+ _$insertNode(_el$42, _el$50);
973
+ _$setProp(_el$42, "flexDirection", "column");
974
+ _$setProp(_el$42, "gap", 1);
872
975
  _$insertNode(_el$43, _el$44);
873
- _$insertNode(_el$43, _el$51);
874
- _$setProp(_el$43, "flexDirection", "column");
875
- _$setProp(_el$43, "gap", 1);
876
- _$insertNode(_el$44, _el$45);
877
- _$insertNode(_el$44, _el$47);
878
- _$insertNode(_el$44, _el$49);
879
- _$insertNode(_el$45, _$createTextNode(`No active goals.`));
880
- _$insertNode(_el$47, _$createTextNode(` /goal`));
881
- _$insertNode(_el$49, _$createTextNode(` in parent chat to create one.`));
882
- _$insertNode(_el$51, _el$52);
883
- _$insertNode(_el$51, _el$54);
884
- _$insertNode(_el$51, _el$56);
885
- _$insertNode(_el$51, _el$58);
886
- _$insertNode(_el$51, _el$60);
887
- _$insertNode(_el$51, _el$62);
888
- _$insertNode(_el$51, _el$64);
889
- _$insertNode(_el$52, _$createTextNode(`Tip: `));
890
- _$insertNode(_el$54, _$createTextNode(`:send`));
891
- _$insertNode(_el$56, _$createTextNode(` to steer the worker \xB7 `));
892
- _$insertNode(_el$58, _$createTextNode(`o`));
893
- _$insertNode(_el$60, _$createTextNode(` to open child \xB7 `));
894
- _$insertNode(_el$62, _$createTextNode(`:force`));
895
- _$insertNode(_el$64, _$createTextNode(` to complete manually.`));
976
+ _$insertNode(_el$43, _el$46);
977
+ _$insertNode(_el$43, _el$48);
978
+ _$insertNode(_el$44, _$createTextNode(`No active goals.`));
979
+ _$insertNode(_el$46, _$createTextNode(` /goal`));
980
+ _$insertNode(_el$48, _$createTextNode(` in parent chat to create one.`));
981
+ _$insertNode(_el$50, _el$51);
982
+ _$insertNode(_el$50, _el$53);
983
+ _$insertNode(_el$50, _el$55);
984
+ _$insertNode(_el$50, _el$57);
985
+ _$insertNode(_el$50, _el$59);
986
+ _$insertNode(_el$50, _el$61);
987
+ _$insertNode(_el$50, _el$63);
988
+ _$insertNode(_el$51, _$createTextNode(`Tip: `));
989
+ _$insertNode(_el$53, _$createTextNode(`:send`));
990
+ _$insertNode(_el$55, _$createTextNode(` to steer the worker \xB7 `));
991
+ _$insertNode(_el$57, _$createTextNode(`o`));
992
+ _$insertNode(_el$59, _$createTextNode(` to open child \xB7 `));
993
+ _$insertNode(_el$61, _$createTextNode(`:force`));
994
+ _$insertNode(_el$63, _$createTextNode(` to complete manually.`));
896
995
  _$effect((_p$) => {
897
996
  var _v$21 = {
898
997
  fg: theme().textMuted
@@ -915,16 +1014,16 @@ function LoopDashboard(props) {
915
1014
  }, _v$30 = {
916
1015
  fg: theme().textMuted
917
1016
  };
918
- _v$21 !== _p$.e && (_p$.e = _$setProp(_el$45, "style", _v$21, _p$.e));
919
- _v$22 !== _p$.t && (_p$.t = _$setProp(_el$47, "style", _v$22, _p$.t));
920
- _v$23 !== _p$.a && (_p$.a = _$setProp(_el$49, "style", _v$23, _p$.a));
921
- _v$24 !== _p$.o && (_p$.o = _$setProp(_el$52, "style", _v$24, _p$.o));
922
- _v$25 !== _p$.i && (_p$.i = _$setProp(_el$54, "style", _v$25, _p$.i));
923
- _v$26 !== _p$.n && (_p$.n = _$setProp(_el$56, "style", _v$26, _p$.n));
924
- _v$27 !== _p$.s && (_p$.s = _$setProp(_el$58, "style", _v$27, _p$.s));
925
- _v$28 !== _p$.h && (_p$.h = _$setProp(_el$60, "style", _v$28, _p$.h));
926
- _v$29 !== _p$.r && (_p$.r = _$setProp(_el$62, "style", _v$29, _p$.r));
927
- _v$30 !== _p$.d && (_p$.d = _$setProp(_el$64, "style", _v$30, _p$.d));
1017
+ _v$21 !== _p$.e && (_p$.e = _$setProp(_el$44, "style", _v$21, _p$.e));
1018
+ _v$22 !== _p$.t && (_p$.t = _$setProp(_el$46, "style", _v$22, _p$.t));
1019
+ _v$23 !== _p$.a && (_p$.a = _$setProp(_el$48, "style", _v$23, _p$.a));
1020
+ _v$24 !== _p$.o && (_p$.o = _$setProp(_el$51, "style", _v$24, _p$.o));
1021
+ _v$25 !== _p$.i && (_p$.i = _$setProp(_el$53, "style", _v$25, _p$.i));
1022
+ _v$26 !== _p$.n && (_p$.n = _$setProp(_el$55, "style", _v$26, _p$.n));
1023
+ _v$27 !== _p$.s && (_p$.s = _$setProp(_el$57, "style", _v$27, _p$.s));
1024
+ _v$28 !== _p$.h && (_p$.h = _$setProp(_el$59, "style", _v$28, _p$.h));
1025
+ _v$29 !== _p$.r && (_p$.r = _$setProp(_el$61, "style", _v$29, _p$.r));
1026
+ _v$30 !== _p$.d && (_p$.d = _$setProp(_el$63, "style", _v$30, _p$.d));
928
1027
  return _p$;
929
1028
  }, {
930
1029
  e: undefined,
@@ -938,7 +1037,7 @@ function LoopDashboard(props) {
938
1037
  r: undefined,
939
1038
  d: undefined
940
1039
  });
941
- return _el$43;
1040
+ return _el$42;
942
1041
  })();
943
1042
  },
944
1043
  get children() {
@@ -961,86 +1060,86 @@ function LoopDashboard(props) {
961
1060
  return phaseColor(runtime().phase || "idle", theme());
962
1061
  };
963
1062
  return (() => {
964
- var _el$66 = _$createElement("box"), _el$67 = _$createElement("text"), _el$68 = _$createElement("span"), _el$69 = _$createElement("span"), _el$71 = _$createElement("span");
1063
+ var _el$65 = _$createElement("box"), _el$66 = _$createElement("text"), _el$67 = _$createElement("span"), _el$68 = _$createElement("span"), _el$70 = _$createElement("span");
1064
+ _$insertNode(_el$65, _el$66);
1065
+ _$setProp(_el$65, "flexDirection", "row");
1066
+ _$setProp(_el$65, "paddingLeft", 1);
1067
+ _$setProp(_el$65, "paddingRight", 1);
965
1068
  _$insertNode(_el$66, _el$67);
966
- _$setProp(_el$66, "flexDirection", "row");
967
- _$setProp(_el$66, "paddingLeft", 1);
968
- _$setProp(_el$66, "paddingRight", 1);
969
- _$insertNode(_el$67, _el$68);
970
- _$insertNode(_el$67, _el$69);
971
- _$insertNode(_el$67, _el$71);
972
- _$insert(_el$68, (() => {
1069
+ _$insertNode(_el$66, _el$68);
1070
+ _$insertNode(_el$66, _el$70);
1071
+ _$insert(_el$67, (() => {
973
1072
  var _c$2 = _$memo(() => !!isActive());
974
1073
  return () => _c$2() ? `\u25B6 ${statusIcon(goal.status)} ${goal.name}` : ` ${statusIcon(goal.status)} ${goal.name}`;
975
1074
  })());
976
- _$insertNode(_el$69, _$createTextNode(` \u2502 `));
977
- _$insert(_el$71, () => goal.status.toUpperCase());
978
- _$insert(_el$67, (() => {
1075
+ _$insertNode(_el$68, _$createTextNode(` \u2502 `));
1076
+ _$insert(_el$70, () => goal.status.toUpperCase());
1077
+ _$insert(_el$66, (() => {
979
1078
  var _c$3 = _$memo(() => !!runtime());
980
1079
  return () => _c$3() && [(() => {
981
- var _el$72 = _$createElement("span");
982
- _$insertNode(_el$72, _$createTextNode(` \u2502 `));
983
- _$effect((_$p) => _$setProp(_el$72, "style", {
1080
+ var _el$71 = _$createElement("span");
1081
+ _$insertNode(_el$71, _$createTextNode(` \u2502 `));
1082
+ _$effect((_$p) => _$setProp(_el$71, "style", {
984
1083
  fg: theme().textMuted
985
1084
  }, _$p));
986
- return _el$72;
1085
+ return _el$71;
987
1086
  })(), (() => {
988
- var _el$74 = _$createElement("span"), _el$75 = _$createTextNode(` `);
989
- _$insertNode(_el$74, _el$75);
990
- _$insert(_el$74, (() => {
1087
+ var _el$73 = _$createElement("span"), _el$74 = _$createTextNode(` `);
1088
+ _$insertNode(_el$73, _el$74);
1089
+ _$insert(_el$73, (() => {
991
1090
  var _c$6 = _$memo(() => runtime().phase === "running");
992
1091
  return () => _c$6() ? runningFrame() : phaseIcon(runtime().phase);
993
- })(), _el$75);
994
- _$insert(_el$74, () => runtime().phase.toUpperCase(), null);
995
- _$effect((_$p) => _$setProp(_el$74, "style", {
1092
+ })(), _el$74);
1093
+ _$insert(_el$73, () => runtime().phase.toUpperCase(), null);
1094
+ _$effect((_$p) => _$setProp(_el$73, "style", {
996
1095
  fg: turnColor(),
997
1096
  bold: runtime().phase === "running"
998
1097
  }, _$p));
999
- return _el$74;
1098
+ return _el$73;
1000
1099
  })(), (() => {
1001
- var _el$76 = _$createElement("span"), _el$77 = _$createTextNode(` `);
1002
- _$insertNode(_el$76, _el$77);
1003
- _$insert(_el$76, () => runtime().turnCount, null);
1004
- _$insert(_el$76, maxTurns ? `/${maxTurns}` : "", null);
1005
- _$effect((_$p) => _$setProp(_el$76, "style", {
1100
+ var _el$75 = _$createElement("span"), _el$76 = _$createTextNode(` `);
1101
+ _$insertNode(_el$75, _el$76);
1102
+ _$insert(_el$75, () => runtime().turnCount, null);
1103
+ _$insert(_el$75, maxTurns ? `/${maxTurns}` : "", null);
1104
+ _$effect((_$p) => _$setProp(_el$75, "style", {
1006
1105
  fg: turnColor()
1007
1106
  }, _$p));
1008
- return _el$76;
1107
+ return _el$75;
1009
1108
  })(), (() => {
1010
- var _el$78 = _$createElement("span"), _el$79 = _$createTextNode(` `);
1011
- _$insertNode(_el$78, _el$79);
1012
- _$insert(_el$78, () => ageLabel(runtime().lastProgressAt || runtime().lastRunAt, clock()), null);
1013
- _$effect((_$p) => _$setProp(_el$78, "style", {
1109
+ var _el$77 = _$createElement("span"), _el$78 = _$createTextNode(` `);
1110
+ _$insertNode(_el$77, _el$78);
1111
+ _$insert(_el$77, () => ageLabel(runtime().lastProgressAt || runtime().lastRunAt, clock()), null);
1112
+ _$effect((_$p) => _$setProp(_el$77, "style", {
1014
1113
  fg: theme().textMuted
1015
1114
  }, _$p));
1016
- return _el$78;
1115
+ return _el$77;
1017
1116
  })()];
1018
1117
  })(), null);
1019
- _$insert(_el$67, (() => {
1118
+ _$insert(_el$66, (() => {
1020
1119
  var _c$4 = _$memo(() => !!(runtime() && runtime().consecutiveFailures > 0));
1021
1120
  return () => _c$4() && (() => {
1022
- var _el$80 = _$createElement("span"), _el$81 = _$createTextNode(` \u2502 \u26A0 `), _el$82 = _$createTextNode(` fail`);
1023
- _$insertNode(_el$80, _el$81);
1024
- _$insertNode(_el$80, _el$82);
1025
- _$insert(_el$80, () => runtime().consecutiveFailures, _el$82);
1026
- _$effect((_$p) => _$setProp(_el$80, "style", {
1121
+ var _el$79 = _$createElement("span"), _el$80 = _$createTextNode(` \u2502 \u26A0 `), _el$81 = _$createTextNode(` fail`);
1122
+ _$insertNode(_el$79, _el$80);
1123
+ _$insertNode(_el$79, _el$81);
1124
+ _$insert(_el$79, () => runtime().consecutiveFailures, _el$81);
1125
+ _$effect((_$p) => _$setProp(_el$79, "style", {
1027
1126
  fg: theme().error,
1028
1127
  bold: true
1029
1128
  }, _$p));
1030
- return _el$80;
1129
+ return _el$79;
1031
1130
  })();
1032
1131
  })(), null);
1033
- _$insert(_el$67, (() => {
1132
+ _$insert(_el$66, (() => {
1034
1133
  var _c$5 = _$memo(() => !!(runtime() && (runtime().noProgressCount || 0) > 0));
1035
1134
  return () => _c$5() && (() => {
1036
- var _el$83 = _$createElement("span"), _el$84 = _$createTextNode(` \u2502 `), _el$85 = _$createTextNode(` no-progress`);
1037
- _$insertNode(_el$83, _el$84);
1038
- _$insertNode(_el$83, _el$85);
1039
- _$insert(_el$83, () => runtime().noProgressCount, _el$85);
1040
- _$effect((_$p) => _$setProp(_el$83, "style", {
1135
+ var _el$82 = _$createElement("span"), _el$83 = _$createTextNode(` \u2502 `), _el$84 = _$createTextNode(` no-progress`);
1136
+ _$insertNode(_el$82, _el$83);
1137
+ _$insertNode(_el$82, _el$84);
1138
+ _$insert(_el$82, () => runtime().noProgressCount, _el$84);
1139
+ _$effect((_$p) => _$setProp(_el$82, "style", {
1041
1140
  fg: theme().warning
1042
1141
  }, _$p));
1043
- return _el$83;
1142
+ return _el$82;
1044
1143
  })();
1045
1144
  })(), null);
1046
1145
  _$effect((_p$) => {
@@ -1053,10 +1152,10 @@ function LoopDashboard(props) {
1053
1152
  fg: statusColor(goal.status, theme()),
1054
1153
  bold: true
1055
1154
  };
1056
- _v$31 !== _p$.e && (_p$.e = _$setProp(_el$66, "backgroundColor", _v$31, _p$.e));
1057
- _v$32 !== _p$.t && (_p$.t = _$setProp(_el$68, "style", _v$32, _p$.t));
1058
- _v$33 !== _p$.a && (_p$.a = _$setProp(_el$69, "style", _v$33, _p$.a));
1059
- _v$34 !== _p$.o && (_p$.o = _$setProp(_el$71, "style", _v$34, _p$.o));
1155
+ _v$31 !== _p$.e && (_p$.e = _$setProp(_el$65, "backgroundColor", _v$31, _p$.e));
1156
+ _v$32 !== _p$.t && (_p$.t = _$setProp(_el$67, "style", _v$32, _p$.t));
1157
+ _v$33 !== _p$.a && (_p$.a = _$setProp(_el$68, "style", _v$33, _p$.a));
1158
+ _v$34 !== _p$.o && (_p$.o = _$setProp(_el$70, "style", _v$34, _p$.o));
1060
1159
  return _p$;
1061
1160
  }, {
1062
1161
  e: undefined,
@@ -1064,7 +1163,7 @@ function LoopDashboard(props) {
1064
1163
  a: undefined,
1065
1164
  o: undefined
1066
1165
  });
1067
- return _el$66;
1166
+ return _el$65;
1068
1167
  })();
1069
1168
  }
1070
1169
  });
@@ -1077,144 +1176,144 @@ function LoopDashboard(props) {
1077
1176
  children: (goal) => {
1078
1177
  const rt = () => state()?.runtimes.find((r) => r.goalID === goal().id);
1079
1178
  return (() => {
1080
- var _el$86 = _$createElement("box"), _el$87 = _$createElement("text"), _el$88 = _$createElement("span"), _el$89 = _$createTextNode(` `), _el$90 = _$createElement("span"), _el$91 = _$createTextNode(` `), _el$92 = _$createTextNode(`
1081
- `), _el$93 = _$createElement("span");
1179
+ var _el$85 = _$createElement("box"), _el$86 = _$createElement("text"), _el$87 = _$createElement("span"), _el$88 = _$createTextNode(` `), _el$89 = _$createElement("span"), _el$90 = _$createTextNode(` `), _el$91 = _$createTextNode(`
1180
+ `), _el$92 = _$createElement("span");
1181
+ _$insertNode(_el$85, _el$86);
1182
+ _$setProp(_el$85, "flexDirection", "column");
1183
+ _$setProp(_el$85, "border", true);
1184
+ _$setProp(_el$85, "padding", 1);
1185
+ _$setProp(_el$85, "flexShrink", 0);
1186
+ _$setProp(_el$85, "maxHeight", 10);
1082
1187
  _$insertNode(_el$86, _el$87);
1083
- _$setProp(_el$86, "flexDirection", "column");
1084
- _$setProp(_el$86, "border", true);
1085
- _$setProp(_el$86, "padding", 1);
1086
- _$setProp(_el$86, "flexShrink", 0);
1087
- _$setProp(_el$86, "maxHeight", 10);
1188
+ _$insertNode(_el$86, _el$89);
1189
+ _$insertNode(_el$86, _el$91);
1190
+ _$insertNode(_el$86, _el$92);
1088
1191
  _$insertNode(_el$87, _el$88);
1089
- _$insertNode(_el$87, _el$90);
1090
- _$insertNode(_el$87, _el$92);
1091
- _$insertNode(_el$87, _el$93);
1092
- _$insertNode(_el$88, _el$89);
1093
- _$insert(_el$88, () => statusIcon(goal().status), _el$89);
1094
- _$insert(_el$88, () => goal().name, null);
1095
- _$insertNode(_el$90, _el$91);
1096
- _$insert(_el$90, () => goal().status.toUpperCase(), null);
1097
- _$insert(_el$87, (() => {
1192
+ _$insert(_el$87, () => statusIcon(goal().status), _el$88);
1193
+ _$insert(_el$87, () => goal().name, null);
1194
+ _$insertNode(_el$89, _el$90);
1195
+ _$insert(_el$89, () => goal().status.toUpperCase(), null);
1196
+ _$insert(_el$86, (() => {
1098
1197
  var _c$7 = _$memo(() => !!rt());
1099
1198
  return () => _c$7() && [(() => {
1100
- var _el$94 = _$createElement("span");
1101
- _$insertNode(_el$94, _$createTextNode(` \u2502 `));
1102
- _$effect((_$p) => _$setProp(_el$94, "style", {
1199
+ var _el$93 = _$createElement("span");
1200
+ _$insertNode(_el$93, _$createTextNode(` \u2502 `));
1201
+ _$effect((_$p) => _$setProp(_el$93, "style", {
1103
1202
  fg: theme().textMuted
1104
1203
  }, _$p));
1105
- return _el$94;
1204
+ return _el$93;
1106
1205
  })(), (() => {
1107
- var _el$96 = _$createElement("span"), _el$97 = _$createTextNode(` `);
1108
- _$insertNode(_el$96, _el$97);
1109
- _$insert(_el$96, () => phaseIcon(rt().phase), _el$97);
1110
- _$insert(_el$96, () => rt().phase, null);
1111
- _$effect((_$p) => _$setProp(_el$96, "style", {
1206
+ var _el$95 = _$createElement("span"), _el$96 = _$createTextNode(` `);
1207
+ _$insertNode(_el$95, _el$96);
1208
+ _$insert(_el$95, () => phaseIcon(rt().phase), _el$96);
1209
+ _$insert(_el$95, () => rt().phase, null);
1210
+ _$effect((_$p) => _$setProp(_el$95, "style", {
1112
1211
  fg: phaseColor(rt().phase, theme()),
1113
1212
  bold: true
1114
1213
  }, _$p));
1115
- return _el$96;
1214
+ return _el$95;
1116
1215
  })(), (() => {
1117
- var _el$98 = _$createElement("span"), _el$99 = _$createTextNode(` turn `);
1118
- _$insertNode(_el$98, _el$99);
1119
- _$insert(_el$98, () => rt().turnCount, null);
1120
- _$effect((_$p) => _$setProp(_el$98, "style", {
1216
+ var _el$97 = _$createElement("span"), _el$98 = _$createTextNode(` turn `);
1217
+ _$insertNode(_el$97, _el$98);
1218
+ _$insert(_el$97, () => rt().turnCount, null);
1219
+ _$effect((_$p) => _$setProp(_el$97, "style", {
1121
1220
  fg: theme().textMuted
1122
1221
  }, _$p));
1123
- return _el$98;
1222
+ return _el$97;
1124
1223
  })()];
1125
- })(), _el$92);
1126
- _$insert(_el$93, () => goal().objective.slice(0, 160));
1127
- _$insert(_el$87, (() => {
1224
+ })(), _el$91);
1225
+ _$insert(_el$92, () => goal().objective.slice(0, 160));
1226
+ _$insert(_el$86, (() => {
1128
1227
  var _c$8 = _$memo(() => !!goal().lastProgress);
1129
1228
  return () => _c$8() && [(() => {
1130
- var _el$100 = _$createElement("span"), _el$101 = _$createTextNode(`
1229
+ var _el$99 = _$createElement("span"), _el$100 = _$createTextNode(`
1131
1230
  \u2714 `);
1132
- _$insertNode(_el$100, _el$101);
1133
- _$effect((_$p) => _$setProp(_el$100, "style", {
1231
+ _$insertNode(_el$99, _el$100);
1232
+ _$effect((_$p) => _$setProp(_el$99, "style", {
1134
1233
  fg: theme().success
1135
1234
  }, _$p));
1136
- return _el$100;
1235
+ return _el$99;
1137
1236
  })(), (() => {
1138
- var _el$103 = _$createElement("span");
1139
- _$insert(_el$103, () => goal().lastProgress.summary.slice(0, 100));
1140
- _$effect((_$p) => _$setProp(_el$103, "style", {
1237
+ var _el$102 = _$createElement("span");
1238
+ _$insert(_el$102, () => goal().lastProgress.summary.slice(0, 100));
1239
+ _$effect((_$p) => _$setProp(_el$102, "style", {
1141
1240
  fg: theme().text
1142
1241
  }, _$p));
1143
- return _el$103;
1242
+ return _el$102;
1144
1243
  })(), (() => {
1145
- var _el$104 = _$createElement("span"), _el$105 = _$createTextNode(` \u2192 `);
1146
- _$insertNode(_el$104, _el$105);
1147
- _$insert(_el$104, () => goal().lastProgress.next?.slice(0, 60) || "", null);
1148
- _$effect((_$p) => _$setProp(_el$104, "style", {
1244
+ var _el$103 = _$createElement("span"), _el$104 = _$createTextNode(` \u2192 `);
1245
+ _$insertNode(_el$103, _el$104);
1246
+ _$insert(_el$103, () => goal().lastProgress.next?.slice(0, 60) || "", null);
1247
+ _$effect((_$p) => _$setProp(_el$103, "style", {
1149
1248
  fg: theme().textMuted
1150
1249
  }, _$p));
1151
- return _el$104;
1250
+ return _el$103;
1152
1251
  })()];
1153
1252
  })(), null);
1154
- _$insert(_el$87, (() => {
1253
+ _$insert(_el$86, (() => {
1155
1254
  var _c$9 = _$memo(() => !!goal().blocker);
1156
1255
  return () => _c$9() && [(() => {
1157
- var _el$106 = _$createElement("span"), _el$107 = _$createTextNode(`
1256
+ var _el$105 = _$createElement("span"), _el$106 = _$createTextNode(`
1158
1257
  \u2716 blocked: `);
1159
- _$insertNode(_el$106, _el$107);
1160
- _$effect((_$p) => _$setProp(_el$106, "style", {
1258
+ _$insertNode(_el$105, _el$106);
1259
+ _$effect((_$p) => _$setProp(_el$105, "style", {
1161
1260
  fg: theme().error,
1162
1261
  bold: true
1163
1262
  }, _$p));
1164
- return _el$106;
1263
+ return _el$105;
1165
1264
  })(), (() => {
1166
- var _el$109 = _$createElement("span");
1167
- _$insert(_el$109, () => goal().blocker.reason.slice(0, 140));
1168
- _$effect((_$p) => _$setProp(_el$109, "style", {
1265
+ var _el$108 = _$createElement("span");
1266
+ _$insert(_el$108, () => goal().blocker.reason.slice(0, 140));
1267
+ _$effect((_$p) => _$setProp(_el$108, "style", {
1169
1268
  fg: theme().error
1170
1269
  }, _$p));
1171
- return _el$109;
1270
+ return _el$108;
1172
1271
  })(), (() => {
1173
- var _el$110 = _$createElement("span"), _el$111 = _$createTextNode(` \u2014 `);
1174
- _$insertNode(_el$110, _el$111);
1175
- _$insert(_el$110, () => goal().blocker.needed.slice(0, 60), null);
1176
- _$effect((_$p) => _$setProp(_el$110, "style", {
1272
+ var _el$109 = _$createElement("span"), _el$110 = _$createTextNode(` \u2014 `);
1273
+ _$insertNode(_el$109, _el$110);
1274
+ _$insert(_el$109, () => goal().blocker.needed.slice(0, 60), null);
1275
+ _$effect((_$p) => _$setProp(_el$109, "style", {
1177
1276
  fg: theme().textMuted
1178
1277
  }, _$p));
1179
- return _el$110;
1278
+ return _el$109;
1180
1279
  })()];
1181
1280
  })(), null);
1182
- _$insert(_el$87, (() => {
1281
+ _$insert(_el$86, (() => {
1183
1282
  var _c$0 = _$memo(() => !!goal().config.artifactDir);
1184
1283
  return () => _c$0() && [(() => {
1185
- var _el$112 = _$createElement("span"), _el$113 = _$createTextNode(`
1284
+ var _el$111 = _$createElement("span"), _el$112 = _$createTextNode(`
1186
1285
  \uD83D\uDCC1 `);
1187
- _$insertNode(_el$112, _el$113);
1188
- _$effect((_$p) => _$setProp(_el$112, "style", {
1286
+ _$insertNode(_el$111, _el$112);
1287
+ _$effect((_$p) => _$setProp(_el$111, "style", {
1189
1288
  fg: theme().accent
1190
1289
  }, _$p));
1191
- return _el$112;
1290
+ return _el$111;
1192
1291
  })(), (() => {
1193
- var _el$115 = _$createElement("span");
1194
- _$insert(_el$115, () => String(goal().config.artifactDir).replace(String(props.directory), "."));
1195
- _$effect((_$p) => _$setProp(_el$115, "style", {
1292
+ var _el$114 = _$createElement("span");
1293
+ _$insert(_el$114, () => String(goal().config.artifactDir).replace(String(props.directory), "."));
1294
+ _$effect((_$p) => _$setProp(_el$114, "style", {
1196
1295
  fg: theme().textMuted
1197
1296
  }, _$p));
1198
- return _el$115;
1297
+ return _el$114;
1199
1298
  })()];
1200
1299
  })(), null);
1201
- _$insert(_el$87, (() => {
1300
+ _$insert(_el$86, (() => {
1202
1301
  var _c$1 = _$memo(() => !!rt()?.lastError);
1203
1302
  return () => _c$1() && [(() => {
1204
- var _el$116 = _$createElement("span"), _el$117 = _$createTextNode(`
1303
+ var _el$115 = _$createElement("span"), _el$116 = _$createTextNode(`
1205
1304
  \u26A0 `);
1206
- _$insertNode(_el$116, _el$117);
1207
- _$effect((_$p) => _$setProp(_el$116, "style", {
1305
+ _$insertNode(_el$115, _el$116);
1306
+ _$effect((_$p) => _$setProp(_el$115, "style", {
1208
1307
  fg: theme().error
1209
1308
  }, _$p));
1210
- return _el$116;
1309
+ return _el$115;
1211
1310
  })(), (() => {
1212
- var _el$119 = _$createElement("span");
1213
- _$insert(_el$119, () => rt().lastError.slice(0, 120));
1214
- _$effect((_$p) => _$setProp(_el$119, "style", {
1311
+ var _el$118 = _$createElement("span");
1312
+ _$insert(_el$118, () => rt().lastError.slice(0, 120));
1313
+ _$effect((_$p) => _$setProp(_el$118, "style", {
1215
1314
  fg: theme().error
1216
1315
  }, _$p));
1217
- return _el$119;
1316
+ return _el$118;
1218
1317
  })()];
1219
1318
  })(), null);
1220
1319
  _$effect((_p$) => {
@@ -1226,10 +1325,10 @@ function LoopDashboard(props) {
1226
1325
  }, _v$38 = {
1227
1326
  fg: theme().text
1228
1327
  };
1229
- _v$35 !== _p$.e && (_p$.e = _$setProp(_el$86, "borderColor", _v$35, _p$.e));
1230
- _v$36 !== _p$.t && (_p$.t = _$setProp(_el$88, "style", _v$36, _p$.t));
1231
- _v$37 !== _p$.a && (_p$.a = _$setProp(_el$90, "style", _v$37, _p$.a));
1232
- _v$38 !== _p$.o && (_p$.o = _$setProp(_el$93, "style", _v$38, _p$.o));
1328
+ _v$35 !== _p$.e && (_p$.e = _$setProp(_el$85, "borderColor", _v$35, _p$.e));
1329
+ _v$36 !== _p$.t && (_p$.t = _$setProp(_el$87, "style", _v$36, _p$.t));
1330
+ _v$37 !== _p$.a && (_p$.a = _$setProp(_el$89, "style", _v$37, _p$.a));
1331
+ _v$38 !== _p$.o && (_p$.o = _$setProp(_el$92, "style", _v$38, _p$.o));
1233
1332
  return _p$;
1234
1333
  }, {
1235
1334
  e: undefined,
@@ -1237,7 +1336,7 @@ function LoopDashboard(props) {
1237
1336
  a: undefined,
1238
1337
  o: undefined
1239
1338
  });
1240
- return _el$86;
1339
+ return _el$85;
1241
1340
  })();
1242
1341
  }
1243
1342
  }), null);
@@ -1263,22 +1362,22 @@ function LoopDashboard(props) {
1263
1362
  return events().slice(-10);
1264
1363
  },
1265
1364
  children: (ev) => (() => {
1266
- var _el$120 = _$createElement("text"), _el$121 = _$createElement("span"), _el$122 = _$createElement("span"), _el$123 = _$createTextNode(` `);
1267
- _$insertNode(_el$120, _el$121);
1268
- _$insertNode(_el$120, _el$122);
1269
- _$insert(_el$121, () => String(ev.type));
1270
- _$insertNode(_el$122, _el$123);
1271
- _$insert(_el$122, () => ev.goalID?.slice(0, 8), null);
1272
- _$insert(_el$120, (() => {
1365
+ var _el$119 = _$createElement("text"), _el$120 = _$createElement("span"), _el$121 = _$createElement("span"), _el$122 = _$createTextNode(` `);
1366
+ _$insertNode(_el$119, _el$120);
1367
+ _$insertNode(_el$119, _el$121);
1368
+ _$insert(_el$120, () => String(ev.type));
1369
+ _$insertNode(_el$121, _el$122);
1370
+ _$insert(_el$121, () => ev.goalID?.slice(0, 8), null);
1371
+ _$insert(_el$119, (() => {
1273
1372
  var _c$10 = _$memo(() => !!ev.summary);
1274
1373
  return () => _c$10() && (() => {
1275
- var _el$124 = _$createElement("span"), _el$125 = _$createTextNode(` \u2014 `);
1276
- _$insertNode(_el$124, _el$125);
1277
- _$insert(_el$124, () => String(ev.summary).slice(0, 60), null);
1278
- _$effect((_$p) => _$setProp(_el$124, "style", {
1374
+ var _el$123 = _$createElement("span"), _el$124 = _$createTextNode(` \u2014 `);
1375
+ _$insertNode(_el$123, _el$124);
1376
+ _$insert(_el$123, () => String(ev.summary).slice(0, 60), null);
1377
+ _$effect((_$p) => _$setProp(_el$123, "style", {
1279
1378
  fg: theme().text
1280
1379
  }, _$p));
1281
- return _el$124;
1380
+ return _el$123;
1282
1381
  })();
1283
1382
  })(), null);
1284
1383
  _$effect((_p$) => {
@@ -1288,14 +1387,14 @@ function LoopDashboard(props) {
1288
1387
  }, _v$40 = {
1289
1388
  fg: theme().textMuted
1290
1389
  };
1291
- _v$39 !== _p$.e && (_p$.e = _$setProp(_el$121, "style", _v$39, _p$.e));
1292
- _v$40 !== _p$.t && (_p$.t = _$setProp(_el$122, "style", _v$40, _p$.t));
1390
+ _v$39 !== _p$.e && (_p$.e = _$setProp(_el$120, "style", _v$39, _p$.e));
1391
+ _v$40 !== _p$.t && (_p$.t = _$setProp(_el$121, "style", _v$40, _p$.t));
1293
1392
  return _p$;
1294
1393
  }, {
1295
1394
  e: undefined,
1296
1395
  t: undefined
1297
1396
  });
1298
- return _el$120;
1397
+ return _el$119;
1299
1398
  })()
1300
1399
  }), null);
1301
1400
  _$effect((_p$) => {
@@ -1464,7 +1563,7 @@ var tui = async (api) => {
1464
1563
  run: open
1465
1564
  }],
1466
1565
  bindings: [{
1467
- key: "ctrl+l",
1566
+ key: "<leader>d",
1468
1567
  cmd: "opencode.loopd.dashboard",
1469
1568
  desc: "Open loop dashboard"
1470
1569
  }]
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@bojackduy/opencode-loopd",
4
- "version": "1.0.0",
5
- "description": "Codex-inspired background goal engine for OpenCode — autonomous child sessions, engine-driven loop, and modal TUI dashboard",
4
+ "version": "1.1.0",
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-only",
8
8
  "private": false,
@@ -21,9 +21,13 @@
21
21
  "loop",
22
22
  "goal",
23
23
  "background",
24
+ "background-agent",
24
25
  "automation",
25
- "worker",
26
+ "autonomous",
27
+ "agent",
26
28
  "subagent",
29
+ "codex",
30
+ "claude-code",
27
31
  "opencode-loopd"
28
32
  ],
29
33
  "bin": {
@@ -34,6 +38,7 @@
34
38
  "scripts",
35
39
  "skills",
36
40
  "commands",
41
+ "assets",
37
42
  "README.md",
38
43
  "LICENSE",
39
44
  "CHANGELOG.md"
@@ -45,6 +50,9 @@
45
50
  "bun": ">=1.1.0"
46
51
  },
47
52
  "exports": {
53
+ ".": {
54
+ "import": "./dist/server.js"
55
+ },
48
56
  "./server": {
49
57
  "import": "./dist/server.js"
50
58
  },
@@ -51,7 +51,7 @@ loopd_create_goal({
51
51
  })
52
52
  ```
53
53
 
54
- The goal starts immediately. The user can monitor it via `/loop` (Ctrl+L).
54
+ The goal starts immediately. The user can monitor it via `/loop` (<leader>d).
55
55
 
56
56
  ## Worker Tools (Running Inside the Goal)
57
57
 
@@ -131,7 +131,7 @@ Sends a message to the worker's next turn. See table above.
131
131
 
132
132
  ## Dashboard Commands
133
133
 
134
- Open the dashboard with `/loop` or Ctrl+L.
134
+ Open the dashboard with `/loop` or <leader>d.
135
135
 
136
136
  ### Keyboard Shortcuts (Normal Mode)
137
137