Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1c2defc
feat(app): add context menu to file tree with open and mention actions
alexyaroshuk Feb 7, 2026
09a37a7
add context menu to file tabs, close others option, add localization …
alexyaroshuk Feb 7, 2026
afe7aa6
Merge remote-tracking branch 'upstream/dev' into feat/file-tree-conte…
alexyaroshuk Feb 7, 2026
0bf2fe7
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 7, 2026
7b5d780
Merge branch 'dev-clean' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 7, 2026
35315ec
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 7, 2026
a286c7f
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 8, 2026
0b8d574
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 9, 2026
8957801
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 9, 2026
9d38c38
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 9, 2026
031c6b3
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 9, 2026
dadbfe0
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 9, 2026
48997db
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 9, 2026
7c5779c
ssr-safe context menu in file tree to fix tests
alexyaroshuk Feb 9, 2026
d06a84a
Merge branch 'feat/file-tree-context-menu-clean' of https://github.co…
alexyaroshuk Feb 9, 2026
b15c2c3
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 9, 2026
b8310ea
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 10, 2026
1a6ae13
Merge remote-tracking branch 'upstream/dev' into feat/file-tree-conte…
alexyaroshuk Feb 10, 2026
d243b10
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 10, 2026
af2bdfe
fix: file-tree spacing and flashing bugs
alexyaroshuk Feb 10, 2026
b6040dd
fix: mock context menu for server-side tests
alexyaroshuk Feb 10, 2026
0edf159
fix: file-tree full-width hover area
alexyaroshuk Feb 10, 2026
0a15089
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 10, 2026
716c763
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 11, 2026
03dcc89
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 11, 2026
c92a33c
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 11, 2026
89b98b0
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 11, 2026
5641291
Merge branch 'dev' into feat/file-tree-context-menu-clean
alexyaroshuk Feb 11, 2026
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
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
### What does this PR do?

Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
Please provide a description of the issue (if there is one), the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the pr.

