pantheon-opencode 1.6.0-beta.6 → 1.6.0-beta.8

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.
Files changed (54) hide show
  1. package/AGENTS.md +19 -769
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +78 -9
  4. package/README.pt-BR.md +10 -4
  5. package/package.json +5 -1
  6. package/plugin.json +2 -2
  7. package/pyproject.toml +3 -1
  8. package/scripts/build-agents-md.mjs +159 -26
  9. package/scripts/install/agents-md.mjs +2 -2
  10. package/scripts/install/config-migration.mjs +115 -1
  11. package/scripts/install/interactive.mjs +1 -1
  12. package/scripts/install/opencode.mjs +245 -12
  13. package/scripts/install/shared.mjs +115 -7
  14. package/scripts/sync-artifacts.mjs +2 -2
  15. package/scripts/test-opencode-v2-sandbox.sh +155 -4
  16. package/scripts/validate-sandbox-install.mjs +138 -0
  17. package/skills-lock.json +5 -5
  18. package/src/agents/aphrodite.md +18 -36
  19. package/src/agents/demeter.md +8 -27
  20. package/src/agents/gaia.md +1 -0
  21. package/src/agents/hephaestus.md +10 -20
  22. package/src/agents/hermes.md +15 -37
  23. package/src/agents/mnemosyne.md +14 -42
  24. package/src/agents/prometheus.md +16 -19
  25. package/src/agents/talos.md +4 -1
  26. package/src/agents/themis.md +4 -4
  27. package/src/agents/zeus.md +24 -127
  28. package/src/instructions/agent-return-format.instructions.md +2 -3
  29. package/src/instructions/backend-standards.instructions.md +2 -0
  30. package/src/instructions/frontend-standards.instructions.md +3 -1
  31. package/src/instructions/memory-protocol.instructions.md +11 -121
  32. package/src/instructions/zeus-anti-stall.instructions.md +14 -69
  33. package/src/instructions/zeus-communication-rules.instructions.md +0 -1
  34. package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
  35. package/src/instructions/zeus-memory-operations.instructions.md +26 -0
  36. package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
  37. package/src/pantheon/v2-delegation-enforce.ts +87 -0
  38. package/src/pantheon/v2-execute-after.ts +186 -0
  39. package/src/pantheon/v2-unsupported.mjs +0 -23
  40. package/src/plugin-v2.ts +271 -36
  41. package/src/plugins/tui/dist/tui.d.ts +11 -8
  42. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  43. package/src/plugins/tui/dist/tui.js +53 -37
  44. package/src/plugins/tui/package-lock.json +30 -33
  45. package/src/plugins/tui/package.json +5 -1
  46. package/src/plugins/tui/src/index.tsx +103 -82
  47. package/src/plugins/tui/tsdown.config.ts +17 -0
  48. package/src/routing.yml +10 -5
  49. package/src/skills/README.md +3 -3
  50. package/src/skills/artifact-management/SKILL.md +12 -5
  51. package/src/skills/auto-continue/SKILL.md +37 -28
  52. package/src/skills/code-review-checklist/SKILL.md +5 -5
  53. package/src/skills/context-compression/SKILL.md +26 -879
  54. package/src/skills/tdd-with-agents/SKILL.md +18 -10
package/src/plugin-v2.ts CHANGED
@@ -19,8 +19,8 @@
19
19
  * `src/plugin-v2` in `plugins`, so the V1 plugin never loads on a V2-only
20
20
  * install and V1's identical block was inert by construction.
21
21
 
22
- * - A tool `execute.after` hook and a permission hook, registered as
23
- * registration points with no V2-side behaviour
22
+ * - A tool `execute.after` hook and a permission hook, with permission
23
+ * evaluation enforcing the caller/target matrix for Pantheon agents
24
24
  *
25
25
  * Every tool registered through `draft.add` MUST declare `output`, and its
26
26
  * `execute` MUST resolve to a value carrying `output`. The host enforces the
@@ -93,9 +93,17 @@ import {
93
93
  type PantheonV2Bridge,
94
94
  type V2ContextLike,
95
95
  } from './pantheon/v2-bridge.ts'
