opencode-agent-dock 1.0.0 → 1.0.1
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 +6 -20
- package/dist/adapters/opencode-logger.js +17 -0
- package/{src/adapters/opencode-subagent-store.ts → dist/adapters/opencode-subagent-store.js} +24 -59
- package/{src/constants.ts → dist/constants.js} +2 -3
- package/{src/domain.ts → dist/domain.js} +13 -37
- package/{src/index.tsx → dist/index.js} +67 -74
- package/dist/ports.js +1 -0
- package/{src/sdk.ts → dist/sdk.js} +4 -9
- package/dist/view/Dock.js +88 -0
- package/package.json +13 -4
- package/src/adapters/opencode-logger.ts +0 -18
- package/src/domain.test.ts +0 -145
- package/src/ports.ts +0 -24
- package/src/view/Dock.tsx +0 -72
package/README.md
CHANGED
|
@@ -62,38 +62,24 @@ The toggle key (`<leader>v`) can be remapped in `tui.json` under the `agent-dock
|
|
|
62
62
|
|
|
63
63
|
> Why a toggle and not bare `↓`? In the parent session the prompt textarea captures arrow keys, and a plugin cannot blur the host prompt. `keymap.intercept` runs before the textarea, so once the dock is focused the arrows are ours. `↓` itself is also the prompt's history key, which made it an unreliable trigger.
|
|
64
64
|
|
|
65
|
-
## Architecture
|
|
66
|
-
|
|
67
|
-
Clean Architecture — dependencies point inward, the domain is pure, and the only place that knows about the opencode SDK / TUI API is the composition root + adapters.
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
src/
|
|
71
|
-
domain.ts pure: Subagent, selection math, formatting (unit-tested)
|
|
72
|
-
ports.ts SubagentStore, Logger contracts
|
|
73
|
-
sdk.ts SDK Session/SessionStatus -> domain Subagent
|
|
74
|
-
constants.ts plugin id, slot name, limits
|
|
75
|
-
adapters/
|
|
76
|
-
opencode-subagent-store.ts SubagentStore via client.session.children + events
|
|
77
|
-
opencode-logger.ts Logger via client.app.log
|
|
78
|
-
view/
|
|
79
|
-
Dock.tsx Solid render into the app_bottom slot
|
|
80
|
-
index.tsx composition root: slot + inline nav (intercept)
|
|
81
|
-
```
|
|
82
|
-
|
|
83
65
|
## Develop
|
|
84
66
|
|
|
85
67
|
```sh
|
|
86
68
|
npm install
|
|
87
|
-
npm test # vitest
|
|
69
|
+
npm test # builds dist/, then vitest: domain unit tests + the packed tarball loaded as OpenCode loads it
|
|
88
70
|
npm run typecheck
|
|
89
71
|
```
|
|
90
72
|
|
|
91
|
-
OpenCode
|
|
73
|
+
The package ships `dist/`, compiled by `npm run build` with the same Solid transform OpenCode applies to local plugins. Raw `.tsx` can't ship: OpenCode compiles none of the files under `node_modules`, where an npm install lives (1.0.0 shipped `src/` and never loaded from npm).
|
|
74
|
+
|
|
75
|
+
For local development a file plugin needs no build, since it is not under `node_modules`:
|
|
92
76
|
|
|
93
77
|
```jsonc
|
|
94
78
|
{ "plugin": [["file:///absolute/path/to/opencode-agent-dock/src/index.tsx"]] }
|
|
95
79
|
```
|
|
96
80
|
|
|
81
|
+
CI runs the typecheck and tests on every pull request and every push to `main`.
|
|
82
|
+
|
|
97
83
|
## License
|
|
98
84
|
|
|
99
85
|
MIT © Artem Bambalov
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Adapter — Logger over the opencode client app log endpoint.
|
|
2
|
+
|
|
3
|
+
import { SERVICE } from "../constants.js";
|
|
4
|
+
export function createLogger(client) {
|
|
5
|
+
const write = (level, message) => {
|
|
6
|
+
client.app.log({
|
|
7
|
+
service: SERVICE,
|
|
8
|
+
level,
|
|
9
|
+
message
|
|
10
|
+
}).catch(() => {});
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
info: m => write("info", m),
|
|
14
|
+
warn: m => write("warn", m),
|
|
15
|
+
error: m => write("error", m)
|
|
16
|
+
};
|
|
17
|
+
}
|
package/{src/adapters/opencode-subagent-store.ts → dist/adapters/opencode-subagent-store.js}
RENAMED
|
@@ -8,50 +8,30 @@
|
|
|
8
8
|
// the panel; native left/right between siblings updates it). `activeId`
|
|
9
9
|
// exposes that focused id for highlighting.
|
|
10
10
|
|
|
11
|
-
import type { Session } from "@opencode-ai/sdk/v2";
|
|
12
|
-
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
13
|
-
import type { Subagent } from "../domain.js";
|
|
14
11
|
import { sortSubagents } from "../domain.js";
|
|
15
12
|
import { projectSubagent } from "../sdk.js";
|
|
16
|
-
import type { Logger, SubagentStore } from "../ports.js";
|
|
17
13
|
import { LIMITS } from "../constants.js";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
children(parameters: {
|
|
22
|
-
sessionID: string;
|
|
23
|
-
directory?: string;
|
|
24
|
-
workspace?: string;
|
|
25
|
-
}): Promise<unknown>;
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function extractSessions(result: unknown): Session[] {
|
|
30
|
-
const data = (result as { data?: unknown } | null | undefined)?.data;
|
|
31
|
-
return Array.isArray(data) ? (data as Session[]) : [];
|
|
14
|
+
function extractSessions(result) {
|
|
15
|
+
const data = result?.data;
|
|
16
|
+
return Array.isArray(data) ? data : [];
|
|
32
17
|
}
|
|
33
|
-
|
|
34
|
-
function isArchived(session: Session): boolean {
|
|
18
|
+
function isArchived(session) {
|
|
35
19
|
const archived = session.time.archived;
|
|
36
20
|
return typeof archived === "number" && archived > 0;
|
|
37
21
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const emit = (): void => {
|
|
22
|
+
export function createSubagentStore(api, log) {
|
|
23
|
+
const client = api.client;
|
|
24
|
+
const listeners = new Set();
|
|
25
|
+
let cache = [];
|
|
26
|
+
let activeId;
|
|
27
|
+
const emit = () => {
|
|
46
28
|
for (const handler of listeners) handler();
|
|
47
29
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const current = api.route.current as { name: string; params?: { sessionID?: string } };
|
|
30
|
+
const resolveCurrent = () => {
|
|
31
|
+
const current = api.route.current;
|
|
51
32
|
return current.name === "session" ? current.params?.sessionID : undefined;
|
|
52
33
|
};
|
|
53
|
-
|
|
54
|
-
const refresh = async (): Promise<void> => {
|
|
34
|
+
const refresh = async () => {
|
|
55
35
|
const currentId = resolveCurrent();
|
|
56
36
|
if (!currentId) {
|
|
57
37
|
if (cache.length !== 0 || activeId !== undefined) {
|
|
@@ -72,14 +52,11 @@ export function createSubagentStore(api: TuiPluginApi, log: Logger): SubagentSto
|
|
|
72
52
|
activeId = currentId;
|
|
73
53
|
} else {
|
|
74
54
|
// Parent session: list running children.
|
|
75
|
-
const result = await client.session.children({
|
|
55
|
+
const result = await client.session.children({
|
|
56
|
+
sessionID: currentId
|
|
57
|
+
});
|
|
76
58
|
const sessions = extractSessions(result);
|
|
77
|
-
cache = sortSubagents(
|
|
78
|
-
sessions
|
|
79
|
-
.filter((item) => !isArchived(item))
|
|
80
|
-
.map((item) => projectSubagent(item, api.state.session.status(item.id)))
|
|
81
|
-
.filter((subagent) => subagent.status === "running"),
|
|
82
|
-
).slice(0, LIMITS.maxRows);
|
|
59
|
+
cache = sortSubagents(sessions.filter(item => !isArchived(item)).map(item => projectSubagent(item, api.state.session.status(item.id))).filter(subagent => subagent.status === "running")).slice(0, LIMITS.maxRows);
|
|
83
60
|
activeId = undefined;
|
|
84
61
|
}
|
|
85
62
|
emit();
|
|
@@ -87,41 +64,29 @@ export function createSubagentStore(api: TuiPluginApi, log: Logger): SubagentSto
|
|
|
87
64
|
log.error(`dock refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
88
65
|
}
|
|
89
66
|
};
|
|
90
|
-
|
|
91
67
|
void refresh();
|
|
92
68
|
const interval = setInterval(() => void refresh(), LIMITS.pollMs);
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
type: string,
|
|
96
|
-
handler: () => void,
|
|
97
|
-
) => () => void;
|
|
98
|
-
const unsubs = [
|
|
99
|
-
subscribe("session.created", () => void refresh()),
|
|
100
|
-
subscribe("session.idle", () => void refresh()),
|
|
101
|
-
subscribe("session.updated", () => void refresh()),
|
|
102
|
-
subscribe("session.removed", () => void refresh()),
|
|
103
|
-
];
|
|
104
|
-
|
|
69
|
+
const subscribe = api.event.on;
|
|
70
|
+
const unsubs = [subscribe("session.created", () => void refresh()), subscribe("session.idle", () => void refresh()), subscribe("session.updated", () => void refresh()), subscribe("session.removed", () => void refresh())];
|
|
105
71
|
api.lifecycle.onDispose(() => {
|
|
106
72
|
clearInterval(interval);
|
|
107
73
|
for (const unsub of unsubs) unsub();
|
|
108
74
|
});
|
|
109
|
-
|
|
110
75
|
return {
|
|
111
|
-
get parentSessionId()
|
|
76
|
+
get parentSessionId() {
|
|
112
77
|
return resolveCurrent();
|
|
113
78
|
},
|
|
114
|
-
get activeId()
|
|
79
|
+
get activeId() {
|
|
115
80
|
return activeId;
|
|
116
81
|
},
|
|
117
|
-
snapshot()
|
|
82
|
+
snapshot() {
|
|
118
83
|
return cache;
|
|
119
84
|
},
|
|
120
|
-
onChange(handler
|
|
85
|
+
onChange(handler) {
|
|
121
86
|
listeners.add(handler);
|
|
122
87
|
return () => {
|
|
123
88
|
listeners.delete(handler);
|
|
124
89
|
};
|
|
125
|
-
}
|
|
90
|
+
}
|
|
126
91
|
};
|
|
127
92
|
}
|
|
@@ -1,54 +1,34 @@
|
|
|
1
1
|
// Domain — pure policy. No IO, no SDK, no Solid. Fully unit-testable.
|
|
2
2
|
|
|
3
|
-
export type SubagentStatus = "running" | "idle" | "done" | "error";
|
|
4
|
-
|
|
5
3
|
/** Neutral projection of a child session. SDK-shape mapping lives in sdk.ts. */
|
|
6
|
-
export type Subagent = {
|
|
7
|
-
id: string;
|
|
8
|
-
title: string;
|
|
9
|
-
status: SubagentStatus;
|
|
10
|
-
startedAt: number;
|
|
11
|
-
model?: string;
|
|
12
|
-
tokens?: number;
|
|
13
|
-
};
|
|
14
4
|
|
|
15
5
|
/** View-model row derived from a Subagent at a point in time. */
|
|
16
|
-
export type DockRow = {
|
|
17
|
-
subagent: Subagent;
|
|
18
|
-
glyph: string;
|
|
19
|
-
label: string;
|
|
20
|
-
meta: string;
|
|
21
|
-
};
|
|
22
6
|
|
|
23
|
-
const STATUS_GLYPH
|
|
7
|
+
const STATUS_GLYPH = {
|
|
24
8
|
running: "●",
|
|
25
9
|
idle: "○",
|
|
26
10
|
done: "✓",
|
|
27
|
-
error: "✗"
|
|
11
|
+
error: "✗"
|
|
28
12
|
};
|
|
29
|
-
|
|
30
|
-
const STATUS_RANK: Record<SubagentStatus, number> = {
|
|
13
|
+
const STATUS_RANK = {
|
|
31
14
|
running: 0,
|
|
32
15
|
idle: 1,
|
|
33
16
|
done: 2,
|
|
34
|
-
error: 3
|
|
17
|
+
error: 3
|
|
35
18
|
};
|
|
36
|
-
|
|
37
|
-
export function sortSubagents(list: readonly Subagent[]): Subagent[] {
|
|
19
|
+
export function sortSubagents(list) {
|
|
38
20
|
return [...list].sort((a, b) => {
|
|
39
21
|
const r = STATUS_RANK[a.status] - STATUS_RANK[b.status];
|
|
40
22
|
return r !== 0 ? r : b.startedAt - a.startedAt;
|
|
41
23
|
});
|
|
42
24
|
}
|
|
43
|
-
|
|
44
|
-
export function clampSelection(index: number, length: number): number {
|
|
25
|
+
export function clampSelection(index, length) {
|
|
45
26
|
if (length <= 0) return 0;
|
|
46
27
|
if (index < 0) return 0;
|
|
47
28
|
if (index >= length) return length - 1;
|
|
48
29
|
return index;
|
|
49
30
|
}
|
|
50
|
-
|
|
51
|
-
export function formatElapsed(startedAt: number, now: number = Date.now()): string {
|
|
31
|
+
export function formatElapsed(startedAt, now = Date.now()) {
|
|
52
32
|
const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
53
33
|
if (seconds < 60) return `${seconds}s`;
|
|
54
34
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -57,31 +37,27 @@ export function formatElapsed(startedAt: number, now: number = Date.now()): stri
|
|
|
57
37
|
const hours = Math.floor(minutes / 60);
|
|
58
38
|
return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`;
|
|
59
39
|
}
|
|
60
|
-
|
|
61
|
-
export function formatTokens(tokens: number | undefined): string {
|
|
40
|
+
export function formatTokens(tokens) {
|
|
62
41
|
if (tokens === undefined || tokens <= 0) return "";
|
|
63
42
|
if (tokens < 1000) return `${tokens}`;
|
|
64
43
|
return `${(tokens / 1000).toFixed(1)}k`;
|
|
65
44
|
}
|
|
66
|
-
|
|
67
|
-
export function truncate(text: string, max: number): string {
|
|
45
|
+
export function truncate(text, max) {
|
|
68
46
|
const trimmed = text.trim();
|
|
69
47
|
if (trimmed.length <= max) return trimmed;
|
|
70
48
|
return trimmed.slice(0, Math.max(0, max - 1)).trimEnd() + "…";
|
|
71
49
|
}
|
|
72
|
-
|
|
73
|
-
export function isTerminal(status: SubagentStatus): boolean {
|
|
50
|
+
export function isTerminal(status) {
|
|
74
51
|
return status === "done" || status === "error";
|
|
75
52
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const meta: string[] = [];
|
|
53
|
+
export function toRow(subagent, now, labelMax) {
|
|
54
|
+
const meta = [];
|
|
79
55
|
if (!isTerminal(subagent.status)) meta.push(formatElapsed(subagent.startedAt, now));
|
|
80
56
|
if (subagent.tokens !== undefined) meta.push(formatTokens(subagent.tokens));
|
|
81
57
|
return {
|
|
82
58
|
subagent,
|
|
83
59
|
glyph: STATUS_GLYPH[subagent.status],
|
|
84
60
|
label: truncate(subagent.title || subagent.id, labelMax),
|
|
85
|
-
meta: meta.join(" \u00b7 ")
|
|
61
|
+
meta: meta.join(" \u00b7 ")
|
|
86
62
|
};
|
|
87
63
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
1
2
|
/** @jsxImportSource @opentui/solid */
|
|
2
3
|
// Composition root.
|
|
3
4
|
//
|
|
@@ -13,29 +14,19 @@
|
|
|
13
14
|
// Toggle: `<leader>v` (ctrl+x then v) or `/subagents` — ctrl+x itself is left
|
|
14
15
|
// untouched (it is opencode's leader key).
|
|
15
16
|
|
|
16
|
-
import type { TuiPlugin, TuiPluginModule, TuiSlotPlugin } from "@opencode-ai/plugin/tui";
|
|
17
17
|
import { createSignal } from "solid-js";
|
|
18
18
|
import { PLUGIN_ID } from "./constants.js";
|
|
19
19
|
import { clampSelection } from "./domain.js";
|
|
20
20
|
import { createSubagentStore } from "./adapters/opencode-subagent-store.js";
|
|
21
21
|
import { createLogger } from "./adapters/opencode-logger.js";
|
|
22
22
|
import { Dock } from "./view/Dock.js";
|
|
23
|
-
|
|
24
|
-
type InterceptEvent = {
|
|
25
|
-
name?: string;
|
|
26
|
-
ctrl?: boolean;
|
|
27
|
-
preventDefault: () => void;
|
|
28
|
-
stopPropagation: () => void;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const tui: TuiPlugin = async (api) => {
|
|
23
|
+
const tui = async api => {
|
|
32
24
|
const log = createLogger(api.client);
|
|
33
25
|
const store = createSubagentStore(api, log);
|
|
34
26
|
log.info("agent-dock initialized");
|
|
35
|
-
|
|
36
27
|
const [selection, setSelection] = createSignal(0);
|
|
37
28
|
const [focused, setFocused] = createSignal(false);
|
|
38
|
-
const [activeId, setActiveId] = createSignal
|
|
29
|
+
const [activeId, setActiveId] = createSignal(undefined);
|
|
39
30
|
|
|
40
31
|
// Mirror the store's active id + keep selection in range as rows change; leave
|
|
41
32
|
// the dock if the roster empties.
|
|
@@ -49,25 +40,27 @@ const tui: TuiPlugin = async (api) => {
|
|
|
49
40
|
const clamped = clampSelection(selection(), rows.length);
|
|
50
41
|
if (clamped !== selection()) setSelection(clamped);
|
|
51
42
|
});
|
|
52
|
-
|
|
53
|
-
const slot: TuiSlotPlugin = {
|
|
43
|
+
const slot = {
|
|
54
44
|
slots: {
|
|
55
|
-
app_bottom:
|
|
45
|
+
app_bottom: ctx => {
|
|
56
46
|
const tone = ctx.theme.current;
|
|
57
|
-
return (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
return _$createComponent(Dock, {
|
|
48
|
+
store: store,
|
|
49
|
+
selection: selection,
|
|
50
|
+
focused: focused,
|
|
51
|
+
activeId: activeId,
|
|
52
|
+
get colors() {
|
|
53
|
+
return {
|
|
54
|
+
text: tone.text,
|
|
55
|
+
muted: tone.textMuted,
|
|
56
|
+
accent: tone.primary
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
68
62
|
};
|
|
69
63
|
api.slots.register(slot);
|
|
70
|
-
|
|
71
64
|
const enterDock = () => {
|
|
72
65
|
if (store.snapshot().length === 0) return;
|
|
73
66
|
setSelection(0);
|
|
@@ -78,65 +71,65 @@ const tui: TuiPlugin = async (api) => {
|
|
|
78
71
|
};
|
|
79
72
|
|
|
80
73
|
// Inline navigation: intercept arrows/enter while the dock is focused.
|
|
81
|
-
api.keymap.intercept(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
74
|
+
api.keymap.intercept("key", ({
|
|
75
|
+
event
|
|
76
|
+
}) => {
|
|
77
|
+
if (!focused()) return;
|
|
78
|
+
const rows = store.snapshot();
|
|
79
|
+
if (rows.length === 0) {
|
|
80
|
+
exitDock();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
switch (event.name) {
|
|
84
|
+
case "up":
|
|
85
|
+
if (selection() <= 0) exitDock();else setSelection(current => current - 1);
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
break;
|
|
89
|
+
case "down":
|
|
90
|
+
setSelection(current => clampSelection(current + 1, rows.length));
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
break;
|
|
94
|
+
case "return":
|
|
95
|
+
{
|
|
103
96
|
const target = rows[clampSelection(selection(), rows.length)];
|
|
104
97
|
if (target) {
|
|
105
98
|
exitDock();
|
|
106
|
-
api.route.navigate("session", {
|
|
99
|
+
api.route.navigate("session", {
|
|
100
|
+
sessionID: target.id
|
|
101
|
+
});
|
|
107
102
|
}
|
|
108
103
|
event.preventDefault();
|
|
109
104
|
event.stopPropagation();
|
|
110
105
|
break;
|
|
111
106
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
);
|
|
116
|
-
|
|
107
|
+
}
|
|
108
|
+
}, {
|
|
109
|
+
priority: 1
|
|
110
|
+
});
|
|
117
111
|
api.keymap.registerLayer({
|
|
118
112
|
mode: "base",
|
|
119
|
-
commands: [
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
113
|
+
commands: [{
|
|
114
|
+
name: "agent-dock.toggle",
|
|
115
|
+
title: "Subagents",
|
|
116
|
+
category: "Plugin",
|
|
117
|
+
namespace: "palette",
|
|
118
|
+
slashName: "subagents",
|
|
119
|
+
desc: "Focus the subagent dock to navigate it",
|
|
120
|
+
run: () => {
|
|
121
|
+
if (focused()) exitDock();else enterDock();
|
|
122
|
+
}
|
|
123
|
+
}],
|
|
124
|
+
bindings: [{
|
|
125
|
+
key: "<leader>v",
|
|
126
|
+
cmd: "agent-dock.toggle",
|
|
127
|
+
desc: "Navigate subagents"
|
|
128
|
+
}]
|
|
134
129
|
});
|
|
135
130
|
};
|
|
136
|
-
|
|
137
|
-
const plugin: TuiPluginModule & { id: string } = {
|
|
131
|
+
const plugin = {
|
|
138
132
|
id: PLUGIN_ID,
|
|
139
|
-
tui
|
|
133
|
+
tui
|
|
140
134
|
};
|
|
141
|
-
|
|
142
135
|
export default plugin;
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// SDK boundary — the only place that knows about @opencode-ai/sdk shapes.
|
|
2
2
|
// Projects SDK Session/SessionStatus into the neutral domain Subagent.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
import type { Subagent, SubagentStatus } from "./domain.js";
|
|
6
|
-
|
|
7
|
-
export function statusFromSessionStatus(status: SessionStatus | undefined): SubagentStatus {
|
|
4
|
+
export function statusFromSessionStatus(status) {
|
|
8
5
|
switch (status?.type) {
|
|
9
6
|
case "busy":
|
|
10
7
|
case "retry":
|
|
@@ -15,20 +12,18 @@ export function statusFromSessionStatus(status: SessionStatus | undefined): Suba
|
|
|
15
12
|
return "done";
|
|
16
13
|
}
|
|
17
14
|
}
|
|
18
|
-
|
|
19
|
-
function totalTokens(tokens: Session["tokens"] | undefined): number | undefined {
|
|
15
|
+
function totalTokens(tokens) {
|
|
20
16
|
if (!tokens) return undefined;
|
|
21
17
|
const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write;
|
|
22
18
|
return total > 0 ? total : undefined;
|
|
23
19
|
}
|
|
24
|
-
|
|
25
|
-
export function projectSubagent(session: Session, status: SessionStatus | undefined): Subagent {
|
|
20
|
+
export function projectSubagent(session, status) {
|
|
26
21
|
return {
|
|
27
22
|
id: session.id,
|
|
28
23
|
title: session.title || session.agent || "subagent",
|
|
29
24
|
status: statusFromSessionStatus(status),
|
|
30
25
|
startedAt: session.time.created,
|
|
31
26
|
model: session.model?.id,
|
|
32
|
-
tokens: totalTokens(session.tokens)
|
|
27
|
+
tokens: totalTokens(session.tokens)
|
|
33
28
|
};
|
|
34
29
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
2
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
3
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
4
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
5
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
6
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
7
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
8
|
+
/** @jsxImportSource @opentui/solid */
|
|
9
|
+
// View — the persistent bottom panel (app_bottom slot). Claude-Code-style:
|
|
10
|
+
// plain text rows under the prompt, no panel chrome. Stays visible in both the
|
|
11
|
+
// parent session (running subagents) and inside a subagent (the focused one,
|
|
12
|
+
// highlighted via `activeId`). When the dock is focused for inline navigation,
|
|
13
|
+
// the selected row gets a `▸` cursor.
|
|
14
|
+
//
|
|
15
|
+
// Reactivity: `selection`, `focused`, `activeId` are real Solid signals (owned
|
|
16
|
+
// by the composition root), so per-row selected/active checks are tracked
|
|
17
|
+
// directly — no signal-reading hacks. The rows memo additionally reads the
|
|
18
|
+
// `version` signal (bumped on every store change) so elapsed times stay fresh.
|
|
19
|
+
|
|
20
|
+
import { For, Show, createMemo, createSignal, onCleanup } from "solid-js";
|
|
21
|
+
import { toRow } from "../domain.js";
|
|
22
|
+
import { LIMITS } from "../constants.js";
|
|
23
|
+
export function Dock(props) {
|
|
24
|
+
// The store emits on every refresh (data change + elapsed tick), which bumps
|
|
25
|
+
// `version` and re-runs the rows memo. No separate timer needed here.
|
|
26
|
+
const [version, setVersion] = createSignal(0);
|
|
27
|
+
const unsubscribe = props.store.onChange(() => setVersion(value => value + 1));
|
|
28
|
+
onCleanup(unsubscribe);
|
|
29
|
+
const rows = createMemo(() => {
|
|
30
|
+
version();
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
return props.store.snapshot().map(subagent => toRow(subagent, now, LIMITS.labelMax));
|
|
33
|
+
});
|
|
34
|
+
return _$createComponent(Show, {
|
|
35
|
+
get when() {
|
|
36
|
+
return rows().length > 0;
|
|
37
|
+
},
|
|
38
|
+
get children() {
|
|
39
|
+
var _el$ = _$createElement("box");
|
|
40
|
+
_$setProp(_el$, "flexDirection", "column");
|
|
41
|
+
_$setProp(_el$, "paddingLeft", 2);
|
|
42
|
+
_$setProp(_el$, "paddingRight", 2);
|
|
43
|
+
_$insert(_el$, _$createComponent(For, {
|
|
44
|
+
get each() {
|
|
45
|
+
return rows();
|
|
46
|
+
},
|
|
47
|
+
children: (row, index) => {
|
|
48
|
+
const selected = () => props.focused() && index() === props.selection();
|
|
49
|
+
const active = () => props.activeId() === row.subagent.id;
|
|
50
|
+
const hot = () => selected() || active();
|
|
51
|
+
return (() => {
|
|
52
|
+
var _el$2 = _$createElement("text"),
|
|
53
|
+
_el$3 = _$createElement("span"),
|
|
54
|
+
_el$4 = _$createElement("span"),
|
|
55
|
+
_el$5 = _$createElement("span");
|
|
56
|
+
_$insertNode(_el$2, _el$3);
|
|
57
|
+
_$insertNode(_el$2, _el$4);
|
|
58
|
+
_$insertNode(_el$2, _el$5);
|
|
59
|
+
_$insert(_el$3, () => selected() ? "\u25b8 " : " ");
|
|
60
|
+
_$insert(_el$4, () => row.glyph);
|
|
61
|
+
_$insert(_el$5, () => ` ${row.label}${row.meta ? " " + row.meta : ""}`);
|
|
62
|
+
_$effect(_p$ => {
|
|
63
|
+
var _v$ = {
|
|
64
|
+
fg: hot() ? props.colors.accent : props.colors.muted
|
|
65
|
+
},
|
|
66
|
+
_v$2 = {
|
|
67
|
+
fg: row.subagent.status === "running" ? props.colors.accent : props.colors.muted
|
|
68
|
+
},
|
|
69
|
+
_v$3 = {
|
|
70
|
+
fg: hot() ? props.colors.text : props.colors.muted
|
|
71
|
+
};
|
|
72
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "style", _v$, _p$.e));
|
|
73
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "style", _v$2, _p$.t));
|
|
74
|
+
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$5, "style", _v$3, _p$.a));
|
|
75
|
+
return _p$;
|
|
76
|
+
}, {
|
|
77
|
+
e: undefined,
|
|
78
|
+
t: undefined,
|
|
79
|
+
a: undefined
|
|
80
|
+
});
|
|
81
|
+
return _el$2;
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
return _el$;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-agent-dock",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "OpenCode TUI plugin — a Claude-Code-style bottom panel of running subagents with inline keyboard navigation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./tui": {
|
|
8
|
-
"import": "./
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
12
|
+
"dist",
|
|
13
13
|
"README.md",
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
+
"build": "node scripts/build.mjs",
|
|
18
|
+
"prepack": "npm run build",
|
|
19
|
+
"pretest": "npm run build",
|
|
17
20
|
"test": "vitest run",
|
|
18
21
|
"typecheck": "tsc --noEmit",
|
|
19
22
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
@@ -44,14 +47,20 @@
|
|
|
44
47
|
"@opentui/solid": ">=0.4.5"
|
|
45
48
|
},
|
|
46
49
|
"peerDependenciesMeta": {
|
|
47
|
-
"@opentui/solid": {
|
|
50
|
+
"@opentui/solid": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
55
|
+
"@babel/core": "7.28.0",
|
|
56
|
+
"@babel/preset-typescript": "7.27.1",
|
|
50
57
|
"@opencode-ai/plugin": "^1.18.4",
|
|
51
58
|
"@opencode-ai/sdk": "^1.18.4",
|
|
52
59
|
"@opentui/core": ">=0.4.5",
|
|
53
60
|
"@opentui/solid": ">=0.4.5",
|
|
54
61
|
"@types/bun": "latest",
|
|
62
|
+
"babel-preset-solid": "1.9.12",
|
|
63
|
+
"bun": "^1.4.4",
|
|
55
64
|
"prettier": "^3.8.1",
|
|
56
65
|
"solid-js": "^1.9.0",
|
|
57
66
|
"typescript": "^5.7.0",
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Adapter — Logger over the opencode client app log endpoint.
|
|
2
|
-
|
|
3
|
-
import type { OpencodeClient } from "@opencode-ai/sdk/v2";
|
|
4
|
-
import type { Logger } from "../ports.js";
|
|
5
|
-
import { SERVICE } from "../constants.js";
|
|
6
|
-
|
|
7
|
-
export function createLogger(client: OpencodeClient): Logger {
|
|
8
|
-
const write = (level: "info" | "warn" | "error", message: string): void => {
|
|
9
|
-
client.app
|
|
10
|
-
.log({ service: SERVICE, level, message })
|
|
11
|
-
.catch(() => {});
|
|
12
|
-
};
|
|
13
|
-
return {
|
|
14
|
-
info: (m) => write("info", m),
|
|
15
|
-
warn: (m) => write("warn", m),
|
|
16
|
-
error: (m) => write("error", m),
|
|
17
|
-
};
|
|
18
|
-
}
|
package/src/domain.test.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
sortSubagents,
|
|
4
|
-
clampSelection,
|
|
5
|
-
formatElapsed,
|
|
6
|
-
formatTokens,
|
|
7
|
-
truncate,
|
|
8
|
-
isTerminal,
|
|
9
|
-
toRow,
|
|
10
|
-
type Subagent,
|
|
11
|
-
} from "./domain.js";
|
|
12
|
-
import { statusFromSessionStatus } from "./sdk.js";
|
|
13
|
-
|
|
14
|
-
const sub = (overrides: Partial<Subagent> = {}): Subagent => ({
|
|
15
|
-
id: "ses_1",
|
|
16
|
-
title: "Analyze architecture",
|
|
17
|
-
status: "running",
|
|
18
|
-
startedAt: 0,
|
|
19
|
-
...overrides,
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("sortSubagents", () => {
|
|
23
|
-
it("orders running before idle before terminal states", () => {
|
|
24
|
-
const list = [
|
|
25
|
-
sub({ id: "done", status: "done", startedAt: 1 }),
|
|
26
|
-
sub({ id: "idle", status: "idle", startedAt: 2 }),
|
|
27
|
-
sub({ id: "run", status: "running", startedAt: 3 }),
|
|
28
|
-
];
|
|
29
|
-
expect(sortSubagents(list).map((s) => s.id)).toEqual(["run", "idle", "done"]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("falls back to newest-first within the same status", () => {
|
|
33
|
-
const list = [
|
|
34
|
-
sub({ id: "old", status: "running", startedAt: 10 }),
|
|
35
|
-
sub({ id: "new", status: "running", startedAt: 90 }),
|
|
36
|
-
];
|
|
37
|
-
expect(sortSubagents(list).map((s) => s.id)).toEqual(["new", "old"]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("does not mutate the input", () => {
|
|
41
|
-
const list = [sub({ id: "a" }), sub({ id: "b" })];
|
|
42
|
-
const snapshot = list.map((s) => s.id);
|
|
43
|
-
sortSubagents(list);
|
|
44
|
-
expect(list.map((s) => s.id)).toEqual(snapshot);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe("clampSelection", () => {
|
|
49
|
-
it("returns 0 for an empty list", () => {
|
|
50
|
-
expect(clampSelection(5, 0)).toBe(0);
|
|
51
|
-
});
|
|
52
|
-
it("clamps below 0 to 0", () => {
|
|
53
|
-
expect(clampSelection(-3, 4)).toBe(0);
|
|
54
|
-
});
|
|
55
|
-
it("clamps overflow to the last index", () => {
|
|
56
|
-
expect(clampSelection(99, 4)).toBe(3);
|
|
57
|
-
});
|
|
58
|
-
it("passes through in-range values", () => {
|
|
59
|
-
expect(clampSelection(2, 4)).toBe(2);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("formatElapsed", () => {
|
|
64
|
-
it("formats seconds under a minute", () => {
|
|
65
|
-
expect(formatElapsed(0, 59_000)).toBe("59s");
|
|
66
|
-
});
|
|
67
|
-
it("formats minutes with zero-padded seconds", () => {
|
|
68
|
-
expect(formatElapsed(0, 125_000)).toBe("2m 05s");
|
|
69
|
-
});
|
|
70
|
-
it("formats hours with zero-padded minutes", () => {
|
|
71
|
-
expect(formatElapsed(0, 3_725_000)).toBe("1h 02m");
|
|
72
|
-
});
|
|
73
|
-
it("never goes negative", () => {
|
|
74
|
-
expect(formatElapsed(10_000, 0)).toBe("0s");
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("formatTokens", () => {
|
|
79
|
-
it("returns empty for undefined / non-positive", () => {
|
|
80
|
-
expect(formatTokens(undefined)).toBe("");
|
|
81
|
-
expect(formatTokens(0)).toBe("");
|
|
82
|
-
});
|
|
83
|
-
it("renders raw numbers under 1000", () => {
|
|
84
|
-
expect(formatTokens(500)).toBe("500");
|
|
85
|
-
expect(formatTokens(999)).toBe("999");
|
|
86
|
-
});
|
|
87
|
-
it("renders thousands with a k suffix", () => {
|
|
88
|
-
expect(formatTokens(1500)).toBe("1.5k");
|
|
89
|
-
expect(formatTokens(85_400)).toBe("85.4k");
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe("truncate", () => {
|
|
94
|
-
it("trims and returns short text as-is", () => {
|
|
95
|
-
expect(truncate(" hi ", 10)).toBe("hi");
|
|
96
|
-
});
|
|
97
|
-
it("truncates with an ellipsis when too long", () => {
|
|
98
|
-
expect(truncate("hello world", 5)).toBe("hell…");
|
|
99
|
-
});
|
|
100
|
-
it("handles exact length without truncation", () => {
|
|
101
|
-
expect(truncate("hello", 5)).toBe("hello");
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("isTerminal", () => {
|
|
106
|
-
it("treats done and error as terminal", () => {
|
|
107
|
-
expect(isTerminal("done")).toBe(true);
|
|
108
|
-
expect(isTerminal("error")).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
it("treats running and idle as non-terminal", () => {
|
|
111
|
-
expect(isTerminal("running")).toBe(false);
|
|
112
|
-
expect(isTerminal("idle")).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("toRow", () => {
|
|
117
|
-
it("maps status to a glyph", () => {
|
|
118
|
-
expect(toRow(sub({ status: "running" }), 0, 48).glyph).toBe("●");
|
|
119
|
-
expect(toRow(sub({ status: "done" }), 0, 48).glyph).toBe("✓");
|
|
120
|
-
});
|
|
121
|
-
it("shows elapsed + tokens in meta for a running subagent", () => {
|
|
122
|
-
const row = toRow(sub({ status: "running", startedAt: 0, tokens: 1500 }), 30_000, 48);
|
|
123
|
-
expect(row.meta).toBe("30s · 1.5k");
|
|
124
|
-
});
|
|
125
|
-
it("drops elapsed for terminal states", () => {
|
|
126
|
-
const row = toRow(sub({ status: "done", tokens: 500 }), 30_000, 48);
|
|
127
|
-
expect(row.meta).toBe("500");
|
|
128
|
-
});
|
|
129
|
-
it("falls back to the id when the title is empty", () => {
|
|
130
|
-
expect(toRow(sub({ title: "", id: "ses_x" }), 0, 48).label).toBe("ses_x");
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe("statusFromSessionStatus", () => {
|
|
135
|
-
it("maps busy and retry to running", () => {
|
|
136
|
-
expect(statusFromSessionStatus({ type: "busy" })).toBe("running");
|
|
137
|
-
expect(statusFromSessionStatus({ type: "retry", attempt: 1, message: "x", next: 1 })).toBe("running");
|
|
138
|
-
});
|
|
139
|
-
it("maps idle to idle", () => {
|
|
140
|
-
expect(statusFromSessionStatus({ type: "idle" })).toBe("idle");
|
|
141
|
-
});
|
|
142
|
-
it("maps undefined to done", () => {
|
|
143
|
-
expect(statusFromSessionStatus(undefined)).toBe("done");
|
|
144
|
-
});
|
|
145
|
-
});
|
package/src/ports.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Ports — abstractions the application layer depends on.
|
|
2
|
-
// (Dependency Inversion: high-level policy depends on these contracts, not on
|
|
3
|
-
// concrete IO or the opencode SDK. Adapters in src/adapters implement them.)
|
|
4
|
-
|
|
5
|
-
import type { Subagent } from "./domain.js";
|
|
6
|
-
|
|
7
|
-
/** Source of the current session's subagents. */
|
|
8
|
-
export interface SubagentStore {
|
|
9
|
-
/** The session whose subagents we render (the active session route). */
|
|
10
|
-
readonly parentSessionId: string | undefined;
|
|
11
|
-
/** When the active route is a subagent session — its id, so the panel can
|
|
12
|
-
* highlight the one currently in focus. Undefined in the parent session. */
|
|
13
|
-
readonly activeId: string | undefined;
|
|
14
|
-
/** Snapshot of subagents, already sorted and capped for display. */
|
|
15
|
-
snapshot(): Subagent[];
|
|
16
|
-
/** Subscribe to store changes; returns an unsubscribe function. */
|
|
17
|
-
onChange(handler: () => void): () => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface Logger {
|
|
21
|
-
info(message: string): void;
|
|
22
|
-
warn(message: string): void;
|
|
23
|
-
error(message: string): void;
|
|
24
|
-
}
|
package/src/view/Dock.tsx
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
// View — the persistent bottom panel (app_bottom slot). Claude-Code-style:
|
|
3
|
-
// plain text rows under the prompt, no panel chrome. Stays visible in both the
|
|
4
|
-
// parent session (running subagents) and inside a subagent (the focused one,
|
|
5
|
-
// highlighted via `activeId`). When the dock is focused for inline navigation,
|
|
6
|
-
// the selected row gets a `▸` cursor.
|
|
7
|
-
//
|
|
8
|
-
// Reactivity: `selection`, `focused`, `activeId` are real Solid signals (owned
|
|
9
|
-
// by the composition root), so per-row selected/active checks are tracked
|
|
10
|
-
// directly — no signal-reading hacks. The rows memo additionally reads the
|
|
11
|
-
// `version` signal (bumped on every store change) so elapsed times stay fresh.
|
|
12
|
-
|
|
13
|
-
import { For, Show, createMemo, createSignal, onCleanup, type JSX } from "solid-js";
|
|
14
|
-
import type { RGBA } from "@opentui/core";
|
|
15
|
-
import type { SubagentStore } from "../ports.js";
|
|
16
|
-
import { toRow } from "../domain.js";
|
|
17
|
-
import { LIMITS } from "../constants.js";
|
|
18
|
-
|
|
19
|
-
export type DockColors = {
|
|
20
|
-
text: RGBA | string;
|
|
21
|
-
muted: RGBA | string;
|
|
22
|
-
accent: RGBA | string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type DockProps = {
|
|
26
|
-
store: SubagentStore;
|
|
27
|
-
selection: () => number;
|
|
28
|
-
focused: () => boolean;
|
|
29
|
-
activeId: () => string | undefined;
|
|
30
|
-
colors: DockColors;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export function Dock(props: DockProps): JSX.Element {
|
|
34
|
-
// The store emits on every refresh (data change + elapsed tick), which bumps
|
|
35
|
-
// `version` and re-runs the rows memo. No separate timer needed here.
|
|
36
|
-
const [version, setVersion] = createSignal(0);
|
|
37
|
-
const unsubscribe = props.store.onChange(() => setVersion((value) => value + 1));
|
|
38
|
-
onCleanup(unsubscribe);
|
|
39
|
-
|
|
40
|
-
const rows = createMemo(() => {
|
|
41
|
-
version();
|
|
42
|
-
const now = Date.now();
|
|
43
|
-
return props.store.snapshot().map((subagent) => toRow(subagent, now, LIMITS.labelMax));
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<Show when={rows().length > 0}>
|
|
48
|
-
<box flexDirection="column" paddingLeft={2} paddingRight={2}>
|
|
49
|
-
<For each={rows()}>
|
|
50
|
-
{(row, index) => {
|
|
51
|
-
const selected = () => props.focused() && index() === props.selection();
|
|
52
|
-
const active = () => props.activeId() === row.subagent.id;
|
|
53
|
-
const hot = () => selected() || active();
|
|
54
|
-
return (
|
|
55
|
-
<text>
|
|
56
|
-
<span style={{ fg: hot() ? props.colors.accent : props.colors.muted }}>
|
|
57
|
-
{selected() ? "\u25b8 " : " "}
|
|
58
|
-
</span>
|
|
59
|
-
<span style={{ fg: row.subagent.status === "running" ? props.colors.accent : props.colors.muted }}>
|
|
60
|
-
{row.glyph}
|
|
61
|
-
</span>
|
|
62
|
-
<span style={{ fg: hot() ? props.colors.text : props.colors.muted }}>
|
|
63
|
-
{` ${row.label}${row.meta ? " " + row.meta : ""}`}
|
|
64
|
-
</span>
|
|
65
|
-
</text>
|
|
66
|
-
);
|
|
67
|
-
}}
|
|
68
|
-
</For>
|
|
69
|
-
</box>
|
|
70
|
-
</Show>
|
|
71
|
-
);
|
|
72
|
-
}
|