From 592c5960aa677555552b9bedbd8a5619ca95318d Mon Sep 17 00:00:00 2001 From: Patrick Hall Date: Sat, 10 Jan 2026 12:39:12 -0500 Subject: [PATCH] feat(tui): add /instructions command to show loaded instruction files Add a slash command that displays which instruction files (AGENTS.md, CLAUDE.md, config-specified files, URLs) are loaded for the current session. Changes: - Extract path discovery from SystemPrompt.custom() into reusable paths() function - Add /instructions server endpoint via separate route file (avoids TS2589) - Add DialogInstructions TUI component - Register command in autocomplete and command palette --- packages/opencode/src/cli/cmd/tui/app.tsx | 9 +++ .../cmd/tui/component/dialog-instructions.tsx | 66 +++++++++++++++++++ .../cmd/tui/component/prompt/autocomplete.tsx | 5 ++ packages/opencode/src/server/instructions.ts | 35 ++++++++++ packages/opencode/src/server/server.ts | 2 + packages/opencode/src/session/system.ts | 20 ++++-- packages/sdk/js/src/v2/gen/sdk.gen.ts | 24 +++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 23 +++++++ 8 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-instructions.tsx create mode 100644 packages/opencode/src/server/instructions.ts diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index aa62c6c58ef..2b3ef6bc760 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -13,6 +13,7 @@ import { LocalProvider, useLocal } from "@tui/context/local" import { DialogModel, useConnected } from "@tui/component/dialog-model" import { DialogMcp } from "@tui/component/dialog-mcp" import { DialogStatus } from "@tui/component/dialog-status" +import { DialogInstructions } from "@tui/component/dialog-instructions" import { DialogThemeList } from "@tui/component/dialog-theme-list" import { DialogHelp } from "./ui/dialog-help" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" @@ -426,6 +427,14 @@ function App() { }, category: "System", }, + { + title: "View instructions", + value: "opencode.instructions", + onSelect: () => { + dialog.replace(() => ) + }, + category: "System", + }, { title: "Switch theme", value: "theme.switch", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-instructions.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-instructions.tsx new file mode 100644 index 00000000000..28bbeb37534 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-instructions.tsx @@ -0,0 +1,66 @@ +import { TextAttributes } from "@opentui/core" +import { useTheme } from "../context/theme" +import { useSDK } from "@tui/context/sdk" +import { For, Show, createResource } from "solid-js" + +export function DialogInstructions() { + const sdk = useSDK() + const { theme } = useTheme() + + const [instructions] = createResource(async () => { + const result = await sdk.client.instructions.list() + return result.data + }) + + return ( + + + + Instructions + + esc + + Loading...}> + 0} + fallback={No instruction files loaded} + > + + + Files + + {(file) => ( + + + • + + + {file} + + + )} + + + + + + URLs + + {(url) => ( + + + • + + + {url} + + + )} + + + + + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 3c757f81b18..b973f0adcc1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -424,6 +424,11 @@ export function Autocomplete(props: { description: "show status", onSelect: () => command.trigger("opencode.status"), }, + { + display: "/instructions", + description: "show loaded instructions", + onSelect: () => command.trigger("opencode.instructions"), + }, { display: "/mcp", description: "toggle MCPs", diff --git a/packages/opencode/src/server/instructions.ts b/packages/opencode/src/server/instructions.ts new file mode 100644 index 00000000000..09c3c853bca --- /dev/null +++ b/packages/opencode/src/server/instructions.ts @@ -0,0 +1,35 @@ +import { Hono } from "hono" +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi" +import { SystemPrompt } from "../session/system" +import z from "zod" + +export const InstructionsRoute = new Hono().get( + "/", + describeRoute({ + summary: "List instructions", + description: "Get a list of all instruction files loaded for the current session.", + operationId: "instructions.list", + responses: { + 200: { + description: "List of instruction sources", + content: { + "application/json": { + schema: resolver( + z + .object({ + files: z.array(z.string()), + urls: z.array(z.string()), + }) + .meta({ ref: "Instructions" }), + ), + }, + }, + }, + }, + }), + async (c) => { + const result = await SystemPrompt.paths() + return c.json(result) + }, +) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 32d7a179555..3fa50784ca8 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -33,6 +33,7 @@ import { ToolRegistry } from "../tool/registry" import { zodToJsonSchema } from "zod-to-json-schema" import { SessionPrompt } from "../session/prompt" import { SessionCompaction } from "../session/compaction" +import { InstructionsRoute } from "./instructions" import { SessionRevert } from "../session/revert" import { lazy } from "../util/lazy" import { Todo } from "../session/todo" @@ -279,6 +280,7 @@ export namespace Server { .use(validator("query", z.object({ directory: z.string().optional() }))) .route("/project", ProjectRoute) + .route("/instructions", InstructionsRoute) .get( "/pty", diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index fff90808864..188bd08c71a 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -76,26 +76,26 @@ export namespace SystemPrompt { GLOBAL_RULE_FILES.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md")) } - export async function custom() { + export async function paths() { const config = await Config.get() - const paths = new Set() + const files = new Set() + const urls: string[] = [] for (const localRuleFile of LOCAL_RULE_FILES) { const matches = await Filesystem.findUp(localRuleFile, Instance.directory, Instance.worktree) if (matches.length > 0) { - matches.forEach((path) => paths.add(path)) + matches.forEach((p) => files.add(p)) break } } for (const globalRuleFile of GLOBAL_RULE_FILES) { if (await Bun.file(globalRuleFile).exists()) { - paths.add(globalRuleFile) + files.add(globalRuleFile) break } } - const urls: string[] = [] if (config.instructions) { for (let instruction of config.instructions) { if (instruction.startsWith("https://") || instruction.startsWith("http://")) { @@ -117,11 +117,17 @@ export namespace SystemPrompt { } else { matches = await Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => []) } - matches.forEach((path) => paths.add(path)) + matches.forEach((p) => files.add(p)) } } - const foundFiles = Array.from(paths).map((p) => + return { files: Array.from(files), urls } + } + + export async function custom() { + const { files, urls } = await paths() + + const foundFiles = files.map((p) => Bun.file(p) .text() .catch(() => "") diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f83913ea5e1..43671c27706 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -35,6 +35,7 @@ import type { GlobalEventResponses, GlobalHealthResponses, InstanceDisposeResponses, + InstructionsListResponses, LspStatusResponses, McpAddErrors, McpAddResponses, @@ -327,6 +328,27 @@ export class Project extends HeyApiClient { } } +export class Instructions extends HeyApiClient { + /** + * List instructions + * + * Get a list of all instruction files loaded for the current session. + */ + public list( + parameters?: { + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) + return (options?.client ?? this.client).get({ + url: "/instructions", + ...options, + ...params, + }) + } +} + export class Pty extends HeyApiClient { /** * List PTY sessions @@ -2986,6 +3008,8 @@ export class OpencodeClient extends HeyApiClient { project = new Project({ client: this.client }) + instructions = new Instructions({ client: this.client }) + pty = new Pty({ client: this.client }) config = new Config({ client: this.client }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e423fecea42..6c2fc449546 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -901,6 +901,11 @@ export type NotFoundError = { } } +export type Instructions = { + files: Array + urls: Array +} + /** * Custom keybind configurations */ @@ -2205,6 +2210,24 @@ export type ProjectUpdateResponses = { export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] +export type InstructionsListData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/instructions" +} + +export type InstructionsListResponses = { + /** + * List of instruction sources + */ + 200: Instructions +} + +export type InstructionsListResponse = InstructionsListResponses[keyof InstructionsListResponses] + export type PtyListData = { body?: never path?: never