@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 +180 -67
- package/assets/demo.png +0 -0
- package/commands/goal.md +1 -1
- package/dist/server.js +16 -16
- package/dist/tui.js +295 -196
- package/package.json +11 -3
- package/skills/loopd/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -1,137 +1,250 @@
|
|
|
1
1
|
# opencode-loopd
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
+
[](https://www.npmjs.com/package/@bojackduy/opencode-loopd)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://opencode.ai)
|
|
10
|
+
[](https://github.com/anomalyco/opentui)
|
|
11
|
+
|
|
12
|
+

|
|
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
|
|
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
|
-
###
|
|
32
|
+
### Plugin — simple (no installer, just the plugin)
|
|
12
33
|
|
|
13
|
-
Add to
|
|
34
|
+
Add the package to **both** configs.
|
|
14
35
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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` —
|
|
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
|
-
-
|
|
62
|
-
-
|
|
63
|
-
- `Ctrl+N` —
|
|
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
|
-
|
|
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
|
|
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() —
|
|
74
|
-
send_goal_input(goalID, msg) —
|
|
75
|
-
pause_goal(goalID)
|
|
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
|
|
158
|
+
The child sees:
|
|
83
159
|
|
|
84
|
-
- `get_goal` — read objective,
|
|
85
|
-
- `report_goal_progress` —
|
|
86
|
-
- `complete_goal` —
|
|
87
|
-
- `block_goal` —
|
|
88
|
-
- `question` (OpenCode builtin) —
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
102
|
-
- **
|
|
103
|
-
- **
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
224
|
+
Goals accept:
|
|
125
225
|
|
|
126
226
|
| Option | Default | Description |
|
|
127
227
|
|---|---|---|
|
|
128
|
-
| `maxTurns` | 50 | Max continuation turns |
|
|
129
|
-
| `maxFailures` | 3 |
|
|
130
|
-
| `maxNoProgress` | 3 | Turns without progress before
|
|
131
|
-
| `timeoutMs` | 300000 | Per-turn
|
|
132
|
-
| `compactEvery` | — | Compact child
|
|
133
|
-
| `checks` | [] | Shell commands that must pass for
|
|
134
|
-
| `progressFile` |
|
|
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
|
|
package/assets/demo.png
ADDED
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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$
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
_$insert(_el$
|
|
848
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
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$
|
|
874
|
-
_$
|
|
875
|
-
_$
|
|
876
|
-
_$insertNode(_el$
|
|
877
|
-
_$insertNode(_el$
|
|
878
|
-
_$insertNode(_el$
|
|
879
|
-
_$insertNode(_el$
|
|
880
|
-
_$insertNode(_el$
|
|
881
|
-
_$insertNode(_el$
|
|
882
|
-
_$insertNode(_el$
|
|
883
|
-
_$insertNode(_el$
|
|
884
|
-
_$insertNode(_el$
|
|
885
|
-
_$insertNode(_el$51,
|
|
886
|
-
_$insertNode(_el$
|
|
887
|
-
_$insertNode(_el$
|
|
888
|
-
_$insertNode(_el$
|
|
889
|
-
_$insertNode(_el$
|
|
890
|
-
_$insertNode(_el$
|
|
891
|
-
_$insertNode(_el$
|
|
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$
|
|
919
|
-
_v$22 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
920
|
-
_v$23 !== _p$.a && (_p$.a = _$setProp(_el$
|
|
921
|
-
_v$24 !== _p$.o && (_p$.o = _$setProp(_el$
|
|
922
|
-
_v$25 !== _p$.i && (_p$.i = _$setProp(_el$
|
|
923
|
-
_v$26 !== _p$.n && (_p$.n = _$setProp(_el$
|
|
924
|
-
_v$27 !== _p$.s && (_p$.s = _$setProp(_el$
|
|
925
|
-
_v$28 !== _p$.h && (_p$.h = _$setProp(_el$
|
|
926
|
-
_v$29 !== _p$.r && (_p$.r = _$setProp(_el$
|
|
927
|
-
_v$30 !== _p$.d && (_p$.d = _$setProp(_el$
|
|
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$
|
|
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$
|
|
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
|
-
_$
|
|
967
|
-
_$
|
|
968
|
-
_$
|
|
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$
|
|
977
|
-
_$insert(_el$
|
|
978
|
-
_$insert(_el$
|
|
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$
|
|
982
|
-
_$insertNode(_el$
|
|
983
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1085
|
+
return _el$71;
|
|
987
1086
|
})(), (() => {
|
|
988
|
-
var _el$
|
|
989
|
-
_$insertNode(_el$
|
|
990
|
-
_$insert(_el$
|
|
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$
|
|
994
|
-
_$insert(_el$
|
|
995
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1098
|
+
return _el$73;
|
|
1000
1099
|
})(), (() => {
|
|
1001
|
-
var _el$
|
|
1002
|
-
_$insertNode(_el$
|
|
1003
|
-
_$insert(_el$
|
|
1004
|
-
_$insert(_el$
|
|
1005
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1107
|
+
return _el$75;
|
|
1009
1108
|
})(), (() => {
|
|
1010
|
-
var _el$
|
|
1011
|
-
_$insertNode(_el$
|
|
1012
|
-
_$insert(_el$
|
|
1013
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1115
|
+
return _el$77;
|
|
1017
1116
|
})()];
|
|
1018
1117
|
})(), null);
|
|
1019
|
-
_$insert(_el$
|
|
1118
|
+
_$insert(_el$66, (() => {
|
|
1020
1119
|
var _c$4 = _$memo(() => !!(runtime() && runtime().consecutiveFailures > 0));
|
|
1021
1120
|
return () => _c$4() && (() => {
|
|
1022
|
-
var _el$
|
|
1023
|
-
_$insertNode(_el$
|
|
1024
|
-
_$insertNode(_el$
|
|
1025
|
-
_$insert(_el$
|
|
1026
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1129
|
+
return _el$79;
|
|
1031
1130
|
})();
|
|
1032
1131
|
})(), null);
|
|
1033
|
-
_$insert(_el$
|
|
1132
|
+
_$insert(_el$66, (() => {
|
|
1034
1133
|
var _c$5 = _$memo(() => !!(runtime() && (runtime().noProgressCount || 0) > 0));
|
|
1035
1134
|
return () => _c$5() && (() => {
|
|
1036
|
-
var _el$
|
|
1037
|
-
_$insertNode(_el$
|
|
1038
|
-
_$insertNode(_el$
|
|
1039
|
-
_$insert(_el$
|
|
1040
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
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$
|
|
1057
|
-
_v$32 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
1058
|
-
_v$33 !== _p$.a && (_p$.a = _$setProp(_el$
|
|
1059
|
-
_v$34 !== _p$.o && (_p$.o = _$setProp(_el$
|
|
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$
|
|
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$
|
|
1081
|
-
`), _el$
|
|
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
|
-
_$
|
|
1084
|
-
_$
|
|
1085
|
-
_$
|
|
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
|
-
_$
|
|
1090
|
-
_$
|
|
1091
|
-
_$insertNode(_el$
|
|
1092
|
-
_$
|
|
1093
|
-
_$insert(_el$
|
|
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$
|
|
1101
|
-
_$insertNode(_el$
|
|
1102
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1204
|
+
return _el$93;
|
|
1106
1205
|
})(), (() => {
|
|
1107
|
-
var _el$
|
|
1108
|
-
_$insertNode(_el$
|
|
1109
|
-
_$insert(_el$
|
|
1110
|
-
_$insert(_el$
|
|
1111
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1214
|
+
return _el$95;
|
|
1116
1215
|
})(), (() => {
|
|
1117
|
-
var _el$
|
|
1118
|
-
_$insertNode(_el$
|
|
1119
|
-
_$insert(_el$
|
|
1120
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1222
|
+
return _el$97;
|
|
1124
1223
|
})()];
|
|
1125
|
-
})(), _el$
|
|
1126
|
-
_$insert(_el$
|
|
1127
|
-
_$insert(_el$
|
|
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$
|
|
1229
|
+
var _el$99 = _$createElement("span"), _el$100 = _$createTextNode(`
|
|
1131
1230
|
\u2714 `);
|
|
1132
|
-
_$insertNode(_el$
|
|
1133
|
-
_$effect((_$p) => _$setProp(_el$
|
|
1231
|
+
_$insertNode(_el$99, _el$100);
|
|
1232
|
+
_$effect((_$p) => _$setProp(_el$99, "style", {
|
|
1134
1233
|
fg: theme().success
|
|
1135
1234
|
}, _$p));
|
|
1136
|
-
return _el$
|
|
1235
|
+
return _el$99;
|
|
1137
1236
|
})(), (() => {
|
|
1138
|
-
var _el$
|
|
1139
|
-
_$insert(_el$
|
|
1140
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1242
|
+
return _el$102;
|
|
1144
1243
|
})(), (() => {
|
|
1145
|
-
var _el$
|
|
1146
|
-
_$insertNode(_el$
|
|
1147
|
-
_$insert(_el$
|
|
1148
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1250
|
+
return _el$103;
|
|
1152
1251
|
})()];
|
|
1153
1252
|
})(), null);
|
|
1154
|
-
_$insert(_el$
|
|
1253
|
+
_$insert(_el$86, (() => {
|
|
1155
1254
|
var _c$9 = _$memo(() => !!goal().blocker);
|
|
1156
1255
|
return () => _c$9() && [(() => {
|
|
1157
|
-
var _el$
|
|
1256
|
+
var _el$105 = _$createElement("span"), _el$106 = _$createTextNode(`
|
|
1158
1257
|
\u2716 blocked: `);
|
|
1159
|
-
_$insertNode(_el$
|
|
1160
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1263
|
+
return _el$105;
|
|
1165
1264
|
})(), (() => {
|
|
1166
|
-
var _el$
|
|
1167
|
-
_$insert(_el$
|
|
1168
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1270
|
+
return _el$108;
|
|
1172
1271
|
})(), (() => {
|
|
1173
|
-
var _el$
|
|
1174
|
-
_$insertNode(_el$
|
|
1175
|
-
_$insert(_el$
|
|
1176
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1278
|
+
return _el$109;
|
|
1180
1279
|
})()];
|
|
1181
1280
|
})(), null);
|
|
1182
|
-
_$insert(_el$
|
|
1281
|
+
_$insert(_el$86, (() => {
|
|
1183
1282
|
var _c$0 = _$memo(() => !!goal().config.artifactDir);
|
|
1184
1283
|
return () => _c$0() && [(() => {
|
|
1185
|
-
var _el$
|
|
1284
|
+
var _el$111 = _$createElement("span"), _el$112 = _$createTextNode(`
|
|
1186
1285
|
\uD83D\uDCC1 `);
|
|
1187
|
-
_$insertNode(_el$
|
|
1188
|
-
_$effect((_$p) => _$setProp(_el$
|
|
1286
|
+
_$insertNode(_el$111, _el$112);
|
|
1287
|
+
_$effect((_$p) => _$setProp(_el$111, "style", {
|
|
1189
1288
|
fg: theme().accent
|
|
1190
1289
|
}, _$p));
|
|
1191
|
-
return _el$
|
|
1290
|
+
return _el$111;
|
|
1192
1291
|
})(), (() => {
|
|
1193
|
-
var _el$
|
|
1194
|
-
_$insert(_el$
|
|
1195
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
1297
|
+
return _el$114;
|
|
1199
1298
|
})()];
|
|
1200
1299
|
})(), null);
|
|
1201
|
-
_$insert(_el$
|
|
1300
|
+
_$insert(_el$86, (() => {
|
|
1202
1301
|
var _c$1 = _$memo(() => !!rt()?.lastError);
|
|
1203
1302
|
return () => _c$1() && [(() => {
|
|
1204
|
-
var _el$
|
|
1303
|
+
var _el$115 = _$createElement("span"), _el$116 = _$createTextNode(`
|
|
1205
1304
|
\u26A0 `);
|
|
1206
|
-
_$insertNode(_el$
|
|
1207
|
-
_$effect((_$p) => _$setProp(_el$
|
|
1305
|
+
_$insertNode(_el$115, _el$116);
|
|
1306
|
+
_$effect((_$p) => _$setProp(_el$115, "style", {
|
|
1208
1307
|
fg: theme().error
|
|
1209
1308
|
}, _$p));
|
|
1210
|
-
return _el$
|
|
1309
|
+
return _el$115;
|
|
1211
1310
|
})(), (() => {
|
|
1212
|
-
var _el$
|
|
1213
|
-
_$insert(_el$
|
|
1214
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
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$
|
|
1230
|
-
_v$36 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
1231
|
-
_v$37 !== _p$.a && (_p$.a = _$setProp(_el$
|
|
1232
|
-
_v$38 !== _p$.o && (_p$.o = _$setProp(_el$
|
|
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$
|
|
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$
|
|
1267
|
-
_$insertNode(_el$
|
|
1268
|
-
_$insertNode(_el$
|
|
1269
|
-
_$insert(_el$
|
|
1270
|
-
_$insertNode(_el$
|
|
1271
|
-
_$insert(_el$
|
|
1272
|
-
_$insert(_el$
|
|
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$
|
|
1276
|
-
_$insertNode(_el$
|
|
1277
|
-
_$insert(_el$
|
|
1278
|
-
_$effect((_$p) => _$setProp(_el$
|
|
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$
|
|
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$
|
|
1292
|
-
_v$40 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
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$
|
|
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: "
|
|
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.
|
|
5
|
-
"description": "Codex-inspired background goal engine for OpenCode — autonomous
|
|
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
|
-
"
|
|
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
|
},
|
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
|
|