Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dedd0e3cb1 | |||
| 59fa06d2c3 | |||
| 27f866216a | |||
| ad17c00695 | |||
| 170f44f17c | |||
| e72c132ff5 | |||
| 084f27a132 |
@@ -1787,6 +1787,7 @@ function InlineTool(props: {
|
||||
complete: any
|
||||
pending: string
|
||||
spinner?: boolean
|
||||
task?: boolean
|
||||
children: JSX.Element
|
||||
part: ToolPart
|
||||
onClick?: () => void
|
||||
@@ -1827,6 +1828,7 @@ function InlineTool(props: {
|
||||
|
||||
return (
|
||||
<InlineToolRow
|
||||
id={`tool-inline-${props.task ? "task-" : ""}${props.part.id}`}
|
||||
icon={props.icon}
|
||||
iconColor={props.iconColor}
|
||||
color={fg()}
|
||||
@@ -1838,6 +1840,7 @@ function InlineTool(props: {
|
||||
complete={props.complete}
|
||||
pending={props.pending}
|
||||
spinner={props.spinner}
|
||||
task={props.task}
|
||||
separateAfter={(id) =>
|
||||
sync.data.message[ctx.sessionID]?.some((message) => message.role === "user" && message.id === id) ?? false
|
||||
}
|
||||
@@ -1858,6 +1861,7 @@ function InlineTool(props: {
|
||||
}
|
||||
|
||||
export function InlineToolRow(props: {
|
||||
id?: string
|
||||
icon: string
|
||||
iconColor?: RGBA
|
||||
color?: RGBA
|
||||
@@ -1869,6 +1873,7 @@ export function InlineToolRow(props: {
|
||||
complete: any
|
||||
pending: string
|
||||
spinner?: boolean
|
||||
task?: boolean
|
||||
children: JSX.Element
|
||||
separateAfter?: (id: string | undefined) => boolean
|
||||
onMouseOver?: () => void
|
||||
@@ -1879,6 +1884,7 @@ export function InlineToolRow(props: {
|
||||
|
||||
return (
|
||||
<box
|
||||
id={props.id}
|
||||
marginTop={margin()}
|
||||
paddingLeft={3}
|
||||
onMouseOver={props.onMouseOver}
|
||||
@@ -1893,9 +1899,12 @@ export function InlineToolRow(props: {
|
||||
const children = parent.getChildren()
|
||||
const index = children.indexOf(el)
|
||||
const previous = children[index - 1]
|
||||
const previousInline = previous?.id.startsWith("tool-inline-") ?? false
|
||||
const previousTask = previous?.id.startsWith("tool-inline-task-") ?? false
|
||||
setMargin(
|
||||
previous?.id.startsWith("text-") ||
|
||||
previous?.id.startsWith("tool-block-") ||
|
||||
(previousInline && previousTask !== Boolean(props.task)) ||
|
||||
props.separateAfter?.(previous?.id)
|
||||
? 1
|
||||
: 0,
|
||||
@@ -2121,8 +2130,8 @@ function Read(props: ToolProps<typeof ReadTool>) {
|
||||
Read {pathFormatter.format(props.input.filePath)} {input(props.input, ["filePath"])}
|
||||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
{(filepath) => (
|
||||
<box paddingLeft={3}>
|
||||
{(filepath, index) => (
|
||||
<box id={`tool-inline-loaded-${props.part.id}-${index()}`} paddingLeft={3}>
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
↳ Loaded {pathFormatter.format(filepath)}
|
||||
</text>
|
||||
@@ -2188,11 +2197,14 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
tools().findLast((x) => (x.state.status === "running" || x.state.status === "completed") && x.state.title),
|
||||
)
|
||||
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const status = createMemo(() => sync.data.session_status[props.metadata.sessionId ?? ""])
|
||||
const isRunning = createMemo(
|
||||
() => props.part.state.status === "running" || (props.metadata.background === true && status()?.type !== "idle"),
|
||||
)
|
||||
const retry = createMemo(() => {
|
||||
const status = sync.data.session_status[props.metadata.sessionId ?? ""]
|
||||
if (status?.type !== "retry") return
|
||||
return status
|
||||
const value = status()
|
||||
if (value?.type !== "retry") return
|
||||
return value
|
||||
})
|
||||
|
||||
const duration = createMemo(() => {
|
||||
@@ -2220,11 +2232,11 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
} else content.push(`↳ ${tools().length} toolcalls`)
|
||||
}
|
||||
|
||||
if (props.part.state.status === "completed") {
|
||||
if (!isRunning() && props.part.state.status === "completed") {
|
||||
content.push(
|
||||
props.metadata.background === true
|
||||
? `└ ${tools().length} toolcalls`
|
||||
: `└ ${tools().length} toolcalls · ${Locale.duration(duration())}`,
|
||||
? `↳ ${tools().length} toolcalls`
|
||||
: `↳ ${tools().length} toolcalls · ${Locale.duration(duration())}`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2233,7 +2245,8 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
|
||||
return (
|
||||
<InlineTool
|
||||
icon="│"
|
||||
icon={props.part.state.status === "completed" ? "✓" : "│"}
|
||||
task={true}
|
||||
color={retry() ? theme.error : undefined}
|
||||
spinner={isRunning()}
|
||||
complete={props.input.description}
|
||||
|
||||
@@ -0,0 +1,753 @@
|
||||
import type {
|
||||
AssistantMessage,
|
||||
GlobalEvent,
|
||||
Message,
|
||||
Part,
|
||||
Session,
|
||||
TextPart,
|
||||
ToolPart,
|
||||
ToolState,
|
||||
UserMessage,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import type { EventSource } from "./context/sdk"
|
||||
|
||||
export const names = ["tools-mixed", "subagents"] as const
|
||||
export type Name = (typeof names)[number]
|
||||
|
||||
type Transcript = Array<{ info: Message; parts: Part[] }>
|
||||
type ScenarioEvent = GlobalEvent["payload"]
|
||||
type Step = { after: number; event: ScenarioEvent }
|
||||
|
||||
export function createScenario(input: { name: Name; directory: string; fetch: typeof fetch; speed?: number }) {
|
||||
const sessionID = `ses_tui_scenario_${input.name.replaceAll("-", "_")}`
|
||||
const created = Date.now()
|
||||
const scenario = {
|
||||
session: session(sessionID, input.directory, input.name, created),
|
||||
transcript: input.name === "subagents" ? subagents(sessionID, created) : toolsMixed(sessionID, created),
|
||||
}
|
||||
const childID = `${sessionID}_child`
|
||||
const completedChildID = `${sessionID}_completed_child`
|
||||
const child = {
|
||||
session: { ...session(childID, input.directory, input.name, created), parentID: sessionID },
|
||||
transcript: [
|
||||
{
|
||||
info: userMessage(childID, "msg_child_user", created),
|
||||
parts: [text(childID, "msg_child_user", "part_child_user", "Inspect current delegated work.")],
|
||||
},
|
||||
{
|
||||
info: assistantMessage(childID, "msg_child_assistant", "msg_child_user", created + 1),
|
||||
parts: [
|
||||
tool(
|
||||
childID,
|
||||
"msg_child_assistant",
|
||||
"part_child_read",
|
||||
"read",
|
||||
running(
|
||||
{ filePath: "src/cli/cmd/tui/routes/session/index.tsx" },
|
||||
"src/cli/cmd/tui/routes/session/index.tsx",
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
const completedChild = {
|
||||
session: { ...session(completedChildID, input.directory, input.name, created), parentID: sessionID },
|
||||
transcript: [
|
||||
{
|
||||
info: userMessage(completedChildID, "msg_completed_child_user", created),
|
||||
parts: [text(completedChildID, "msg_completed_child_user", "part_completed_child_user", "Review result.")],
|
||||
},
|
||||
{
|
||||
info: assistantMessage(
|
||||
completedChildID,
|
||||
"msg_completed_child_assistant",
|
||||
"msg_completed_child_user",
|
||||
created + 1,
|
||||
true,
|
||||
),
|
||||
parts: [
|
||||
tool(
|
||||
completedChildID,
|
||||
"msg_completed_child_assistant",
|
||||
"part_completed_child_read",
|
||||
"read",
|
||||
completed({ filePath: "src/cli/cmd/tui/routes/session/index.tsx" }, {}, "Read session renderer"),
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
const children = new Map([
|
||||
[childID, child],
|
||||
[completedChildID, completedChild],
|
||||
])
|
||||
const providers = [
|
||||
{
|
||||
id: "scenario",
|
||||
name: "Scenario",
|
||||
source: "custom",
|
||||
env: [],
|
||||
options: {},
|
||||
models: {
|
||||
scenario: {
|
||||
id: "scenario",
|
||||
providerID: "scenario",
|
||||
api: { id: "scenario", url: "", npm: "" },
|
||||
name: "Preview",
|
||||
capabilities: {
|
||||
temperature: false,
|
||||
reasoning: false,
|
||||
attachment: false,
|
||||
toolcall: true,
|
||||
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||
interleaved: false,
|
||||
},
|
||||
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||
limit: { context: 1, output: 1 },
|
||||
status: "active",
|
||||
options: {},
|
||||
headers: {},
|
||||
release_date: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
const handlers = new Set<(event: GlobalEvent) => void>()
|
||||
const timers = new Set<Timer>()
|
||||
const speed = input.speed && input.speed > 0 ? input.speed : 1
|
||||
let count = 1
|
||||
let eventID = 0
|
||||
|
||||
function emit(event: ScenarioEvent) {
|
||||
eventID += 1
|
||||
event.id = `scenario_event_${eventID}`
|
||||
for (const handler of handlers) {
|
||||
handler({ directory: "global", payload: event })
|
||||
}
|
||||
}
|
||||
|
||||
function play(steps: Step[]) {
|
||||
for (const step of steps) {
|
||||
const timer = setTimeout(() => {
|
||||
timers.delete(timer)
|
||||
emit(step.event)
|
||||
}, step.after / speed)
|
||||
timers.add(timer)
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = Object.assign(
|
||||
async (resource: RequestInfo | URL, init?: RequestInit) => {
|
||||
const request = new Request(resource, init)
|
||||
const pathname = new URL(request.url).pathname
|
||||
const base = `/session/${sessionID}`
|
||||
|
||||
if (request.method === "GET" && pathname === base) return json(scenario.session)
|
||||
if (request.method === "GET" && pathname === `${base}/message`) return json(scenario.transcript)
|
||||
if (request.method === "GET" && (pathname === `${base}/todo` || pathname === `${base}/diff`)) return json([])
|
||||
const child = children.get(pathname.split("/")[2] ?? "")
|
||||
if (request.method === "GET" && child && pathname === `/session/${child.session.id}`) return json(child.session)
|
||||
if (request.method === "GET" && child && pathname === `/session/${child.session.id}/message`)
|
||||
return json(child.transcript)
|
||||
if (
|
||||
request.method === "GET" &&
|
||||
child &&
|
||||
(pathname === `/session/${child.session.id}/todo` || pathname === `/session/${child.session.id}/diff`)
|
||||
)
|
||||
return json([])
|
||||
if (request.method === "POST" && pathname === `${base}/message`) {
|
||||
const prompt = await promptText(request)
|
||||
count += 1
|
||||
const turn = interactiveTurn(sessionID, Date.now(), count, prompt)
|
||||
play(turn.steps)
|
||||
return json({ info: turn.assistant, parts: [] })
|
||||
}
|
||||
|
||||
switch (pathname) {
|
||||
case "/command":
|
||||
case "/experimental/workspace":
|
||||
case "/experimental/workspace/status":
|
||||
case "/formatter":
|
||||
case "/lsp":
|
||||
return json([])
|
||||
case "/agent":
|
||||
return json([
|
||||
{
|
||||
name: "build",
|
||||
description: "Synthetic state preview",
|
||||
mode: "primary",
|
||||
native: true,
|
||||
permission: [],
|
||||
model: { providerID: "scenario", modelID: "scenario" },
|
||||
options: {},
|
||||
},
|
||||
])
|
||||
case "/config":
|
||||
case "/experimental/resource":
|
||||
case "/mcp":
|
||||
case "/provider/auth":
|
||||
return json({})
|
||||
case "/session/status":
|
||||
return json({ [childID]: { type: "busy" }, [completedChildID]: { type: "idle" } })
|
||||
case "/config/providers":
|
||||
return json({ providers, default: { scenario: "scenario" } })
|
||||
case "/experimental/console":
|
||||
return json({ consoleManagedProviders: [], switchableOrgCount: 0 })
|
||||
case "/path":
|
||||
return json({ home: "", state: "", config: "", worktree: input.directory, directory: input.directory })
|
||||
case "/project/current":
|
||||
return json({ id: "scenario" })
|
||||
case "/provider":
|
||||
return json({ all: [], default: { scenario: "scenario" }, connected: ["scenario"] })
|
||||
case "/session":
|
||||
return json([scenario.session])
|
||||
case "/vcs":
|
||||
return json({ branch: "scenario" })
|
||||
}
|
||||
|
||||
throw new Error(`unexpected scenario request: ${request.method} ${pathname}`)
|
||||
},
|
||||
{ preconnect: input.fetch.preconnect },
|
||||
)
|
||||
|
||||
const events: EventSource = {
|
||||
subscribe: async (handler) => {
|
||||
handlers.add(handler)
|
||||
return () => {
|
||||
handlers.delete(handler)
|
||||
if (handlers.size > 0) return
|
||||
for (const timer of timers) clearTimeout(timer)
|
||||
timers.clear()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return { sessionID, fetch, events }
|
||||
}
|
||||
|
||||
function session(sessionID: string, directory: string, name: Name, created: number): Session {
|
||||
return {
|
||||
id: sessionID,
|
||||
slug: `tui-scenario-${name}`,
|
||||
projectID: "global",
|
||||
directory,
|
||||
title: `TUI Scenario: ${name}`,
|
||||
version: "scenario",
|
||||
time: { created, updated: created },
|
||||
}
|
||||
}
|
||||
|
||||
function toolsMixed(sessionID: string, time: number): Transcript {
|
||||
return turn(sessionID, time, 1, "Preview mixed tool rendering states.", [
|
||||
text(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_01_intro",
|
||||
"Block tools should retain space while consecutive inline tools remain dense.",
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_02_shell",
|
||||
"bash",
|
||||
completed(
|
||||
{ command: "git status --short", description: "Show changed files", workdir: "." },
|
||||
{ output: " M packages/opencode/src/cli/cmd/tui/routes/session/index.tsx\n?? scenario-notes.md" },
|
||||
"Show changed files",
|
||||
),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_03_grep",
|
||||
"grep",
|
||||
completed(
|
||||
{ pattern: "InlineTool|BlockTool|renderBefore", path: "packages/opencode/src/cli/cmd/tui/routes/session" },
|
||||
{ matches: 21 },
|
||||
"Found matches",
|
||||
),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_04_glob",
|
||||
"glob",
|
||||
completed({ pattern: "packages/opencode/test/cli/tui/**/*snapshot*" }, { count: 6 }, "Found files"),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_05_read",
|
||||
"read",
|
||||
completed(
|
||||
{ filePath: "packages/opencode/src/cli/cmd/tui/routes/session/index.tsx", offset: 1780, limit: 130 },
|
||||
{},
|
||||
"Read session row renderer",
|
||||
),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_06_edit",
|
||||
"edit",
|
||||
completed(
|
||||
{ filePath: "packages/opencode/src/cli/cmd/tui/routes/session/index.tsx" },
|
||||
{
|
||||
diff: '@@ -1896,1 +1896,2 @@\n- previous?.id.startsWith("text-")\n+ previous?.id.startsWith("text-") ||\n+ previous?.id.startsWith("tool-block-")',
|
||||
diagnostics: {},
|
||||
},
|
||||
"Edit spacing boundary",
|
||||
),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_07_after_block",
|
||||
"grep",
|
||||
completed(
|
||||
{ pattern: "tool-block-", path: "packages/opencode/src/cli/cmd/tui/routes/session/index.tsx" },
|
||||
{ matches: 2 },
|
||||
"Verify block marker",
|
||||
),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_08_error",
|
||||
"read",
|
||||
failed(
|
||||
{ filePath: "packages/opencode/src/cli/cmd/tui/routes/session/missing.tsx" },
|
||||
"Scenario read failed: file does not exist",
|
||||
),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_09_todo",
|
||||
"todowrite",
|
||||
completed(
|
||||
{
|
||||
todos: [
|
||||
{ content: "Inspect spacing states", status: "completed", priority: "high" },
|
||||
{ content: "Review subagent density", status: "in_progress", priority: "high" },
|
||||
],
|
||||
},
|
||||
{
|
||||
todos: [
|
||||
{ content: "Inspect spacing states", status: "completed", priority: "high" },
|
||||
{ content: "Review subagent density", status: "in_progress", priority: "high" },
|
||||
],
|
||||
},
|
||||
"1 todo",
|
||||
),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_10_question",
|
||||
"question",
|
||||
completed(
|
||||
{
|
||||
questions: [
|
||||
{
|
||||
header: "Spacing",
|
||||
question: "Should adjacent completed subagents retain a blank row?",
|
||||
options: [{ label: "Yes", description: "Keep tasks visually distinct" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ answers: [["Yes"]] },
|
||||
"Asked question",
|
||||
),
|
||||
),
|
||||
text(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_11_tip",
|
||||
'Type "agent tool weave" to inspect active and completed task groups, or "parallel subagents" to append another group.',
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
function subagents(sessionID: string, time: number): Transcript {
|
||||
return turn(sessionID, time, 1, "Preview adjacent and parallel subagent rows.", [
|
||||
text(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_01_intro",
|
||||
"These completed task rows are intentionally adjacent so task spacing changes are immediately visible.",
|
||||
),
|
||||
task(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_02_task",
|
||||
"Explore renderer history for task-row regressions",
|
||||
false,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
task(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_03_task",
|
||||
"Compare spacing around long wrapped delegation summaries",
|
||||
true,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
task(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_04_task",
|
||||
"Audit parallel subagent presentation and footer affordances",
|
||||
true,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
text(
|
||||
sessionID,
|
||||
"msg_01_assistant",
|
||||
"part_05_tip",
|
||||
'Type "agent tool weave" to inspect active and completed task groups, or "parallel subagents" to append another group.',
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
function interactiveTurn(sessionID: string, time: number, index: number, prompt: string) {
|
||||
const ids = {
|
||||
user: `msg_${index.toString().padStart(2, "0")}_user`,
|
||||
assistant: `msg_${index.toString().padStart(2, "0")}_assistant`,
|
||||
}
|
||||
const parts = responseParts(sessionID, ids.assistant, prompt)
|
||||
const user = userMessage(sessionID, ids.user, time)
|
||||
const assistant = assistantMessage(sessionID, ids.assistant, ids.user, time + 1)
|
||||
return {
|
||||
assistant,
|
||||
steps: [
|
||||
{ after: 0, event: messageUpdated(sessionID, user) },
|
||||
{ after: 0, event: partUpdated(sessionID, text(sessionID, ids.user, `${ids.user}_text`, prompt)) },
|
||||
{ after: 80, event: messageUpdated(sessionID, assistant) },
|
||||
...parts.map((part, partIndex) => ({ after: 280 + partIndex * 260, event: partUpdated(sessionID, part) })),
|
||||
{
|
||||
after: 400 + parts.length * 260,
|
||||
event: messageUpdated(sessionID, {
|
||||
...assistant,
|
||||
time: { ...assistant.time, completed: Date.now() },
|
||||
finish: "stop",
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function responseParts(sessionID: string, messageID: string, prompt: string): Part[] {
|
||||
if (/sandwich|weave/i.test(prompt)) {
|
||||
return [
|
||||
text(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_01_text`,
|
||||
"Active and completed delegated agent groups among inline tools:",
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_02_grep`,
|
||||
"grep",
|
||||
completed({ pattern: "InlineToolRow" }, { matches: 3 }, "Locate inline row renderer"),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_03_task`,
|
||||
"task",
|
||||
running(
|
||||
{ description: "Trace spacing while surrounding tools stay visible", subagent_type: "explore" },
|
||||
"Delegating",
|
||||
{ sessionId: `${sessionID}_child` },
|
||||
),
|
||||
),
|
||||
task(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_04_task`,
|
||||
"Compare completed task separators after reads",
|
||||
false,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
task(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_05_task`,
|
||||
"Confirm completed agent presentation",
|
||||
true,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_06_glob`,
|
||||
"glob",
|
||||
completed({ pattern: "test/cli/tui/**/*.snap" }, { count: 4 }, "Locate snapshots"),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_07_task`,
|
||||
"task",
|
||||
completed(
|
||||
{ description: "Audit background task while its child is active", subagent_type: "explore" },
|
||||
{ background: true, sessionId: `${sessionID}_child` },
|
||||
"Audit background task while its child is active",
|
||||
),
|
||||
),
|
||||
task(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_08_task`,
|
||||
"Confirm secondary completed agent presentation",
|
||||
false,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_09_grep`,
|
||||
"grep",
|
||||
completed(
|
||||
{ pattern: "spinner", path: "src/cli/cmd/tui/routes/session/index.tsx" },
|
||||
{ matches: 3 },
|
||||
"Verify spinner path",
|
||||
),
|
||||
),
|
||||
]
|
||||
}
|
||||
if (/parallel|subagent|task/i.test(prompt)) {
|
||||
return [
|
||||
text(sessionID, messageID, `${messageID}_01_text`, "Three delegated investigations returned concurrently:"),
|
||||
task(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_02_task`,
|
||||
"Review layout behavior in the session renderer",
|
||||
false,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
task(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_03_task`,
|
||||
"Audit wrapping behavior for long task summaries",
|
||||
true,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
task(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_04_task`,
|
||||
"Check task spacing against inline tool density",
|
||||
true,
|
||||
`${sessionID}_completed_child`,
|
||||
),
|
||||
]
|
||||
}
|
||||
if (/block|diff|edit/i.test(prompt)) {
|
||||
return [
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_01_shell`,
|
||||
"bash",
|
||||
completed(
|
||||
{ command: "git diff --stat", description: "Inspect diff" },
|
||||
{ output: " session/index.tsx | 4 ++--" },
|
||||
"Inspect diff",
|
||||
),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_02_read`,
|
||||
"read",
|
||||
completed({ filePath: "src/session/index.tsx" }, {}, "Read source"),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_03_grep`,
|
||||
"grep",
|
||||
completed({ pattern: "InlineTool" }, { matches: 4 }, "Search source"),
|
||||
),
|
||||
]
|
||||
}
|
||||
if (/error|fail|denied/i.test(prompt)) {
|
||||
return [
|
||||
tool(sessionID, messageID, `${messageID}_01_error`, "read", failed({ filePath: "missing.ts" }, "File not found")),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_02_read`,
|
||||
"read",
|
||||
completed({ filePath: "recovered.ts" }, {}, "Read fallback"),
|
||||
),
|
||||
]
|
||||
}
|
||||
if (/pending|running|spinner/i.test(prompt)) {
|
||||
return [
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_01_read`,
|
||||
"read",
|
||||
running({ filePath: "src/session/index.tsx" }, "Reading source"),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_02_task`,
|
||||
"task",
|
||||
running({ description: "Inspect running agent", subagent_type: "explore" }, "Delegating"),
|
||||
),
|
||||
]
|
||||
}
|
||||
return [
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_01_grep`,
|
||||
"grep",
|
||||
completed({ pattern: prompt }, { matches: 12 }, "Search preview"),
|
||||
),
|
||||
tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
`${messageID}_02_read`,
|
||||
"read",
|
||||
completed({ filePath: "src/cli/cmd/tui/routes/session/index.tsx" }, {}, "Read preview"),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
function turn(sessionID: string, time: number, index: number, prompt: string, parts: Part[]): Transcript {
|
||||
const userID = `msg_${index.toString().padStart(2, "0")}_user`
|
||||
const assistantID = `msg_${index.toString().padStart(2, "0")}_assistant`
|
||||
return [
|
||||
{
|
||||
info: userMessage(sessionID, userID, time),
|
||||
parts: [text(sessionID, userID, `${userID}_text`, prompt)],
|
||||
},
|
||||
{
|
||||
info: assistantMessage(sessionID, assistantID, userID, time + 1, true),
|
||||
parts,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function userMessage(sessionID: string, id: string, time: number): UserMessage {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: { created: time },
|
||||
agent: "build",
|
||||
model: { providerID: "scenario", modelID: "scenario" },
|
||||
}
|
||||
}
|
||||
|
||||
function assistantMessage(
|
||||
sessionID: string,
|
||||
id: string,
|
||||
parentID: string,
|
||||
time: number,
|
||||
complete = false,
|
||||
): AssistantMessage {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
role: "assistant",
|
||||
time: { created: time, ...(complete ? { completed: time + 500 } : {}) },
|
||||
parentID,
|
||||
modelID: "scenario",
|
||||
providerID: "scenario",
|
||||
mode: "build",
|
||||
agent: "build",
|
||||
path: { cwd: process.cwd(), root: process.cwd() },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
...(complete ? { finish: "stop" } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function text(sessionID: string, messageID: string, id: string, value: string): TextPart {
|
||||
return { id, sessionID, messageID, type: "text", text: value }
|
||||
}
|
||||
|
||||
function task(
|
||||
sessionID: string,
|
||||
messageID: string,
|
||||
id: string,
|
||||
description: string,
|
||||
background: boolean,
|
||||
childSessionID?: string,
|
||||
) {
|
||||
return tool(
|
||||
sessionID,
|
||||
messageID,
|
||||
id,
|
||||
"task",
|
||||
completed(
|
||||
{ description, subagent_type: "explore" },
|
||||
{ background, ...(childSessionID ? { sessionId: childSessionID } : {}) },
|
||||
description,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function tool(sessionID: string, messageID: string, id: string, name: string, state: ToolState): ToolPart {
|
||||
return { id, sessionID, messageID, type: "tool", callID: `call_${id}`, tool: name, state }
|
||||
}
|
||||
|
||||
function completed(input: Record<string, unknown>, metadata: Record<string, unknown>, title: string): ToolState {
|
||||
return {
|
||||
status: "completed",
|
||||
input,
|
||||
output: title,
|
||||
title,
|
||||
metadata,
|
||||
time: { start: Date.now(), end: Date.now() + 1 },
|
||||
}
|
||||
}
|
||||
|
||||
function running(
|
||||
input: Record<string, unknown>,
|
||||
title: string,
|
||||
metadata: Record<string, unknown> = {},
|
||||
): Extract<ToolState, { status: "running" }> {
|
||||
return { status: "running", input, title, metadata, time: { start: Date.now() } }
|
||||
}
|
||||
|
||||
function failed(input: Record<string, unknown>, error: string): ToolState {
|
||||
return { status: "error", input, error, time: { start: Date.now(), end: Date.now() + 1 } }
|
||||
}
|
||||
|
||||
function messageUpdated(sessionID: string, info: Message): ScenarioEvent {
|
||||
return { id: "scenario", type: "message.updated", properties: { sessionID, info } }
|
||||
}
|
||||
|
||||
function partUpdated(sessionID: string, part: Part): ScenarioEvent {
|
||||
return { id: "scenario", type: "message.part.updated", properties: { sessionID, part, time: Date.now() } }
|
||||
}
|
||||
|
||||
async function promptText(request: Request) {
|
||||
const body: unknown = await request.json()
|
||||
if (!body || typeof body !== "object" || !("parts" in body) || !Array.isArray(body.parts)) return "Preview tools"
|
||||
return body.parts
|
||||
.flatMap((part) => {
|
||||
if (!part || typeof part !== "object" || !("type" in part) || part.type !== "text" || !("text" in part)) return []
|
||||
return typeof part.text === "string" ? [part.text] : []
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function json(data: unknown) {
|
||||
return new Response(JSON.stringify(data), { headers: { "content-type": "application/json" } })
|
||||
}
|
||||
@@ -111,6 +111,16 @@ export const TuiThreadCommand = cmd({
|
||||
.option("agent", {
|
||||
type: "string",
|
||||
describe: "agent to use",
|
||||
})
|
||||
.option("scenario", {
|
||||
type: "string",
|
||||
choices: ["tools-mixed", "subagents"] as const,
|
||||
describe: "render a synthetic TUI state preview",
|
||||
})
|
||||
.option("scenario-speed", {
|
||||
type: "number",
|
||||
default: 1,
|
||||
describe: "playback speed for synthetic TUI responses",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
// Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it.
|
||||
@@ -127,6 +137,27 @@ export const TuiThreadCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
const network = resolveNetworkOptionsNoConfig(args)
|
||||
const external =
|
||||
process.argv.includes("--port") ||
|
||||
process.argv.includes("--hostname") ||
|
||||
process.argv.includes("--mdns") ||
|
||||
network.mdns ||
|
||||
network.port !== 0 ||
|
||||
network.hostname !== "127.0.0.1"
|
||||
|
||||
if (args.scenario && external) {
|
||||
UI.error("--scenario is only available with the internal transport")
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
if (args.scenario && (args.continue || args.session || args.fork)) {
|
||||
UI.error("--scenario cannot be combined with --continue, --session, or --fork")
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve relative --project paths from PWD, then use the real cwd after
|
||||
// chdir so the thread and worker share the same directory key.
|
||||
const next = resolveThreadDirectory(args.project)
|
||||
@@ -189,15 +220,6 @@ export const TuiThreadCommand = cmd({
|
||||
const prompt = await input(args.prompt)
|
||||
const config = await TuiConfig.get()
|
||||
|
||||
const network = resolveNetworkOptionsNoConfig(args)
|
||||
const external =
|
||||
process.argv.includes("--port") ||
|
||||
process.argv.includes("--hostname") ||
|
||||
process.argv.includes("--mdns") ||
|
||||
network.mdns ||
|
||||
network.port !== 0 ||
|
||||
network.hostname !== "127.0.0.1"
|
||||
|
||||
const transport = external
|
||||
? {
|
||||
url: (await client.call("server", network)).url,
|
||||
@@ -210,12 +232,24 @@ export const TuiThreadCommand = cmd({
|
||||
events: createEventSource(client),
|
||||
}
|
||||
|
||||
const scenario = args.scenario
|
||||
? (await import("./scenario")).createScenario({
|
||||
name: args.scenario,
|
||||
directory: cwd,
|
||||
fetch: transport.fetch!,
|
||||
speed: args.scenarioSpeed,
|
||||
})
|
||||
: undefined
|
||||
const sessionID = scenario?.sessionID ?? args.session
|
||||
const fetch = scenario?.fetch ?? transport.fetch
|
||||
const events = scenario?.events ?? transport.events
|
||||
|
||||
try {
|
||||
await validateSession({
|
||||
url: transport.url,
|
||||
sessionID: args.session,
|
||||
sessionID,
|
||||
directory: cwd,
|
||||
fetch: transport.fetch,
|
||||
fetch,
|
||||
})
|
||||
} catch (error) {
|
||||
UI.error(errorMessage(error))
|
||||
@@ -223,9 +257,10 @@ export const TuiThreadCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
||||
}, 1000).unref?.()
|
||||
if (!args.scenario)
|
||||
setTimeout(() => {
|
||||
client.call("checkUpgrade", { directory: cwd }).catch(() => {})
|
||||
}, 1000).unref?.()
|
||||
|
||||
try {
|
||||
const { createTuiRenderer, tui } = await import("./app")
|
||||
@@ -240,11 +275,11 @@ export const TuiThreadCommand = cmd({
|
||||
},
|
||||
config,
|
||||
directory: cwd,
|
||||
fetch: transport.fetch,
|
||||
events: transport.events,
|
||||
fetch,
|
||||
events,
|
||||
args: {
|
||||
continue: args.continue,
|
||||
sessionID: args.session,
|
||||
sessionID,
|
||||
agent: args.agent,
|
||||
model: args.model,
|
||||
prompt,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Event = {
|
||||
Connected: EventV2.define({ type: "server.connected", schema: {} }),
|
||||
Heartbeat: EventV2.define({ type: "server.heartbeat", schema: {} }),
|
||||
InstanceDisposed: EventV2.define({ type: "server.instance.disposed", schema: { directory: Schema.String } }),
|
||||
Disposed: EventV2.define({ type: "global.disposed", schema: {} }),
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ControlApi } from "./groups/control"
|
||||
import { EventApi } from "./groups/event"
|
||||
import { ExperimentalApi } from "./groups/experimental"
|
||||
import { FileApi } from "./groups/file"
|
||||
import { GlobalApi } from "./groups/global"
|
||||
import { InstanceApi } from "./groups/instance"
|
||||
import { McpApi } from "./groups/mcp"
|
||||
import { PermissionApi } from "./groups/permission"
|
||||
@@ -19,6 +18,8 @@ import { SyncApi } from "./groups/sync"
|
||||
import { TuiApi } from "./groups/tui"
|
||||
import { WorkspaceApi } from "./groups/workspace"
|
||||
import { V2Api } from "./groups/v2"
|
||||
// GlobalEventSchema snapshots the registry, after event-producing groups register their variants.
|
||||
import { GlobalApi } from "./groups/global"
|
||||
import { Authorization } from "./middleware/authorization"
|
||||
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import "@/server/event"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { described } from "./metadata"
|
||||
import { ReplayEvent } from "./sync"
|
||||
|
||||
const GlobalHealth = Schema.Struct({
|
||||
healthy: Schema.Literal(true),
|
||||
@@ -14,14 +15,19 @@ const GlobalEventSchema = Schema.Struct({
|
||||
directory: Schema.String,
|
||||
project: Schema.optional(Schema.String),
|
||||
workspace: Schema.optional(Schema.String),
|
||||
payload: Schema.Union(
|
||||
EventV2.registry
|
||||
payload: Schema.Union([
|
||||
...EventV2.registry
|
||||
.values()
|
||||
.map((definition) =>
|
||||
Schema.Struct({ id: Schema.String, type: Schema.Literal(definition.type), properties: definition.data }),
|
||||
)
|
||||
.toArray(),
|
||||
),
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
type: Schema.Literal("sync"),
|
||||
syncEvent: Schema.optional(ReplayEvent),
|
||||
}),
|
||||
]),
|
||||
}).annotate({ identifier: "GlobalEvent" })
|
||||
|
||||
export const GlobalUpgradeInput = Schema.Struct({
|
||||
|
||||
@@ -52,3 +52,21 @@ exports[`TUI inline tool wrapping keeps separation after a padded user message 1
|
||||
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
|
||||
Path\\.data|data =" in packages/opencode/src (115 matches)"
|
||||
`;
|
||||
|
||||
exports[`TUI inline tool wrapping separates a contiguous task group from inline tools 1`] = `
|
||||
" ✱ Grep "Task" (2 matches)
|
||||
|
||||
⠙ Explore Task — Inspect active task spacing
|
||||
✓ General Task — Confirm completed task spacing
|
||||
↳ 1 toolcall · 501ms
|
||||
|
||||
→ Read src/cli/cmd/tui/routes/session/index.tsx"
|
||||
`;
|
||||
|
||||
exports[`TUI inline tool wrapping separates a task group after an expanded read 1`] = `
|
||||
" → Read src/cli/cmd/tui/routes/session/index.tsx
|
||||
↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx
|
||||
|
||||
✓ Explore Task — Inspect active task spacing
|
||||
↳ 1 toolcall · 501ms"
|
||||
`;
|
||||
|
||||
@@ -86,6 +86,41 @@ function Fixture(props: { errorExpanded?: boolean; before?: "shell" | "user" })
|
||||
)
|
||||
}
|
||||
|
||||
function TaskGroupFixture() {
|
||||
return (
|
||||
<box flexDirection="column" width={72}>
|
||||
<InlineToolRow id="tool-inline-before" icon="✱" complete={true} pending="">
|
||||
Grep "Task" (2 matches)
|
||||
</InlineToolRow>
|
||||
<InlineToolRow id="tool-inline-task-one" icon="⠙" complete={true} pending="" task={true}>
|
||||
Explore Task — Inspect active task spacing
|
||||
</InlineToolRow>
|
||||
<InlineToolRow id="tool-inline-task-two" icon="✓" complete={true} pending="" task={true}>
|
||||
{"General Task — Confirm completed task spacing\n↳ 1 toolcall · 501ms"}
|
||||
</InlineToolRow>
|
||||
<InlineToolRow id="tool-inline-after" icon="→" complete={true} pending="">
|
||||
Read src/cli/cmd/tui/routes/session/index.tsx
|
||||
</InlineToolRow>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadedReadBeforeTaskFixture() {
|
||||
return (
|
||||
<box flexDirection="column" width={72}>
|
||||
<InlineToolRow id="tool-inline-read" icon="→" complete={true} pending="">
|
||||
Read src/cli/cmd/tui/routes/session/index.tsx
|
||||
</InlineToolRow>
|
||||
<box id="tool-inline-loaded-read-child" paddingLeft={3}>
|
||||
<text paddingLeft={3}>↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx</text>
|
||||
</box>
|
||||
<InlineToolRow id="tool-inline-task-after-read" icon="✓" complete={true} pending="" task={true}>
|
||||
{"Explore Task — Inspect active task spacing\n↳ 1 toolcall · 501ms"}
|
||||
</InlineToolRow>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
|
||||
testSetup = await testRender(component, options)
|
||||
await testSetup.renderOnce()
|
||||
@@ -116,4 +151,12 @@ describe("TUI inline tool wrapping", () => {
|
||||
test("keeps separation after a padded user message", async () => {
|
||||
expect(await renderFrame(() => <Fixture before="user" />, { width: 72, height: 14 })).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("separates a contiguous task group from inline tools", async () => {
|
||||
expect(await renderFrame(() => <TaskGroupFixture />, { width: 72, height: 10 })).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("separates a task group after an expanded read", async () => {
|
||||
expect(await renderFrame(() => <LoadedReadBeforeTaskFixture />, { width: 72, height: 8 })).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,11 +76,13 @@ export type Event =
|
||||
| EventPtyDeleted
|
||||
| EventInstallationUpdated
|
||||
| EventInstallationUpdateAvailable
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventAccountAdded
|
||||
| EventAccountRemoved
|
||||
| EventAccountSwitched
|
||||
| EventServerConnected
|
||||
| EventServerHeartbeat
|
||||
| EventServerInstanceDisposed
|
||||
| EventGlobalDisposed
|
||||
|
||||
export type OAuth = {
|
||||
type: "oauth"
|
||||
@@ -1389,6 +1391,29 @@ export type GlobalEvent = {
|
||||
version: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "account.added"
|
||||
properties: {
|
||||
account: AuthInfo
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "account.removed"
|
||||
properties: {
|
||||
account: AuthInfo
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "account.switched"
|
||||
properties: {
|
||||
serviceID: string
|
||||
from?: string
|
||||
to?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
@@ -1396,6 +1421,20 @@ export type GlobalEvent = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "server.heartbeat"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "server.instance.disposed"
|
||||
properties: {
|
||||
directory: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "global.disposed"
|
||||
@@ -1403,6 +1442,19 @@ export type GlobalEvent = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
type: "sync"
|
||||
syncEvent?: {
|
||||
id: string
|
||||
aggregateID: string
|
||||
seq: number
|
||||
type: string
|
||||
data: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3229,6 +3281,30 @@ export type PermissionV2Rule = {
|
||||
|
||||
export type PermissionV2Ruleset = Array<PermissionV2Rule>
|
||||
|
||||
export type AuthOAuthCredential = {
|
||||
type: "oauth"
|
||||
refresh: string
|
||||
access: string
|
||||
expires: number
|
||||
}
|
||||
|
||||
export type AuthApiKeyCredential = {
|
||||
type: "api"
|
||||
key: string
|
||||
metadata?: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type AuthCredential = AuthOAuthCredential | AuthApiKeyCredential
|
||||
|
||||
export type AuthInfo = {
|
||||
id: string
|
||||
serviceID: string
|
||||
description: string
|
||||
credential: AuthCredential
|
||||
}
|
||||
|
||||
export type PolicyEffect = "allow" | "deny"
|
||||
|
||||
export type ConfigV2ExperimentalPolicy = {
|
||||
@@ -4359,46 +4435,6 @@ export type EventInstallationUpdateAvailable = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventGlobalDisposed = {
|
||||
id: string
|
||||
type: "global.disposed"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type AuthOAuthCredential = {
|
||||
type: "oauth"
|
||||
refresh: string
|
||||
access: string
|
||||
expires: number
|
||||
}
|
||||
|
||||
export type AuthApiKeyCredential = {
|
||||
type: "api"
|
||||
key: string
|
||||
metadata?: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type AuthCredential = AuthOAuthCredential | AuthApiKeyCredential
|
||||
|
||||
export type AuthInfo = {
|
||||
id: string
|
||||
serviceID: string
|
||||
description: string
|
||||
credential: AuthCredential
|
||||
}
|
||||
|
||||
export type EventAccountAdded = {
|
||||
id: string
|
||||
type: "account.added"
|
||||
@@ -4425,6 +4461,38 @@ export type EventAccountSwitched = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerHeartbeat = {
|
||||
id: string
|
||||
type: "server.heartbeat"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerInstanceDisposed = {
|
||||
id: string
|
||||
type: "server.instance.disposed"
|
||||
properties: {
|
||||
directory: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventGlobalDisposed = {
|
||||
id: string
|
||||
type: "global.disposed"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type BadRequestError = {
|
||||
name: "BadRequest"
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user