**If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!**

Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/components/file-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ beforeAll(async () => {
mock.module("@opencode-ai/ui/file-icon", () => ({ FileIcon: () => null }))
mock.module("@opencode-ai/ui/icon", () => ({ Icon: () => null }))
mock.module("@opencode-ai/ui/tooltip", () => ({ Tooltip: (props: { children?: unknown }) => props.children }))
mock.module("@opencode-ai/ui/context-menu", () => ({
ContextMenu: {
Trigger: (props: { as?: string; children?: unknown }) => props.children,
Portal: (props: { children?: unknown }) => props.children,
Content: (props: { children?: unknown }) => props.children,
Item: (props: { onSelect?: () => void; children?: unknown }) => props.children,
ItemLabel: (props: { children?: unknown }) => props.children,
},
}))
const mod = await import("./file-tree")
shouldListRoot = mod.shouldListRoot
shouldListExpanded = mod.shouldListExpanded
Expand Down
62 changes: 49 additions & 13 deletions packages/app/src/components/file-tree.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
import { encodeFilePath } from "@/context/file/path"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { ContextMenu } from "@opencode-ai/ui/context-menu"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
Expand Down Expand Up @@ -71,13 +73,15 @@ export default function FileTree(props: {
draggable?: boolean
tooltip?: boolean
onFileClick?: (file: FileNode) => void
onFileMention?: (file: FileNode) => void

_filter?: Filter
_marks?: Set<string>
_deeps?: Map<string, number>
_kinds?: ReadonlyMap<string, Kind>
}) {
const file = useFile()
const language = useLanguage()
const level = props.level ?? 0
const draggable = () => props.draggable ?? true
const tooltip = () => props.tooltip ?? true
Expand Down Expand Up @@ -415,13 +419,28 @@ export default function FileTree(props: {
onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
>
<Collapsible.Trigger>
<Wrapper>
<Node node={node}>
<div class="size-4 flex items-center justify-center text-icon-weak">
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
</div>
</Node>
</Wrapper>
<ContextMenu>
<ContextMenu.Trigger as="div" class="w-full">
<Wrapper>
<Node node={node}>
<div class="size-4 flex items-center justify-center text-icon-weak">
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
</div>
</Node>
</Wrapper>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<Show when={props.onFileMention}>
<ContextMenu.Item
onSelect={props.onFileMention ? () => props.onFileMention!(node) : undefined}
>
<ContextMenu.ItemLabel>{language.t("session.files.mention")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</Show>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
</Collapsible.Trigger>
<Collapsible.Content class="relative pt-0.5">
<div
Expand All @@ -442,6 +461,7 @@ export default function FileTree(props: {
draggable={props.draggable}
tooltip={props.tooltip}
onFileClick={props.onFileClick}
onFileMention={props.onFileMention}
_filter={filter()}
_marks={marks()}
_deeps={deeps()}
Expand All @@ -451,12 +471,28 @@ export default function FileTree(props: {
</Collapsible>
</Match>
<Match when={node.type === "file"}>
<Wrapper>
<Node node={node} as="button" type="button" onClick={() => props.onFileClick?.(node)}>
<div class="w-4 shrink-0" />
<FileIcon node={node} class="text-icon-weak size-4" />
</Node>
</Wrapper>
<ContextMenu>
<ContextMenu.Trigger as="div" class="w-full">
<Wrapper>
<Node node={node} as="button" type="button" onClick={() => props.onFileClick?.(node)}>
<div class="w-4 shrink-0" />
<FileIcon node={node} class="text-icon-weak size-4" />
</Node>
</Wrapper>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item onSelect={props.onFileClick ? () => props.onFileClick!(node) : undefined}>
<ContextMenu.ItemLabel>{language.t("common.open")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<Show when={props.onFileMention}>
<ContextMenu.Item onSelect={props.onFileMention ? () => props.onFileMention!(node) : undefined}>
<ContextMenu.ItemLabel>{language.t("session.files.mention")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</Show>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
</Match>
</Switch>
)
Expand Down
75 changes: 52 additions & 23 deletions packages/app/src/components/session/session-sortable-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Tabs } from "@opencode-ai/ui/tabs"
import { ContextMenu } from "@opencode-ai/ui/context-menu"
import { getFilename } from "@opencode-ai/util/path"
import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
Expand All @@ -25,7 +26,13 @@ export function FileVisual(props: { path: string; active?: boolean }): JSX.Eleme
)
}

export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
export function SortableTab(props: {
tab: string
onTabClose: (tab: string) => void
onClick?: () => void
onCloseOthers?: (tab: string) => void
onMention?: (tab: string) => void
}): JSX.Element {
const file = useFile()
const language = useLanguage()
const command = useCommand()
Expand All @@ -35,28 +42,50 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
<div class="relative h-full">
<Tabs.Trigger
value={props.tab}
closeButton={
<TooltipKeybind
title={language.t("common.closeTab")}
keybind={command.keybind("tab.close")}
placement="bottom"
>
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => props.onTabClose(props.tab)}
aria-label={language.t("common.closeTab")}
/>
</TooltipKeybind>
}
hideCloseButton
onMiddleClick={() => props.onTabClose(props.tab)}
>
<Show when={path()}>{(p) => <FileVisual path={p()} />}</Show>
</Tabs.Trigger>
<ContextMenu>
<ContextMenu.Trigger
as={Tabs.Trigger}
value={props.tab}
closeButton={
<TooltipKeybind
title={language.t("common.closeTab")}
keybind={command.keybind("tab.close")}
placement="bottom"
>
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => props.onTabClose(props.tab)}
aria-label={language.t("common.closeTab")}
/>
</TooltipKeybind>
}
hideCloseButton
onMiddleClick={() => props.onTabClose(props.tab)}
onClick={props.onClick}
>
<Show when={path()}>{(p) => <FileVisual path={p()} />}</Show>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item onSelect={() => props.onTabClose(props.tab)}>
<ContextMenu.ItemLabel>{language.t("common.closeTab")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<Show when={props.onCloseOthers}>
<ContextMenu.Item onSelect={() => props.onCloseOthers?.(props.tab)}>
<ContextMenu.ItemLabel>{language.t("session.tab.closeOthers")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</Show>
<Show when={props.onMention}>
<ContextMenu.Separator />
<ContextMenu.Item onSelect={() => props.onMention?.(props.tab)}>
<ContextMenu.ItemLabel>{language.t("session.files.mention")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</Show>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
</div>
</div>
)
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export const dict = {
"session.tab.session": "جلسة",
"session.tab.review": "مراجعة",
"session.tab.context": "سياق",
"session.tab.closeOthers": "إغلاق البقية",
"session.panel.reviewAndFiles": "المراجعة والملفات",
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
"session.review.change.one": "تغيير",
Expand All @@ -441,6 +442,7 @@ export const dict = {
"session.files.selectToOpen": "اختر ملفًا لفتحه",
"session.files.all": "كل الملفات",
"session.files.binaryContent": "ملف ثنائي (لا يمكن عرض المحتوى)",
"session.files.mention": "إشارة",
"session.messages.renderEarlier": "عرض الرسائل السابقة",
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
"session.messages.loadEarlier": "تحميل الرسائل السابقة",
Expand Down Expand Up @@ -495,6 +497,7 @@ export const dict = {
"common.archive": "أرشفة",
"common.delete": "حذف",
"common.close": "إغلاق",
"common.open": "فتح",
"common.edit": "تحرير",
"common.loadMore": "تحميل المزيد",
"common.key.esc": "ESC",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ export const dict = {
"session.tab.session": "Sessão",
"session.tab.review": "Revisão",
"session.tab.context": "Contexto",
"session.tab.closeOthers": "Fechar outras",
"session.panel.reviewAndFiles": "Revisão e arquivos",
"session.review.filesChanged": "{{count}} Arquivos Alterados",
"session.review.change.one": "Alteração",
Expand All @@ -444,6 +445,7 @@ export const dict = {
"session.files.selectToOpen": "Selecione um arquivo para abrir",
"session.files.all": "Todos os arquivos",
"session.files.binaryContent": "Arquivo binário (conteúdo não pode ser exibido)",
"session.files.mention": "Mencionar",
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
"session.messages.loadEarlier": "Carregar mensagens anteriores",
Expand Down Expand Up @@ -501,6 +503,7 @@ export const dict = {
"common.archive": "Arquivar",
"common.delete": "Excluir",
"common.close": "Fechar",
"common.open": "Abrir",
"common.edit": "Editar",
"common.loadMore": "Carregar mais",
"common.key.esc": "ESC",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/bs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ export const dict = {
"session.tab.session": "Sesija",
"session.tab.review": "Pregled",
"session.tab.context": "Kontekst",
"session.tab.closeOthers": "Zatvori ostale",
"session.panel.reviewAndFiles": "Pregled i datoteke",
"session.review.filesChanged": "Izmijenjeno {{count}} datoteka",
"session.review.change.one": "Izmjena",
Expand All @@ -497,6 +498,7 @@ export const dict = {
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
"session.files.all": "Sve datoteke",
"session.files.binaryContent": "Binarna datoteka (sadržaj se ne može prikazati)",
"session.files.mention": "Spomeni",

"session.messages.renderEarlier": "Prikaži ranije poruke",
"session.messages.loadingEarlier": "Učitavanje ranijih poruka...",
Expand Down Expand Up @@ -561,6 +563,7 @@ export const dict = {
"common.archive": "Arhiviraj",
"common.delete": "Izbriši",
"common.close": "Zatvori",
"common.open": "Otvori",
"common.edit": "Uredi",
"common.loadMore": "Učitaj još",
"common.key.esc": "ESC",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ export const dict = {
"session.tab.session": "Session",
"session.tab.review": "Gennemgang",
"session.tab.context": "Kontekst",
"session.tab.closeOthers": "Luk andre",
"session.panel.reviewAndFiles": "Gennemgang og filer",
"session.review.filesChanged": "{{count}} Filer ændret",
"session.review.change.one": "Ændring",
Expand All @@ -493,6 +494,7 @@ export const dict = {
"session.files.selectToOpen": "Vælg en fil at åbne",
"session.files.all": "Alle filer",
"session.files.binaryContent": "Binær fil (indhold kan ikke vises)",
"session.files.mention": "Nævn",
"session.messages.renderEarlier": "Vis tidligere beskeder",
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
"session.messages.loadEarlier": "Indlæs tidligere beskeder",
Expand Down Expand Up @@ -557,6 +559,7 @@ export const dict = {
"common.archive": "Arkivér",
"common.delete": "Slet",
"common.close": "Luk",
"common.open": "Åbn",
"common.edit": "Rediger",
"common.loadMore": "Indlæs flere",

Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ export const dict = {
"session.tab.session": "Sitzung",
"session.tab.review": "Überprüfung",
"session.tab.context": "Kontext",
"session.tab.closeOthers": "Andere schließen",
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
"session.review.filesChanged": "{{count}} Dateien geändert",
"session.review.change.one": "Änderung",
Expand All @@ -452,6 +453,7 @@ export const dict = {
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
"session.files.all": "Alle Dateien",
"session.files.binaryContent": "Binärdatei (Inhalt kann nicht angezeigt werden)",
"session.files.mention": "Erwähnen",
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
"session.messages.loadEarlier": "Frühere Nachrichten laden",
Expand Down Expand Up @@ -509,6 +511,7 @@ export const dict = {
"common.archive": "Archivieren",
"common.delete": "Löschen",
"common.close": "Schließen",
"common.open": "Öffnen",
"common.edit": "Bearbeiten",
"common.loadMore": "Mehr laden",
"common.key.esc": "ESC",
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ export const dict = {
"session.tab.session": "Session",
"session.tab.review": "Review",
"session.tab.context": "Context",
"session.tab.closeOthers": "Close others",
"session.panel.reviewAndFiles": "Review and files",
"session.review.filesChanged": "{{count}} Files Changed",
"session.review.change.one": "Change",
Expand All @@ -495,6 +496,7 @@ export const dict = {
"session.files.selectToOpen": "Select a file to open",
"session.files.all": "All files",
"session.files.binaryContent": "Binary file (content cannot be displayed)",
"session.files.mention": "Mention",

"session.messages.renderEarlier": "Render earlier messages",
"session.messages.loadingEarlier": "Loading earlier messages...",
Expand Down Expand Up @@ -561,6 +563,7 @@ export const dict = {
"common.archive": "Archive",
"common.delete": "Delete",
"common.close": "Close",
"common.open": "Open",
"common.edit": "Edit",
"common.loadMore": "Load more",
"common.key.esc": "ESC",
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ export const dict = {
"session.tab.session": "Sesión",
"session.tab.review": "Revisión",
"session.tab.context": "Contexto",
"session.tab.closeOthers": "Cerrar otras",
"session.panel.reviewAndFiles": "Revisión y archivos",
"session.review.filesChanged": "{{count}} Archivos Cambiados",
"session.review.change.one": "Cambio",
Expand All @@ -498,7 +499,7 @@ export const dict = {
"session.files.selectToOpen": "Selecciona un archivo para abrir",
"session.files.all": "Todos los archivos",
"session.files.binaryContent": "Archivo binario (el contenido no puede ser mostrado)",

"session.files.mention": "Mencionar",
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
"session.messages.loadingEarlier": "Cargando mensajes anteriores...",
"session.messages.loadEarlier": "Cargar mensajes anteriores",
Expand Down Expand Up @@ -564,6 +565,7 @@ export const dict = {
"common.archive": "Archivar",
"common.delete": "Eliminar",
"common.close": "Cerrar",
"common.open": "Abrir",
"common.edit": "Editar",
"common.loadMore": "Cargar más",
"common.key.esc": "ESC",
Expand Down
Loading
Loading