96
+ import {
97
+ type AuthoritativeSession,
98
+ evaluateV2Delegation,
99
+ PANTHEON_AGENTS,
100
+ type V2PermissionEvent,
101
+ v2ResourceTarget,
102
+ } from './pantheon/v2-delegation-enforce.ts'
103
+ import { createV2ExecuteAfter, resolveV2SandboxConfig } from './pantheon/v2-execute-after.ts'
96
104
  import { createV2ToolDefinitions, type V2ToolResult } from './pantheon/v2-tools.ts'
97
105
  import { V2_UNSUPPORTED_FEATURE_SEED } from './pantheon/v2-unsupported.mjs'
98
- import { type HookPayload, runHook } from './plugins/hook-runner.ts'
106
+ import { type HookPayload, type HookResult, runHook } from './plugins/hook-runner.ts'
99
107
 
100
108
  // ─── Unsupported Features Registry ───────────────────────────────────────
101
109
 
@@ -220,23 +228,26 @@ interface V2EventDomain {
220
228
  /**
221
229
  * The minimal shape of `ctx.permission`.
222
230
  *
223
- * Only a hook name and a handler: the plugin registers `evaluate` as a
224
- * registration point and deliberately runs no V2-side permission logic, so no
225
- * richer payload type is claimed here.
231
+ * The V2.0.25 live contract supplies an `evaluate` event with `resources`;
232
+ * runtime validation remains defensive because SDK declarations omit this
233
+ * domain and the host may evolve independently.
226
234
  */
227
235
  interface V2PermissionDomain {
228
- hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
236
+ hook?: (
237
+ name: string,
238
+ handler: (event: unknown, output?: unknown) => void | Promise<void>,
239
+ ) => Promise<unknown>
229
240
  }
230
241
 
231
242
  /**
232
- * The minimal shape of `ctx.session`.
233
- *
234
- * Only `hook(name, handler)`. The host accepts any string and silently drops an
235
- * unknown one — which is why the hook names below were a live defect once and
236
- * why the host-backed hook canary exists.
243
+ * The minimal shape of `ctx.session` used by this module. `get` follows the V2
244
+ * Promise API's object input (`{ sessionID }`); session hooks accept any string
245
+ * and silently drop an unknown one, which is why the host-backed hook canary
246
+ * exists.
237
247
  */
238
248
  interface V2SessionDomain {
239
249
  hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
250
+ get?: (input: { sessionID: string }) => Promise<unknown>
240
251
  }
241
252
 
242
253
  /** `PluginContext` plus the four domains 1.18.x omits. See the module header. */
@@ -760,6 +771,26 @@ function maskSecret(text: string): string {
760
771
  const SECRET_BLOCK_MESSAGE =
761
772
  '[plugin-v2] Blocked: high-confidence secret detected in tool input — see .pantheon/logs/hooks.log'
762
773
 
774
+ /**
775
+ * Message thrown when the secret scanner cannot produce a trustworthy verdict —
776
+ * the script is missing/unexecutable, the child timed out or was killed, the
777
+ * payload could not be serialized, the report was malformed, or the exit status
778
+ * was unexpected.
779
+ *
780
+ * This is a deliberate fail-CLOSED denial. The previous handler blocked only
781
+ * `code === 2` and treated every other outcome as advisory — but the runner
782
+ * reports a missing script, a spawn failure and a payload-serialization failure
783
+ * as `code: 1` too (src/plugins/hook-runner.ts), so a broken scanner read as a
784
+ * clean advisory and the tool ran, silently disabling the only hard secret block
785
+ * on the V2 surface.
786
+ *
787
+ * English and user-facing, like {@link SECRET_BLOCK_MESSAGE}. It carries no
788
+ * scanner output on purpose: a scan that failed mid-flight may still be holding
789
+ * the raw payload, and the denial must not become the leak it exists to prevent.
790
+ */
791
+ const SECRET_SCAN_UNAVAILABLE_MESSAGE =
792
+ '[plugin-v2] Blocked: the secret scanner could not complete, so this tool call was denied (fail-closed) — see .pantheon/logs/hooks.log'
793
+
763
794
  /**
764
795
  * Append one masked, ISO-stamped line to .pantheon/logs/hooks.log under the
765
796
  * working directory, and return whether it landed.
@@ -795,6 +826,98 @@ function appendV2HookLog(line: string): boolean {
795
826
  }
796
827
  }
797
828
 
829
+ // ─── V2 Secret-Scan Verdict ─────────────────────────────────────────────
830
+
831
+ /**
832
+ * The scanner's own report marker, written to stderr on every non-clean run
833
+ * (exit 1 advisory, exit 2 block). It is the discriminator between a genuine
834
+ * low-confidence advisory and a runner infrastructure failure, BOTH of which
835
+ * surface as `code === 1`: the runner's own failures (missing/unexecutable
836
+ * script, spawn error, payload-serialization error) resolve a structured
837
+ * `HookResult` with `code: 1` and no scan report, while the real scanner's
838
+ * advisory always carries this marker. Requiring it means a scanner that stops
839
+ * reporting fails toward a denial, never toward a silent allow.
840
+ */
841
+ const SECRET_SCAN_MARKER = '[SECRET SCAN]'
842
+
843
+ /** Sanitized failure classes — logged in place of raw scanner output. */
844
+ type SecretScanFailureReason =
845
+ | 'scanner-timeout'
846
+ | 'scanner-signal'
847
+ | 'scanner-spawn-failure'
848
+ | 'scanner-serialization-failure'
849
+ | 'scanner-malformed-output'
850
+ | 'scanner-unexpected-exit'
851
+
852
+ type SecretScanDecision =
853
+ | { action: 'allow' }
854
+ | { action: 'advisory' }
855
+ | { action: 'block' }
856
+ | { action: 'fail-closed'; reason: SecretScanFailureReason }
857
+
858
+ /**
859
+ * The runner's own infrastructure-failure messages (see hook-runner.ts), matched
860
+ * at the start of stderr where the runner is the only writer. Matching a prefix
861
+ * rather than a substring keeps a scan report that merely happens to quote one
862
+ * of these phrases from flipping a valid advisory into a denial.
863
+ */
864
+ const SCANNER_INFRA_FAILURES: ReadonlyArray<[string, SecretScanFailureReason]> = [
865
+ ['payload serialization failed:', 'scanner-serialization-failure'],
866
+ ['spawn failed:', 'scanner-spawn-failure'],
867
+ ['hook script failed to spawn:', 'scanner-spawn-failure'],
868
+ ]
869
+
870
+ /**
871
+ * Classify one secret-scan {@link HookResult} into an enforcement decision.
872
+ *
873
+ * fail-closed by construction: `allow` and `advisory` are reachable only from a
874
+ * result that is unambiguously a completed scanner run. Everything else —
875
+ * timeout, signal, spawn/serialization failure, missing report, unknown or
876
+ * unexpected exit status — denies. `advisory` additionally requires the
877
+ * scanner's own report marker, which is what separates an exit-1 advisory from
878
+ * the runner's exit-1 infrastructure failure.
879
+ *
880
+ * `block` covers the high-confidence match (exit 2); the caller turns it into
881
+ * the user-facing block.
882
+ */
883
+ function evaluateSecretScanResult(scan: HookResult): SecretScanDecision {
884
+ if (scan.timedOut) return { action: 'fail-closed', reason: 'scanner-timeout' }
885
+ if (scan.signal !== null) return { action: 'fail-closed', reason: 'scanner-signal' }
886
+ if (!Number.isInteger(scan.code)) {
887
+ return { action: 'fail-closed', reason: 'scanner-malformed-output' }
888
+ }
889
+ for (const [signature, reason] of SCANNER_INFRA_FAILURES) {
890
+ if (scan.stderr.startsWith(signature)) return { action: 'fail-closed', reason }
891
+ }
892
+ if (scan.code === 0) return { action: 'allow' }
893
+ if (scan.code === 2) return { action: 'block' }
894
+ if (scan.code === 1) {
895
+ return scan.stderr.includes(SECRET_SCAN_MARKER)
896
+ ? { action: 'advisory' }
897
+ : { action: 'fail-closed', reason: 'scanner-malformed-output' }
898
+ }
899
+ return { action: 'fail-closed', reason: 'scanner-unexpected-exit' }
900
+ }
901
+
902
+ /** Invocation of scripts/hooks/scan-secrets.sh used by the tool guard. */
903
+ type SecretScanRunner = (payload: HookPayload) => Promise<HookResult>
904
+
905
+ const defaultSecretScanRunner: SecretScanRunner = (payload) => runHook('scan-secrets.sh', payload)
906
+
907
+ let secretScanRunner: SecretScanRunner = defaultSecretScanRunner
908
+
909
+ /**
910
+ * Test seam — override (or, with `null`, restore) the V2 secret-scan runner.
911
+ *
912
+ * Exported only for tests/pantheon/plugin-v2-contract.test.ts, which injects
913
+ * structured {@link HookResult}s (missing scanner, timeout, malformed output)
914
+ * that the real script cannot be made to produce on demand. There is no
915
+ * production caller; the shipped handler always uses `defaultSecretScanRunner`.
916
+ */
917
+ export function setV2SecretScanRunnerForTests(runner: SecretScanRunner | null): void {
918
+ secretScanRunner = runner ?? defaultSecretScanRunner
919
+ }
920
+
798
921
  // ─── V2 Tool Hooks ──────────────────────────────────────────────────────
799
922
 
800
923
  /**
@@ -952,10 +1075,13 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
952
1075
  // `plugins` declares only `src/plugin-v2`. V1's scan-secrets block was
953
1076
  // therefore inert by construction, not by misconfiguration.
954
1077
  //
955
- // exit 2 → block. exit 1 (low-confidence header/KEY names) → advisory,
956
- // logged only. A scanner failure (missing script, timeout) is fail-OPEN:
957
- // runHook never rejects, and an infrastructure error must not deny
958
- // unrelated tool calls.
1078
+ // exit 0 → allow. exit 1 with a valid scanner report → advisory, logged
1079
+ // only. exit 2 (high-confidence match) → deny. A scanner that does NOT
1080
+ // produce a trustworthy verdict — missing/unexecutable script, timeout,
1081
+ // killed child, spawn/serialization failure, malformed or unexpected
1082
+ // output — is fail-CLOSED: this pending tool call is denied, the session
1083
+ // is not. (runHook resolves a structured result instead of rejecting, so
1084
+ // the exit-code-only reading below cannot lean on an exception.)
959
1085
  let blockError: Error | null = null
960
1086
  try {
961
1087
  const payload: HookPayload = {
@@ -964,18 +1090,29 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
964
1090
  agent_id: typeof agent === 'string' ? agent : '',
965
1091
  session_id: input.sessionID,
966
1092
  }
967
- const scan = await runHook('scan-secrets.sh', payload)
968
- if (scan.code === 2) {
1093
+ const scan = await secretScanRunner(payload)
1094
+ const decision = evaluateSecretScanResult(scan)
1095
+ if (decision.action === 'block') {
969
1096
  appendV2HookLog(SECRET_BLOCK_MESSAGE)
970
1097
  appendV2HookLog(scan.stderr)
971
1098
  blockError = new Error(SECRET_BLOCK_MESSAGE)
972
- } else if (scan.code === 1) {
1099
+ } else if (decision.action === 'advisory') {
973
1100
  // Advisory: log only, never block.
974
1101
  appendV2HookLog(scan.stderr)
1102
+ } else if (decision.action === 'fail-closed') {
1103
+ // Sanitized failure category only — never the scanner's raw stderr,
1104
+ // which on a failure path is unverified and may carry the very
1105
+ // payload the scan exists to keep out of logs.
1106
+ appendV2HookLog(`${SECRET_SCAN_UNAVAILABLE_MESSAGE} (${decision.reason})`)
1107
+ blockError = new Error(SECRET_SCAN_UNAVAILABLE_MESSAGE)
975
1108
  }
976
1109
  } catch {
977
- // Never let the scanner take down the session — runHook resolves a
978
- // structured result instead of rejecting, so this is belt-and-braces.
1110
+ // The runner is contracted to resolve a structured result, so this only
1111
+ // guards an unexpected throw (including a future change to the runner).
1112
+ // A scanner that cannot run is fail-CLOSED: deny this call rather than
1113
+ // let it through with no scan at all.
1114
+ appendV2HookLog(SECRET_SCAN_UNAVAILABLE_MESSAGE)
1115
+ blockError = new Error(SECRET_SCAN_UNAVAILABLE_MESSAGE)
979
1116
  }
980
1117
  if (blockError !== null) throw blockError
981
1118
  })
@@ -985,15 +1122,19 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
985
1122
  }
986
1123
 
987
1124
  try {
988
- // "execute.after" — hashline read enhance + context sandbox.
1125
+ // "execute.after" — the V1 parity chain (task-result-guard →
1126
+ // context-sandbox → read-enhancer) rebuilt on the V2 event shape.
989
1127
  //
990
- // Registration point only: there is no V2-side behaviour here. The
991
- // enhancer/sandbox live in the V1 path (`pantheon-hooks.ts` +
992
- // `context-sandbox.ts`), a separate plugin instance that is not loaded when
993
- // only `plugin-v2` is configured.
994
- await toolCtx.hook('execute.after', (_event: unknown) => {
995
- // Intentionally empty.
1128
+ // This IS behaviour, not a registration point: on a V2-only install the V1
1129
+ // `pantheon-hooks` plugin is not loaded, so this handler is the only thing
1130
+ // that truncates oversized output and tags `read` with hashline refs.
1131
+ // The sandbox config comes from `ctx.options.context_sandbox` (the V2 copy
1132
+ // the installer writes — V2.0.25 drops the V1 top-level key), resolved with
1133
+ // the same resolver V1 uses.
1134
+ const v2ExecuteAfter = createV2ExecuteAfter({
1135
+ sandbox: resolveV2SandboxConfig(context.options),
996
1136
  })
1137
+ await toolCtx.hook('execute.after', (event: unknown) => v2ExecuteAfter(event))
997
1138
  registered = true
998
1139
  } catch {
999
1140
  // Tool after-hook not supported
@@ -1008,12 +1149,15 @@ async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
1008
1149
  * Register permission hook via V2 ctx.permission.hook.
1009
1150
  *
1010
1151
  * The V2 permission.hook API (when available) provides:
1011
- * ctx.permission.hook("evaluate", handler) — custom permission logic
1152
+ * ctx.permission.hook("evaluate", handler) — host permission evaluation
1153
+ * The handler receives one mutable PermissionEvaluation event; decisions are
1154
+ * written to event.effect/event.message (there is no separate V1-style output).
1012
1155
  *
1013
1156
  * Returns true if the hook was registered.
1014
1157
  */
1015
1158
  async function registerV2PermissionHook(context: PluginContext): Promise<boolean> {
1016
- const permCtx = hostDomains(context).permission
1159
+ const domains = hostDomains(context)
1160
+ const permCtx = domains.permission
1017
1161
 
1018
1162
  if (!permCtx?.hook) {
1019
1163
  markUnsupported('permission-hook')
@@ -1021,12 +1165,103 @@ async function registerV2PermissionHook(context: PluginContext): Promise<boolean
1021
1165
  }
1022
1166
 
1023
1167
  try {
1024
- await permCtx.hook('evaluate', (_event: unknown) => {
1025
- // Registration point only — no V2-side permission logic. Read-only
1026
- // enforcement does NOT run here: it runs in the `execute.before` handler
1027
- // above, which is the hook the host consults before a tool executes. Do
1028
- // not read this as "the V1 guards cover it": V1 is a different plugin
1029
- // instance and is not loaded when only `plugin-v2` is configured.
1168
+ await permCtx.hook('evaluate', async (rawEvent: unknown) => {
1169
+ const event = rawEvent as V2PermissionEvent | null
1170
+ const effectBefore = event?.effect
1171
+ const writeProof = (): void => {
1172
+ const proofPath = process.env.PANTHEON_HOOK_CANARY_PERMISSION_PROOF
1173
+ if (!proofPath || !event || !Array.isArray(event.resources)) return
1174
+ try {
1175
+ appendFileSync(
1176
+ proofPath,
1177
+ `permission.evaluate ${JSON.stringify({
1178
+ resources: event.resources,
1179
+ agent: event.agent,
1180
+ sessionID: event.sessionID,
1181
+ effect: event.effect,
1182
+ message: event.message,
1183
+ eventFields: ['resources', 'agent', 'sessionID', 'effect'].filter((key) =>
1184
+ Object.hasOwn(event, key),
1185
+ ),
1186
+ effectBefore: effectBefore ?? null,
1187
+ effectAfter: event.effect ?? null,
1188
+ })}\n`,
1189
+ )
1190
+ } catch {
1191
+ // Instrumentation is best-effort and must not affect permission flow.
1192
+ }
1193
+ }
1194
+ const deny = (reason: string): void => {
1195
+ if (event === null || typeof event !== 'object') {
1196
+ throw new Error(
1197
+ `permission event is unavailable; denying V2 permission evaluation (${reason})`,
1198
+ )
1199
+ }
1200
+ event.effect = 'deny'
1201
+ event.message = `Pantheon delegation denied: ${reason}`
1202
+ if (event.effect !== 'deny') {
1203
+ throw new Error(
1204
+ `permission event is not mutable; denying V2 permission evaluation (${reason})`,
1205
+ )
1206
+ }
1207
+ writeProof()
1208
+ }
1209
+
1210
+ if (event?.effect === 'deny') {
1211
+ writeProof()
1212
+ return // explicit host deny is final
1213
+ }
1214
+ if (event == null) {
1215
+ deny('permission event is missing')
1216
+ return
1217
+ }
1218
+ if (!Array.isArray(event.resources) || event.resources.length === 0) {
1219
+ deny('permission resources are missing or malformed')
1220
+ return
1221
+ }
1222
+ const targets = event.resources.map(v2ResourceTarget)
1223
+ if (targets.some((target) => target === undefined)) {
1224
+ deny('permission resource target is malformed')
1225
+ return
1226
+ }
1227
+ const hasPantheonTarget = targets.some((target) => PANTHEON_AGENTS.has(target ?? ''))
1228
+ let session: AuthoritativeSession | undefined
1229
+ if (hasPantheonTarget) {
1230
+ const sessionID = typeof event.sessionID === 'string' ? event.sessionID.trim() : ''
1231
+ if (!sessionID) {
1232
+ deny('caller session ID is missing')
1233
+ return
1234
+ }
1235
+
1236
+ const sessionDomain = domains.session
1237
+ if (!sessionDomain || typeof sessionDomain.get !== 'function') {
1238
+ deny('session lookup API is unavailable')
1239
+ return
1240
+ }
1241
+
1242
+ let retrieved: unknown
1243
+ try {
1244
+ retrieved = await sessionDomain.get({ sessionID })
1245
+ } catch {
1246
+ deny('caller session lookup failed')
1247
+ return
1248
+ }
1249
+ if (retrieved === null || retrieved === undefined) {
1250
+ deny('caller session was not found')
1251
+ return
1252
+ }
1253
+ if (typeof retrieved !== 'object' || Array.isArray(retrieved)) {
1254
+ deny('caller session lookup returned invalid data')
1255
+ return
1256
+ }
1257
+ session = retrieved as AuthoritativeSession
1258
+ }
1259
+
1260
+ // Native-only resources remain under host policy. If a Pantheon target
1261
+ // appears anywhere, the shared policy evaluates the complete resource set.
1262
+ const decision = evaluateV2Delegation(event, session, event.effect)
1263
+ if (decision === 'deny') deny('caller or target is not allowed by delegation policy')
1264
+ else writeProof()
1030
1265
  })
1031
1266
  return true
1032
1267
  } catch {
@@ -1,4 +1,4 @@
1
- import { TuiPluginModule } from "@opencode-ai/plugin/tui";
1
+ import { TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui";
2
2
  //#region src/index.d.ts
3
3
  export type DelegationEntry = {
4
4
  /** Job alias, e.g. "apo-1" (from the H1 title, falling back to filename). */
@@ -487,17 +487,20 @@ export declare function navigateToDelegationSession(route: {
487
487
  export declare function createDelegationRowOpenHandler(route: {
488
488
  navigate?: (name: string, params?: Record<string, unknown>) => void | PromiseLike<void>;
489
489
  } | undefined, taskID: string | undefined): () => void;
490
- /** Plugin-level live delegation store shared with the event subscriptions
491
- * in `tui()`: the map of live entries + a version signal bumped on every
492
- * mutation. The View subscribes to the version (in an effect) to refresh the
493
- * durable child list and also reads the map as an optimistic live source. */
490
+ /** Plugin-level live delegation store shared by event subscriptions and View.
491
+ * Mutations notify subscribers so the View can refresh its durable child
492
+ * list while also reading the map as an optimistic live source. */
494
493
  export type LiveDelegationStore = {
495
494
  map: Map<string, LiveDelegationEntry>;
496
- /** Reactive version getter — View reads it inside an effect to re-fetch. */
497
- version: () => number;
498
- /** Bump the version after a live mutation. */
495
+ /** Subscribe to invalidations consumed by the sidebar's refresh path. */
496
+ subscribe: (listener: () => void) => () => void;
497
+ /** Notify sidebar subscribers after a live mutation. */
499
498
  bump: () => void;
500
499
  };
500
+ /** Create the observable live store shared by task events and the sidebar. */
501
+ export declare function createLiveDelegationStore(): LiveDelegationStore;
502
+ /** Register the live task-part events that invalidate the sidebar store. */
503
+ export declare function registerLiveDelegationEvents(api: TuiPluginApi, liveStore: LiveDelegationStore): () => void;
501
504
  declare const plugin: TuiPluginModule & {
502
505
  id: string;
503
506
  setup: () => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"}
1
+ {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promise
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { For, Show, createEffect, createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js";
7
+ import { For, Show, createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js";
8
8
  //#region src/index.tsx
9
9
  /** @jsxImportSource @opentui/solid */
10
10
  /**
@@ -167,7 +167,7 @@ async function detectVersion(api) {
167
167
  if (ver) return ver;
168
168
  }
169
169
  } catch {}
170
- return "1.6.0-beta.6";
170
+ return "1.6.0-beta.8";
171
171
  }
172
172
  /**
173
173
  * usage-bar — AI subscription usage gauge for the opencode TUI.
@@ -1805,10 +1805,49 @@ function DelegationRow(props) {
1805
1805
  return _el$13;
1806
1806
  })();
1807
1807
  }
1808
- /** Plugin-level live delegation store shared with the event subscriptions
1809
- * in `tui()`: the map of live entries + a version signal bumped on every
1810
- * mutation. The View subscribes to the version (in an effect) to refresh the
1811
- * durable child list and also reads the map as an optimistic live source. */
1808
+ /** Plugin-level live delegation store shared by event subscriptions and View.
1809
+ * Mutations notify subscribers so the View can refresh its durable child
1810
+ * list while also reading the map as an optimistic live source. */
1811
+ /** Create the observable live store shared by task events and the sidebar. */
1812
+ function createLiveDelegationStore() {
1813
+ const listeners = /* @__PURE__ */ new Set();
1814
+ return {
1815
+ map: /* @__PURE__ */ new Map(),
1816
+ subscribe(listener) {
1817
+ listeners.add(listener);
1818
+ return () => listeners.delete(listener);
1819
+ },
1820
+ bump() {
1821
+ for (const listener of listeners) listener();
1822
+ }
1823
+ };
1824
+ }
1825
+ /** Register the live task-part events that invalidate the sidebar store. */
1826
+ function registerLiveDelegationEvents(api, liveStore) {
1827
+ const unsubLive = [];
1828
+ try {
1829
+ unsubLive.push(api.event.on("message.part.updated", (event) => {
1830
+ const props = event?.properties ?? {};
1831
+ const part = props.part ?? props.info?.part;
1832
+ if (part === void 0) return;
1833
+ const sample = extractToolActivity(part);
1834
+ if (sample !== null) trackToolActivity(latestToolActivity, sample.sessionID, sample.activity);
1835
+ if (reduceDelegationToolPart(liveStore.map, part)) liveStore.bump();
1836
+ }));
1837
+ } catch {}
1838
+ try {
1839
+ unsubLive.push(api.event.on("message.part.removed", (event) => {
1840
+ const props = event?.properties ?? {};
1841
+ const partID = props.partID ?? props.part?.id;
1842
+ if (partID !== void 0 && removeDelegationEntry(liveStore.map, partID)) liveStore.bump();
1843
+ }));
1844
+ } catch {}
1845
+ return () => {
1846
+ for (const unsub of unsubLive) try {
1847
+ unsub();
1848
+ } catch {}
1849
+ };
1850
+ }
1812
1851
  function View(props) {
1813
1852
  const [showSessions, setShowSessions] = createSignal(false);
1814
1853
  const [showDelegations, setShowDelegations] = createSignal(true);
@@ -1938,11 +1977,12 @@ function View(props) {
1938
1977
  cleanup.push(props.api.event.on("session.deleted", eventRefresh));
1939
1978
  cleanup.push(props.api.event.on("session.status", eventRefresh));
1940
1979
  } catch {}
1941
- createEffect(() => {
1942
- props.liveStore.version();
1980
+ cleanup.push(props.liveStore.subscribe(() => {
1943
1981
  eventRefreshCount += 1;
1944
1982
  refreshDelegations();
1945
- });
1983
+ }));
1984
+ eventRefreshCount += 1;
1985
+ refreshDelegations();
1946
1986
  const poll = setInterval(() => {
1947
1987
  setNow(Date.now());
1948
1988
  refreshDelegations();
@@ -2169,34 +2209,10 @@ const tui = (api, _options, _meta) => {
2169
2209
  const [version, setVersion] = createSignal(null);
2170
2210
  detectVersion(api).then((detected) => setVersion(detected)).catch(() => setVersion(null));
2171
2211
  setupUsageBar(api);
2172
- const [liveVersion, setLiveVersion] = createSignal(0);
2173
- const liveStore = {
2174
- map: /* @__PURE__ */ new Map(),
2175
- version: liveVersion,
2176
- bump: () => setLiveVersion((v) => v + 1)
2177
- };
2178
- const unsubLive = [];
2179
- try {
2180
- unsubLive.push(api.event.on("message.part.updated", (event) => {
2181
- const props = event?.properties ?? {};
2182
- const part = props.part ?? props.info?.part;
2183
- if (part === void 0) return;
2184
- const sample = extractToolActivity(part);
2185
- if (sample !== null) trackToolActivity(latestToolActivity, sample.sessionID, sample.activity);
2186
- if (reduceDelegationToolPart(liveStore.map, part)) liveStore.bump();
2187
- }));
2188
- } catch {}
2189
- try {
2190
- unsubLive.push(api.event.on("message.part.removed", (event) => {
2191
- const props = event?.properties ?? {};
2192
- const partID = props.partID ?? props.part?.id;
2193
- if (partID !== void 0 && removeDelegationEntry(liveStore.map, partID)) liveStore.bump();
2194
- }));
2195
- } catch {}
2212
+ const liveStore = createLiveDelegationStore();
2213
+ const unsubscribeLive = registerLiveDelegationEvents(api, liveStore);
2196
2214
  api.lifecycle.onDispose(() => {
2197
- for (const unsub of unsubLive) try {
2198
- unsub();
2199
- } catch {}
2215
+ unsubscribeLive();
2200
2216
  liveStore.map.clear();
2201
2217
  });
2202
2218
  api.slots.register({
@@ -2221,6 +2237,6 @@ const plugin = {
2221
2237
  setup: async () => {}
2222
2238
  };
2223
2239
  //#endregion
2224
- export { DELEGATION_ALIAS_WIDTH, DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_CHILD_STATUS_GRACE_MS, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, IDLE_SILENCE_MS, NATIVE_LIVE_ALIASLESS_TTL_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList };
2240
+ export { DELEGATION_ALIAS_WIDTH, DELEGATION_CHILDREN_RECENCY_MS, DELEGATION_CHILD_STATUS_GRACE_MS, DELEGATION_DESCRIPTION_MAX, DELEGATION_DONE_RETENTION_MS, DELEGATION_ELAPSED_WIDTH, DELEGATION_FAILED_RETENTION_MS, DELEGATION_ROW_GLYPHS, DELEGATION_VISIBLE_CEILING, IDLE_SILENCE_MS, NATIVE_LIVE_ALIASLESS_TTL_MS, STALE_RUNNING_THRESHOLD_MS, buildChildrenPath, ceilingDelegationList, childStatusToState, childrenToDelegationEntries, collectDelegationToolParts, compareDelegationEntries, countDelegationSources, createDelegationRowOpenHandler, createLiveDelegationStore, plugin as default, delegationActivity, delegationActivityLabel, delegationElapsed, delegationRowGlyph, delegationRowIdentity, delegationRowMarker, delegationRowStatus, delegationSpinnerFrame, delegationStateTone, extractToolActivity, filterDelegationsToSession, fmtElapsed, formatDelegationAlias, formatDelegationElapsed, formatDelegationHeader, formatDelegationRowLead, formatPanelLogLine, isValidSessionId, latestToolActivityFor, markStaleIfRunning, mergeChildDelegationSources, mergeDelegationSources, navigateToDelegationSession, panelLogDir, parseDelegationMarkdown, parseDelegationToolPart, readAllDelegationEntries, readDelegationEntries, reduceDelegationToolPart, registerLiveDelegationEvents, removeDelegationEntry, resolveCurrentSessionID, resolveDelegationsDir, resolvePantheonRoot, safeSessionPath, seedLiveDelegationMap, splitDelegationList, toDelegationEntry, trackToolActivity, truncateDelegationDescription, tuiLogPath, visibleDelegationList };
2225
2241
 
2226
2242
  //# sourceMappingURL=tui.js.map