diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4b177e292cf..f3c10f821dc 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" @@ -447,6 +448,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/server/routes/instructions.ts b/packages/opencode/src/server/routes/instructions.ts new file mode 100644 index 00000000000..ea927fce17c --- /dev/null +++ b/packages/opencode/src/server/routes/instructions.ts @@ -0,0 +1,38 @@ +import { Hono } from "hono" +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi" +import { SystemPrompt } from "../../session/system" +import z from "zod" +import { lazy } from "../../util/lazy" + +export const InstructionsRoutes = lazy(() => + 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 fa646f21ea8..dead50de386 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -29,6 +29,7 @@ import { FileRoutes } from "./routes/file" import { ConfigRoutes } from "./routes/config" import { ExperimentalRoutes } from "./routes/experimental" import { ProviderRoutes } from "./routes/provider" +import { InstructionsRoutes } from "./routes/instructions" import { lazy } from "../util/lazy" import { InstanceBootstrap } from "../project/bootstrap" import { Storage } from "../storage/storage" @@ -152,6 +153,7 @@ export namespace Server { ) .use(validator("query", z.object({ directory: z.string().optional() }))) .route("/project", ProjectRoutes()) + .route("/instructions", InstructionsRoutes()) .route("/pty", PtyRoutes()) .route("/config", ConfigRoutes()) .route("/experimental", ExperimentalRoutes()) diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 8d619357a4f..c721363b284 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -92,16 +92,17 @@ 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[] = [] // Only scan local rule files when project discovery is enabled if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { 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 } } @@ -109,12 +110,11 @@ export namespace SystemPrompt { 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://")) { @@ -136,11 +136,17 @@ export namespace SystemPrompt { } else { matches = await resolveRelativeInstruction(instruction) } - 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 67e7ac80cb9..47499de8e9b 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -36,6 +36,7 @@ import type { GlobalEventResponses, GlobalHealthResponses, InstanceDisposeResponses, + InstructionsListResponses, LspStatusResponses, McpAddErrors, McpAddResponses, @@ -341,6 +342,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 @@ -3131,6 +3153,11 @@ export class OpencodeClient extends HeyApiClient { return (this._project ??= new Project({ client: this.client })) } + private _instructions?: Instructions + get instructions(): Instructions { + return (this._instructions ??= new Instructions({ client: this.client })) + } + private _pty?: Pty get pty(): Pty { return (this._pty ??= new Pty({ 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 38a52b325ad..22aa1843fcf 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -945,6 +945,11 @@ export type NotFoundError = { } } +export type Instructions = { + files: Array + urls: Array +} + /** * Custom keybind configurations */ @@ -2297,6 +2302,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