changes
This commit is contained in:
BIN
.config/opencode/2026-03-11-123734_hyprshot.png
Normal file
BIN
.config/opencode/2026-03-11-123734_hyprshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 438 KiB |
BIN
.config/opencode/2026-03-11-123743_hyprshot.png
Normal file
BIN
.config/opencode/2026-03-11-123743_hyprshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 481 KiB |
@@ -211,6 +211,16 @@ Use stable identifiers so agents can pass note references between delegations.
|
||||
|
||||
All agents except `lead`, `coder`, and `librarian` are code/source read-only. Agents with `permission.edit: allow` may update basic-memory notes for their recording duties; they must not edit implementation source files.
|
||||
|
||||
### Explorer Scope Boundary
|
||||
|
||||
- **Explorer is local-only.** Use `explorer` only for mapping files, directories, symbols, dependencies, configuration, and edit points that already exist inside the current repository/worktree.
|
||||
- **Do not use `explorer` for external research.** Repository discovery on GitHub, upstream project behavior, package/library docs, web content, or competitor/tool comparisons belong to `researcher` or direct Lead research tools (`gh`, `webfetch`, docs lookup).
|
||||
- **Do not mix local and external discovery in one explorer prompt.** If a task needs both, split it explicitly:
|
||||
1. `explorer` → local file map only
|
||||
2. `researcher` or Lead tools → external behavior/references only
|
||||
3. Lead → synthesize the results
|
||||
- Explorer outputs should stay concrete: local file paths, likely edit points, dependency chains, and risks inside this repo only.
|
||||
|
||||
## Parallelization
|
||||
|
||||
- **Always parallelize independent work.** Any tool calls that do not depend on each other's output must be issued in the same message as parallel calls — never sequentially. This applies to bash commands, file reads, and subagent delegations alike.
|
||||
|
||||
@@ -9,6 +9,7 @@ permission:
|
||||
webfetch: deny
|
||||
websearch: deny
|
||||
codesearch: deny
|
||||
playwright_*: deny
|
||||
permalink: opencode-config/agents/explorer
|
||||
---
|
||||
|
||||
@@ -47,4 +48,5 @@ DEPENDENCIES:
|
||||
|
||||
RISKS:
|
||||
- <risk description>
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
@@ -18,6 +18,24 @@ import { spawn } from "bun"
|
||||
const isInsideTmux = () => Boolean(process.env.TMUX)
|
||||
const getCurrentPaneId = () => process.env.TMUX_PANE
|
||||
|
||||
const runTmux = async (args: string[]) => {
|
||||
const proc = spawn(["tmux", ...args], { stdout: "pipe", stderr: "pipe" })
|
||||
const stdout = await new Response(proc.stdout).text()
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
const exitCode = await proc.exited
|
||||
|
||||
return {
|
||||
exitCode,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
const removeItem = (items: string[], value: string) => {
|
||||
const idx = items.indexOf(value)
|
||||
if (idx !== -1) items.splice(idx, 1)
|
||||
}
|
||||
|
||||
const plugin: Plugin = async (ctx) => {
|
||||
if (!isInsideTmux()) return {}
|
||||
|
||||
@@ -28,6 +46,64 @@ const plugin: Plugin = async (ctx) => {
|
||||
// Ordered list of pane IDs in the right column.
|
||||
// Empty = no right column yet; length > 0 = right column exists.
|
||||
const rightColumnPanes: string[] = []
|
||||
let paneOps = Promise.resolve()
|
||||
|
||||
const getWindowInfo = async () => {
|
||||
const targetPane = sourcePaneId ?? rightColumnPanes[0]
|
||||
if (!targetPane) return null
|
||||
|
||||
const result = await runTmux([
|
||||
"display-message",
|
||||
"-p",
|
||||
"-t",
|
||||
targetPane,
|
||||
"#{window_id} #{window_width}",
|
||||
])
|
||||
|
||||
if (result.exitCode !== 0 || !result.stdout) return null
|
||||
|
||||
const [windowId, widthText] = result.stdout.split(/\s+/, 2)
|
||||
const width = Number(widthText)
|
||||
if (!windowId || Number.isNaN(width)) return null
|
||||
|
||||
return { windowId, width }
|
||||
}
|
||||
|
||||
const applyLayout = async () => {
|
||||
if (rightColumnPanes.length === 0) return
|
||||
|
||||
const windowInfo = await getWindowInfo()
|
||||
if (!windowInfo) return
|
||||
|
||||
const mainWidth = Math.max(1, Math.round(windowInfo.width * 0.6))
|
||||
|
||||
await runTmux([
|
||||
"set-window-option",
|
||||
"-t",
|
||||
windowInfo.windowId,
|
||||
"main-pane-width",
|
||||
String(mainWidth),
|
||||
])
|
||||
await runTmux(["select-layout", "-t", sourcePaneId ?? rightColumnPanes[0], "main-vertical"])
|
||||
}
|
||||
|
||||
const closeSessionPane = async (sessionId: string) => {
|
||||
const paneId = sessions.get(sessionId)
|
||||
if (!paneId) return
|
||||
|
||||
await runTmux(["kill-pane", "-t", paneId])
|
||||
sessions.delete(sessionId)
|
||||
removeItem(rightColumnPanes, paneId)
|
||||
await applyLayout()
|
||||
}
|
||||
|
||||
const enqueuePaneOp = (operation: () => Promise<void>) => {
|
||||
paneOps = paneOps.then(operation).catch(() => {})
|
||||
return paneOps
|
||||
}
|
||||
|
||||
const isTerminalSessionUpdate = (info: any) =>
|
||||
Boolean(info?.time?.archived || info?.time?.compacting)
|
||||
|
||||
return {
|
||||
event: async ({ event }) => {
|
||||
@@ -36,66 +112,77 @@ const plugin: Plugin = async (ctx) => {
|
||||
const info = (event as any).properties?.info
|
||||
// parentID presence distinguishes subagents from the root session
|
||||
if (!info?.id || !info?.parentID) return
|
||||
const sessionId: string = info.id
|
||||
if (sessions.has(sessionId)) return
|
||||
const sessionId: string = info.id
|
||||
if (sessions.has(sessionId)) return
|
||||
|
||||
const cmd = `opencode attach ${serverUrl} --session ${sessionId}`
|
||||
await enqueuePaneOp(async () => {
|
||||
if (sessions.has(sessionId)) return
|
||||
|
||||
let args: string[]
|
||||
if (rightColumnPanes.length === 0) {
|
||||
// First subagent: open a horizontal split, giving the new (right)
|
||||
// pane 40% of the window width so the main pane keeps 60%.
|
||||
args = [
|
||||
"tmux",
|
||||
"split-window",
|
||||
"-h", // horizontal split — new pane appears on the right
|
||||
"-p", "40", // new pane gets 40% of window width
|
||||
"-d", // don't focus the new pane
|
||||
"-P",
|
||||
"-F",
|
||||
"#{pane_id}",
|
||||
...(sourcePaneId ? ["-t", sourcePaneId] : []),
|
||||
cmd,
|
||||
]
|
||||
} else {
|
||||
// Additional subagents: stack vertically within the right column
|
||||
// by splitting the last right-column pane horizontally.
|
||||
const lastRightPane = rightColumnPanes[rightColumnPanes.length - 1]
|
||||
args = [
|
||||
"tmux",
|
||||
"split-window",
|
||||
"-v", // vertical split — new pane stacks below the target
|
||||
"-d", // don't focus the new pane
|
||||
"-P",
|
||||
"-F",
|
||||
"#{pane_id}",
|
||||
"-t", lastRightPane,
|
||||
cmd,
|
||||
]
|
||||
}
|
||||
const cmd = `opencode attach ${serverUrl} --session ${sessionId}`
|
||||
let args: string[]
|
||||
|
||||
const proc = spawn(args, { stdout: "pipe", stderr: "pipe" })
|
||||
const paneId = (await new Response(proc.stdout).text()).trim()
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode === 0 && paneId) {
|
||||
sessions.set(sessionId, paneId)
|
||||
rightColumnPanes.push(paneId)
|
||||
}
|
||||
if (rightColumnPanes.length === 0) {
|
||||
const windowInfo = await getWindowInfo()
|
||||
const rightWidth = windowInfo
|
||||
? Math.max(1, Math.round(windowInfo.width * 0.4))
|
||||
: 40
|
||||
|
||||
args = [
|
||||
"split-window",
|
||||
"-h",
|
||||
"-l",
|
||||
String(rightWidth),
|
||||
"-d",
|
||||
"-P",
|
||||
"-F",
|
||||
"#{pane_id}",
|
||||
...(sourcePaneId ? ["-t", sourcePaneId] : []),
|
||||
cmd,
|
||||
]
|
||||
} else {
|
||||
const lastRightPane = rightColumnPanes[rightColumnPanes.length - 1]
|
||||
args = [
|
||||
"split-window",
|
||||
"-v",
|
||||
"-d",
|
||||
"-P",
|
||||
"-F",
|
||||
"#{pane_id}",
|
||||
"-t",
|
||||
lastRightPane,
|
||||
cmd,
|
||||
]
|
||||
}
|
||||
|
||||
const result = await runTmux(args)
|
||||
const paneId = result.stdout
|
||||
|
||||
if (result.exitCode === 0 && paneId) {
|
||||
sessions.set(sessionId, paneId)
|
||||
rightColumnPanes.push(paneId)
|
||||
await applyLayout()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Kill the pane when the subagent session ends
|
||||
if (event.type === "session.deleted") {
|
||||
const info = (event as any).properties?.info
|
||||
const paneId = sessions.get(info?.id)
|
||||
if (paneId) {
|
||||
spawn(["tmux", "kill-pane", "-t", paneId], {
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
sessions.delete(info.id)
|
||||
const idx = rightColumnPanes.indexOf(paneId)
|
||||
if (idx !== -1) rightColumnPanes.splice(idx, 1)
|
||||
}
|
||||
if (!info?.id) return
|
||||
await enqueuePaneOp(() => closeSessionPane(info.id))
|
||||
}
|
||||
|
||||
if (event.type === "session.updated") {
|
||||
const info = (event as any).properties?.info
|
||||
if (!info?.id || !sessions.has(info.id) || !isTerminalSessionUpdate(info)) return
|
||||
await enqueuePaneOp(() => closeSessionPane(info.id))
|
||||
}
|
||||
|
||||
if (event.type === "session.status") {
|
||||
const sessionID = (event as any).properties?.sessionID
|
||||
const statusType = (event as any).properties?.status?.type
|
||||
if (!sessionID || !sessions.has(sessionID) || statusType !== "idle") return
|
||||
await enqueuePaneOp(() => closeSessionPane(sessionID))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user