jorgex-stack 1.9.74 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.9.74",
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",
@@ -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
+ })
package/upstreams.json CHANGED
@@ -59,7 +59,14 @@
59
59
  "version": null,
60
60
  "strategy": "provider-managed",
61
61
  "reviewed": "2026-09-24",
62
- "note": "CLI global compartido por la máquina, camino recomendado opt-in con smoke de navegador; preferencia observada por máquina; provider-managed sin re-pin manual ni selector fijo futuro"
62
+ "note": "Árbol Stack gestionado y verificado con guard (no un CLI global compartido); opt-in para los runtimes distintos de OpenCode; OpenCode v2 usa Browser Control obligatorio (sin selector Playwright CLI). provider-managed sin re-pin manual ni selector fijo futuro"
63
+ },
64
+ "browser-control": {
65
+ "source": "npm:@opencode-ai/browser-control",
66
+ "version": null,
67
+ "strategy": "provider-managed",
68
+ "reviewed": "2026-10-02",
69
+ "note": "CLI+skill+MCP oficiales; provider gestiona la versión (rolling/latest) con SRI verificado y guard de ejecución; solo OpenCode v2. Último observado latest 0.8.3 (SRI sha512) como referencia informativa, sin fijarlo"
63
70
  }
64
71
  },
65
72
  "skills": {