@bojackduy/opencode-loopd 1.0.1 → 1.2.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 +29 -12
- package/commands/goal.md +1 -1
- package/dist/server.js +1 -1
- package/dist/tui.js +102 -3
- package/package.json +2 -2
- package/skills/loopd/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|

|
|
13
13
|
|
|
14
|
-
*Modal dashboard (`/loop` / `
|
|
14
|
+
*Modal dashboard (`/loop` / `<leader>d`): zero chat pollution — keys are trapped inside the dialog, NORMAL vs INSERT modes, vivid per-status coloring.*
|
|
15
15
|
|
|
16
16
|
## Why opencode-loopd
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
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
20
|
- **Parent ↔ child visibility** — `list/inspect/read_transcript/send_input` give the parent full observability. Bidirectional inbox lets you steer mid-run.
|
|
21
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** — `
|
|
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
23
|
|
|
24
24
|
Keywords: `opencode` `opencode-plugin` `background-agent` `autonomous` `subagent` `loop` `goal` `tui` `codex` `claude-code` `worker`
|
|
25
25
|
|
|
@@ -49,7 +49,7 @@ Add the package to **both** configs.
|
|
|
49
49
|
}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
Then restart OpenCode. Verify with `/loop` (palette → Loop Dashboard) or
|
|
52
|
+
Then restart OpenCode. Verify with `/loop` (palette → Loop Dashboard) or `<leader>d`.
|
|
53
53
|
|
|
54
54
|
For a local checkout:
|
|
55
55
|
|
|
@@ -126,7 +126,7 @@ The agent will clarify (what/where/how to verify) and then call `loopd_create_go
|
|
|
126
126
|
|
|
127
127
|
### 2. Monitor with dashboard — `/loop`
|
|
128
128
|
|
|
129
|
-
Press
|
|
129
|
+
Press **`<leader>d`** or open the command palette → **"Loop Dashboard"** (also `/loop`).
|
|
130
130
|
|
|
131
131
|
Dashboard (NORMAL / INSERT `:`):
|
|
132
132
|
|
|
@@ -168,14 +168,31 @@ Completion is semantic: the child writes the summary; the plugin forwards it to
|
|
|
168
168
|
## Architecture
|
|
169
169
|
|
|
170
170
|
```
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
└─────────────────────────────────┘
|
|
179
196
|
```
|
|
180
197
|
|
|
181
198
|
- **Engine-driven loop** (like Codex): `session.idle` → lease + retry + polling → re-prompt child. Not a parent-driven re-prompt machine.
|
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
|
|
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
|
@@ -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 (
|
|
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,7 +927,7 @@ 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
|
|
@@ -1464,7 +1563,7 @@ var tui = async (api) => {
|
|
|
1464
1563
|
run: open
|
|
1465
1564
|
}],
|
|
1466
1565
|
bindings: [{
|
|
1467
|
-
key: "
|
|
1566
|
+
key: "<leader>d",
|
|
1468
1567
|
cmd: "opencode.loopd.dashboard",
|
|
1469
1568
|
desc: "Open loop dashboard"
|
|
1470
1569
|
}]
|
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.0
|
|
4
|
+
"version": "1.2.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-only",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/bojackduy/opencode-loopd.git"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://github.
|
|
13
|
+
"homepage": "https://bojackduy.github.io/opencode-loopd/",
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/bojackduy/opencode-loopd/issues"
|
|
16
16
|
},
|
package/skills/loopd/SKILL.md
CHANGED
|
@@ -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` (
|
|
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
|
|
134
|
+
Open the dashboard with `/loop` or <leader>d.
|
|
135
135
|
|
|
136
136
|
### Keyboard Shortcuts (Normal Mode)
|
|
137
137
|
|