jorgex-stack 1.9.73 → 1.9.75
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 +42 -7
- package/dist/browser-playwright.js +22 -10
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11773 -7585
- package/package.json +3 -2
- package/stack/assets/opencode/sounds/done.wav +0 -0
- package/stack/assets/opencode/sounds/silent.wav +0 -0
- package/stack/assets/opencode/tui/subagents/state.mjs +37 -0
- package/stack/assets/opencode/tui/subagents/tui.tsx +145 -0
- package/stack/plugins/opencode/hooks.ts +404 -514
- package/stack/plugins/opencode/package.json +1 -4
- package/stack/plugins/opencode/worktree.ts +291 -320
- package/upstreams.json +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jorgex-stack",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.75",
|
|
4
4
|
"description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI, OpenCode y Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@clack/prompts": "^1.5.1"
|
|
52
|
+
"@clack/prompts": "^1.5.1",
|
|
53
|
+
"jsonc-parser": "3.3.1"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@types/node": "^25.9.2",
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function subagentState(status, outcome, blocked = false) {
|
|
2
|
+
if (blocked) return "blocked"
|
|
3
|
+
if (status === "running") return "running"
|
|
4
|
+
if (outcome === "succeeded") return "done"
|
|
5
|
+
if (outcome === "failed") return "failed"
|
|
6
|
+
if (outcome === "interrupted") return "stopped"
|
|
7
|
+
return "idle"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isActiveSubagent(state) {
|
|
11
|
+
return state === "running" || state === "blocked"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function activeElapsed(state, startedAt, now) {
|
|
15
|
+
if (!isActiveSubagent(state) || !Number.isFinite(startedAt)) return undefined
|
|
16
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1000))
|
|
17
|
+
const minutes = Math.floor(seconds / 60) % 60
|
|
18
|
+
const parts = [minutes, seconds % 60].map((value) => String(value).padStart(2, "0"))
|
|
19
|
+
if (seconds >= 3600) parts.unshift(String(Math.floor(seconds / 3600)).padStart(2, "0"))
|
|
20
|
+
return parts.join(":")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatTokens(tokens) {
|
|
24
|
+
if (!tokens) return undefined
|
|
25
|
+
const buckets = [tokens.input, tokens.output, tokens.reasoning, tokens.cache?.read, tokens.cache?.write]
|
|
26
|
+
if (buckets.some((value) => !Number.isFinite(value) || value < 0)) return undefined
|
|
27
|
+
const total = buckets.reduce((sum, value) => sum + value, 0)
|
|
28
|
+
if (!Number.isFinite(total)) return undefined
|
|
29
|
+
if (total >= 1000000) return `${(total / 1000000).toFixed(1)}m tok`
|
|
30
|
+
if (total >= 1000) return `${(total / 1000).toFixed(1)}k tok`
|
|
31
|
+
return `${Math.round(total)} tok`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatModel(model) {
|
|
35
|
+
if (!model?.providerID || !model?.id) return undefined
|
|
36
|
+
return `${model.providerID}/${model.id}${model.variant ? `#${model.variant}` : ""}`
|
|
37
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// OpenCode v2 CLI sidebar plugin, registered as "./tui/subagents" in cli.json.
|
|
2
|
+
import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from "solid-js"
|
|
3
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
4
|
+
import { activeElapsed, formatModel, formatTokens, isActiveSubagent, subagentState } from "./state.mjs"
|
|
5
|
+
|
|
6
|
+
export default Plugin.define({
|
|
7
|
+
id: "jorgex.subagents",
|
|
8
|
+
setup(context) {
|
|
9
|
+
const [open, setOpen] = createSignal(false)
|
|
10
|
+
const toggle = () => { setOpen((value) => !value) }
|
|
11
|
+
const [starts, setStarts] = createSignal<Record<string, number>>({})
|
|
12
|
+
// Do not infer execution duration from session creation: sessions can run again.
|
|
13
|
+
const stopStarted = context.data.on("session.execution.started", (event) => {
|
|
14
|
+
setStarts((current) => ({ ...current, [event.data.sessionID]: event.created }))
|
|
15
|
+
})
|
|
16
|
+
const stopEnded = context.data.listen(({ details }) => {
|
|
17
|
+
if (!["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.deleted"].includes(details.type)) return
|
|
18
|
+
const id = details.data.sessionID
|
|
19
|
+
if (!id) return
|
|
20
|
+
setStarts((current) => {
|
|
21
|
+
const next = { ...current }
|
|
22
|
+
delete next[id]
|
|
23
|
+
return next
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
const theme = () => context.theme
|
|
27
|
+
const state = (id: string) => subagentState(
|
|
28
|
+
context.data.session.status(id),
|
|
29
|
+
context.data.session.get(id)?.outcome,
|
|
30
|
+
(context.data.session.permission.list(id) ?? []).length > 0 ||
|
|
31
|
+
(context.data.session.form.list(id) ?? []).length > 0,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// Keymap layers require the keymap provider, which is not available during setup.
|
|
35
|
+
const unregisterCommands = context.ui.slot({
|
|
36
|
+
append: "app",
|
|
37
|
+
render: () => {
|
|
38
|
+
context.keymap.layer(() => ({
|
|
39
|
+
mode: "global",
|
|
40
|
+
commands: [{
|
|
41
|
+
id: "jorgex.subagents.toggle",
|
|
42
|
+
title: "Toggle subagent panel",
|
|
43
|
+
group: "Subagents",
|
|
44
|
+
palette: true,
|
|
45
|
+
slash: { name: "subagents" },
|
|
46
|
+
run: toggle,
|
|
47
|
+
}],
|
|
48
|
+
}))
|
|
49
|
+
return null
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const Card = (props: { sessionID: string }) => {
|
|
54
|
+
createEffect(on(() => props.sessionID, () => setOpen(false)))
|
|
55
|
+
const [now, setNow] = createSignal(Date.now())
|
|
56
|
+
const children = createMemo(() => {
|
|
57
|
+
const root = context.data.session.root(props.sessionID) ?? props.sessionID
|
|
58
|
+
return context.data.session.family(root).filter((id) => id !== root)
|
|
59
|
+
})
|
|
60
|
+
const active = createMemo(() => children().filter((id) => isActiveSubagent(state(id))))
|
|
61
|
+
createEffect(() => {
|
|
62
|
+
if (!open() || active().length === 0) return
|
|
63
|
+
setNow(Date.now())
|
|
64
|
+
const timer = setInterval(() => setNow(Date.now()), 1000)
|
|
65
|
+
onCleanup(() => clearInterval(timer))
|
|
66
|
+
})
|
|
67
|
+
const counts = createMemo(() => {
|
|
68
|
+
const result = { running: 0, blocked: 0, done: 0 }
|
|
69
|
+
for (const id of children()) {
|
|
70
|
+
const value = state(id)
|
|
71
|
+
if (value === "running" || value === "blocked" || value === "done") result[value]++
|
|
72
|
+
}
|
|
73
|
+
return result
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<box flexDirection="column" gap={0}>
|
|
78
|
+
<box flexDirection="column" onMouseDown={(event) => {
|
|
79
|
+
if (event.button !== 0) return
|
|
80
|
+
event.preventDefault()
|
|
81
|
+
event.stopPropagation()
|
|
82
|
+
toggle()
|
|
83
|
+
}}>
|
|
84
|
+
<text fg={theme().text.base} selectable={false}>
|
|
85
|
+
<b>{`${open() ? "▼" : "▶"} Subagents`}</b>
|
|
86
|
+
</text>
|
|
87
|
+
<Show when={children().length > 0} fallback={<text fg={theme().text.muted}>No subagents</text>}>
|
|
88
|
+
<text selectable={false}>
|
|
89
|
+
<span style={{ fg: theme().text.feedback.warning.base }}>{`• ${counts().running} running`}</span>
|
|
90
|
+
<Show when={counts().blocked > 0}>
|
|
91
|
+
<span style={{ fg: theme().text.feedback.warning.base }}>{` · ${counts().blocked} needs input`}</span>
|
|
92
|
+
</Show>
|
|
93
|
+
<span style={{ fg: theme().text.feedback.success.base }}>{` · ✓ ${counts().done} done`}</span>
|
|
94
|
+
</text>
|
|
95
|
+
</Show>
|
|
96
|
+
</box>
|
|
97
|
+
<Show when={open()}>
|
|
98
|
+
<Show when={active().length > 0} fallback={<text fg={theme().text.muted}>No active subagents</text>}>
|
|
99
|
+
<For each={active()}>
|
|
100
|
+
{(id) => {
|
|
101
|
+
const info = () => context.data.session.get(id)
|
|
102
|
+
const duration = () => activeElapsed(state(id), starts()[id], now())
|
|
103
|
+
const metrics = () => [duration(), formatTokens(info()?.tokens)].filter(Boolean).join(" · ")
|
|
104
|
+
const model = () => formatModel(info()?.model)
|
|
105
|
+
return (
|
|
106
|
+
<box flexDirection="column" paddingLeft={1} onMouseUp={(event) => {
|
|
107
|
+
if (event.button !== 0) return
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
event.stopPropagation()
|
|
110
|
+
context.ui.router.navigate({ type: "session", sessionID: id })
|
|
111
|
+
}}>
|
|
112
|
+
<box flexDirection="row" gap={1} minWidth={0}>
|
|
113
|
+
<text fg={theme().text.feedback.warning.base} flexShrink={0} selectable={false}>•</text>
|
|
114
|
+
<text fg={theme().text.base} flexGrow={1} minWidth={0} wrapMode="none" truncate selectable={false}>
|
|
115
|
+
<b>{info()?.agent ?? info()?.title ?? id}</b>
|
|
116
|
+
</text>
|
|
117
|
+
<Show when={state(id) === "blocked"}>
|
|
118
|
+
<text fg={theme().text.feedback.warning.base} flexShrink={0} selectable={false}>Needs input</text>
|
|
119
|
+
</Show>
|
|
120
|
+
</box>
|
|
121
|
+
<Show when={metrics()}>
|
|
122
|
+
<text fg={theme().text.muted} paddingLeft={2} wrapMode="none" truncate selectable={false}>{metrics()}</text>
|
|
123
|
+
</Show>
|
|
124
|
+
<Show when={model()}>
|
|
125
|
+
<text fg={theme().text.muted} paddingLeft={2} wrapMode="none" truncate selectable={false}>
|
|
126
|
+
{model()}
|
|
127
|
+
</text>
|
|
128
|
+
</Show>
|
|
129
|
+
</box>
|
|
130
|
+
)
|
|
131
|
+
}}
|
|
132
|
+
</For>
|
|
133
|
+
</Show>
|
|
134
|
+
</Show>
|
|
135
|
+
</box>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const unregister = context.ui.slot({
|
|
140
|
+
append: "sidebar.content",
|
|
141
|
+
render: ({ sessionID }) => <Card sessionID={sessionID} />,
|
|
142
|
+
})
|
|
143
|
+
return () => { unregister(); unregisterCommands(); stopStarted(); stopEnded() }
|
|
144
|
+
},
|
|
145
|
+
})
|