Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -426,6 +427,14 @@ function App() {
},
category: "System",
},
{
title: "View instructions",
value: "opencode.instructions",
onSelect: () => {
dialog.replace(() => <DialogInstructions />)
},
category: "System",
},
{
title: "Switch theme",
value: "theme.switch",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text} attributes={TextAttributes.BOLD}>
Instructions
</text>
<text fg={theme.textMuted}>esc</text>
</box>
<Show when={!instructions.loading} fallback={<text fg={theme.textMuted}>Loading...</text>}>
<Show
when={(instructions()?.files?.length ?? 0) + (instructions()?.urls?.length ?? 0) > 0}
fallback={<text fg={theme.textMuted}>No instruction files loaded</text>}
>
<Show when={instructions()?.files?.length}>
<box>
<text fg={theme.text}>Files</text>
<For each={instructions()?.files}>
{(file) => (
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={theme.success}>
</text>
<text fg={theme.text} wrapMode="word">
{file}
</text>
</box>
)}
</For>
</box>
</Show>
<Show when={instructions()?.urls?.length}>
<box>
<text fg={theme.text}>URLs</text>
<For each={instructions()?.urls}>
{(url) => (
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={theme.success}>
</text>
<text fg={theme.text} wrapMode="word">
{url}
</text>
</box>
)}
</For>
</box>
</Show>
</Show>
</Show>
</box>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,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",
Expand Down
35 changes: 35 additions & 0 deletions packages/opencode/src/server/instructions.ts
Original file line number Diff line number Diff line change
@@ -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)
},
)
2 changes: 2 additions & 0 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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"
Expand Down Expand Up @@ -289,6 +290,7 @@ export namespace Server {
.use(validator("query", z.object({ directory: z.string().optional() })))

.route("/project", ProjectRoute)
.route("/instructions", InstructionsRoute)

.get(
"/pty",
Expand Down
20 changes: 13 additions & 7 deletions packages/opencode/src/session/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>()
const files = new Set<string>()
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://")) {
Expand All @@ -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(() => "")
Expand Down
24 changes: 24 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type {
GlobalEventResponses,
GlobalHealthResponses,
InstanceDisposeResponses,
InstructionsListResponses,
LspStatusResponses,
McpAddErrors,
McpAddResponses,
Expand Down Expand Up @@ -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<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
return (options?.client ?? this.client).get<InstructionsListResponses, unknown, ThrowOnError>({
url: "/instructions",
...options,
...params,
})
}
}

export class Pty extends HeyApiClient {
/**
* List PTY sessions
Expand Down Expand Up @@ -2988,6 +3010,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 })
Expand Down
23 changes: 23 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,11 @@ export type NotFoundError = {
}
}

export type Instructions = {
files: Array<string>
urls: Array<string>
}

/**
* Custom keybind configurations
*/
Expand Down Expand Up @@ -2210,6 +2215,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
Expand Down