Skip to content

Commit 4bfabba

Browse files
committed
feat(tui): copy root path on click in context sidebar
1 parent 6430cf4 commit 4bfabba

5 files changed

Lines changed: 22 additions & 4 deletions

File tree

‎PATCH.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Clicking a markdown link to a directory (trailing `/`, e.g. `[core](packages/cor
127127

128128
## Context Sidebar
129129

130-
Plugin (`custom.context` + TUI `custom.sidebar.context`): the sidebar lists the current session's context as **Instructions** (ambient and session-injected instruction files; home paths show as `~`, and a symlinked global config dir shows the configured path), **Skills** (by name: mentions, `skill` messages, and completed `skill` tool calls in the post-compaction window, plus skills from `custom.skills.auto-load` via `custom-auto-load-skills`), and one node per session root. The backend takes each root's Instructions from `SessionRoots` (the same files the model receives) and groups loaded Skills by the root that provides them (a skill the primary Location also has stays under the primary Skills). Each root renders as a top-level node next to Instructions and Skills, labelled with its directory (displayed like other paths: project-relative, else `~`); there is no explicit "Roots" header. Root nodes start collapsed and a click on the root's label toggles its contents (no icons; the state is not persisted). Empty subsections are hidden; Instructions and Skills never collapse.
130+
Plugin (`custom.context` + TUI `custom.sidebar.context`): the sidebar lists the current session's context as **Instructions** (ambient and session-injected instruction files; home paths show as `~`, and a symlinked global config dir shows the configured path), **Skills** (by name: mentions, `skill` messages, and completed `skill` tool calls in the post-compaction window, plus skills from `custom.skills.auto-load` via `custom-auto-load-skills`), and one node per session root. The backend takes each root's Instructions from `SessionRoots` (the same files the model receives) and groups loaded Skills by the root that provides them (a skill the primary Location also has stays under the primary Skills). Each root renders as a top-level node next to Instructions and Skills, labelled with its directory (displayed like other paths: project-relative, else `~`); there is no explicit "Roots" header. Root nodes start collapsed and a click on the root's label toggles its contents and copies the root's absolute path (with a toast; no icons; the open state is not persisted). Empty subsections are hidden; Instructions and Skills never collapse.
131131

132132
## Pull Request Sidebar
133133

‎packages/core/src/plugin/custom/context.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export function contextRoot(
108108
skills: ReadonlyArray<string>,
109109
) {
110110
return {
111+
path: directory,
111112
directory: display(project, home, config, configReal, directory),
112113
files: [...new Set(files)].map((file) => display(directory, home, config, configReal, file)),
113114
skills: [...skills],

‎packages/core/test/plugin/custom.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,19 @@ it.effect("groups a root's instructions and skills under its own directory", ()
131131
["review", "deploy"],
132132
),
133133
).toEqual({
134+
path: `${home}/src/cmux`,
134135
directory: "~/src/cmux",
135136
files: ["AGENTS.md", "web/AGENTS.md", "~/src/AGENTS.md"],
136137
skills: ["review", "deploy"],
137138
})
138139
expect(CustomContextPlugin.contextRoot("/repo", "/opt/shared", home, config, config, [], [])).toEqual({
140+
path: "/opt/shared",
139141
directory: "/opt/shared",
140142
files: [],
141143
skills: [],
142144
})
143145
expect(CustomContextPlugin.contextRoot("/repo", "/repo/packages/web", home, config, config, [], [])).toEqual({
146+
path: "/repo/packages/web",
144147
directory: "packages/web",
145148
files: [],
146149
skills: [],

‎packages/schema/src/custom-context.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Session } from "./session.js"
66

77
const Input = Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Session.ID }))
88
const Root = Schema.Struct({
9+
path: Schema.String,
910
directory: Schema.String,
1011
files: Schema.Array(Schema.String),
1112
skills: Schema.Array(Schema.String),

‎packages/tui/src/feature-plugins/custom/sidebar-context.tsx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { CustomAutoLoadSkills } from "@opencode/schema/custom-auto-load-skills"
33
import { CustomContext } from "@opencode/schema/custom-context"
44
import { createMemo, createResource, For, Show } from "solid-js"
55
import { createStore } from "solid-js/store"
6+
import { useClipboard } from "../../context/clipboard"
7+
import { useToast } from "../../ui/toast"
68

79
type Node = { label: string; children?: readonly Node[] }
810

@@ -40,6 +42,8 @@ export function SidebarContext(props: { context: Plugin.Context; sessionID: stri
4042
const session = createMemo(() => props.context.data.session.get(props.sessionID))
4143
const directory = createMemo(() => (session()?.location ?? props.context.location)?.directory)
4244
const roots = createMemo(() => (session()?.roots ?? []).join("\0"))
45+
const clipboard = useClipboard()
46+
const toast = useToast()
4347
const [open, setOpen] = createStore<Record<string, boolean>>({})
4448
// Edited instructions, nested loads, and skill loads land in the transcript as system, synthetic, user, or
4549
// skill messages, or as a completed skill tool call inside an assistant message that already exists.
@@ -86,6 +90,7 @@ export function SidebarContext(props: { context: Plugin.Context; sessionID: stri
8690
])
8791
const rootNodes = createMemo(() =>
8892
(result()?.roots ?? []).map((root) => ({
93+
path: root.path,
8994
label: root.directory,
9095
children: [...group("Instructions", root.files), ...group("Skills", root.skills)],
9196
})),
@@ -101,12 +106,20 @@ export function SidebarContext(props: { context: Plugin.Context; sessionID: stri
101106
<For each={rootNodes()}>
102107
{(root) => (
103108
<>
104-
<box onMouseDown={() => setOpen(root.label, !open[root.label])}>
105-
<text fg={theme.text.base} wrapMode="none" truncate>
109+
<box
110+
onMouseDown={() => {
111+
setOpen(root.path, !open[root.path])
112+
void clipboard.write(root.path).then(
113+
() => toast.show({ message: "Path copied to clipboard", variant: "info" }),
114+
toast.error,
115+
)
116+
}}
117+
>
118+
<text fg={theme.text.base} wrapMode="none" selectable={false} truncate>
106119
{root.label}
107120
</text>
108121
</box>
109-
<Show when={open[root.label]}>
122+
<Show when={open[root.path]}>
110123
<Tree context={props.context} nodes={root.children} prefix="" />
111124
</Show>
112125
</>

0 commit comments

Comments
 (0)