From 1c2defc583436fe78bf2f941a376ef17d9e01da7 Mon Sep 17 00:00:00 2001 From: Alex Yaroshuk Date: Sat, 7 Feb 2026 20:19:35 +0800 Subject: [PATCH 1/6] feat(app): add context menu to file tree with open and mention actions --- packages/app/src/components/file-tree.tsx | 62 ++++++++++++++----- packages/app/src/i18n/ar.ts | 2 + packages/app/src/i18n/br.ts | 2 + packages/app/src/i18n/bs.ts | 2 + packages/app/src/i18n/da.ts | 2 + packages/app/src/i18n/de.ts | 2 + packages/app/src/i18n/en.ts | 2 + packages/app/src/i18n/es.ts | 2 + packages/app/src/i18n/fr.ts | 2 + packages/app/src/i18n/ja.ts | 2 + packages/app/src/i18n/ko.ts | 2 + packages/app/src/i18n/no.ts | 2 + packages/app/src/i18n/pl.ts | 2 + packages/app/src/i18n/ru.ts | 2 + packages/app/src/i18n/th.ts | 2 + packages/app/src/i18n/zh.ts | 2 + packages/app/src/i18n/zht.ts | 2 + packages/app/src/pages/session.tsx | 35 +++++++++++ .../src/pages/session/session-side-panel.tsx | 3 + 19 files changed, 117 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 4a3e276724d..f8c91f17a48 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -1,5 +1,7 @@ import { useFile } from "@/context/file" +import { useLanguage } from "@/context/language" 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" @@ -74,6 +76,7 @@ export default function FileTree(props: { draggable?: boolean tooltip?: boolean onFileClick?: (file: FileNode) => void + onFileMention?: (file: FileNode) => void _filter?: Filter _marks?: Set @@ -81,6 +84,7 @@ export default function FileTree(props: { _kinds?: ReadonlyMap }) { const file = useFile() + const language = useLanguage() const level = props.level ?? 0 const draggable = () => props.draggable ?? true const tooltip = () => props.tooltip ?? true @@ -415,15 +419,26 @@ export default function FileTree(props: { open={expanded()} onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} > - - - -
- -
-
-
-
+ + + + +
+ +
+
+
+
+ + + + props.onFileMention?.(node)}> + {language.t("session.files.mention")} + + + + +
- - props.onFileClick?.(node)}> -
- - - + + + + props.onFileClick?.(node)}> +
+ + + + + + + props.onFileClick?.(node)}> + {language.t("common.open")} + + + props.onFileMention?.(node)}> + {language.t("session.files.mention")} + + + + + ) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 77a3edb062a..9b09704fbf6 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -426,6 +426,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": "تحميل الرسائل السابقة", @@ -483,6 +484,7 @@ export const dict = { "common.archive": "أرشفة", "common.delete": "حذف", "common.close": "إغلاق", + "common.open": "فتح", "common.edit": "تحرير", "common.loadMore": "تحميل المزيد", "common.key.esc": "ESC", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index a743a3d8969..f63a4ac2f2e 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -427,6 +427,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", @@ -487,6 +488,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", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index ce37989c259..62f66a70455 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -455,6 +455,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...", @@ -514,6 +515,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", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 88704607b33..131e75a3b97 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -428,6 +428,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", @@ -487,6 +488,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", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index a4d12d44543..2149f1af5b7 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -470,6 +470,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", @@ -530,6 +531,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", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 4d7d571afb7..ca45a20090c 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -492,6 +492,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...", @@ -558,6 +559,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", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 5d48ba49493..1ea35bf33b1 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -430,6 +430,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", @@ -490,6 +491,7 @@ export const dict = { "common.archive": "Archivar", "common.delete": "Eliminar", "common.close": "Cerrar", + "common.open": "Abrir", "common.edit": "Editar", "common.loadMore": "Cargar más", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index a76e57ff153..ff7216db64f 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -435,6 +435,7 @@ export const dict = { "session.files.selectToOpen": "Sélectionnez un fichier à ouvrir", "session.files.all": "Tous les fichiers", "session.files.binaryContent": "Fichier binaire (le contenu ne peut pas être affiché)", + "session.files.mention": "Mentionner", "session.messages.renderEarlier": "Afficher les messages précédents", "session.messages.loadingEarlier": "Chargement des messages précédents...", "session.messages.loadEarlier": "Charger les messages précédents", @@ -495,6 +496,7 @@ export const dict = { "common.archive": "Archiver", "common.delete": "Supprimer", "common.close": "Fermer", + "common.open": "Ouvrir", "common.edit": "Modifier", "common.loadMore": "Charger plus", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index e41dea9dc73..9e743ad07f6 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -422,6 +422,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": "以前のメッセージを読み込む", @@ -482,6 +483,7 @@ export const dict = { "common.archive": "アーカイブ", "common.delete": "削除", "common.close": "閉じる", + "common.open": "開く", "common.edit": "編集", "common.loadMore": "さらに読み込む", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index a4f42a583e0..12ada9c29a4 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -429,6 +429,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": "이전 메시지 로드", @@ -488,6 +489,7 @@ export const dict = { "common.archive": "보관", "common.delete": "삭제", "common.close": "닫기", + "common.open": "열기", "common.edit": "편집", "common.loadMore": "더 불러오기", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 3de7837f800..e17cc9a203b 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -430,6 +430,7 @@ export const dict = { "session.files.selectToOpen": "Velg en fil å åpne", "session.files.all": "Alle filer", "session.files.binaryContent": "Binær fil (innhold kan ikke vises)", + "session.files.mention": "Nevn", "session.messages.renderEarlier": "Vis tidligere meldinger", "session.messages.loadingEarlier": "Laster inn tidligere meldinger...", "session.messages.loadEarlier": "Last inn tidligere meldinger", @@ -490,6 +491,7 @@ export const dict = { "common.archive": "Arkiver", "common.delete": "Slett", "common.close": "Lukk", + "common.open": "Åpne", "common.edit": "Rediger", "common.loadMore": "Last flere", "common.key.esc": "ESC", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 44bc4677be9..13497aabb07 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -429,6 +429,7 @@ export const dict = { "session.files.selectToOpen": "Wybierz plik do otwarcia", "session.files.all": "Wszystkie pliki", "session.files.binaryContent": "Plik binarny (zawartość nie może być wyświetlona)", + "session.files.mention": "Wspomnij", "session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości", "session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...", "session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości", @@ -489,6 +490,7 @@ export const dict = { "common.archive": "Archiwizuj", "common.delete": "Usuń", "common.close": "Zamknij", + "common.open": "Otwórz", "common.edit": "Edytuj", "common.loadMore": "Załaduj więcej", "common.key.esc": "ESC", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 28785c0e9fb..474abb3ea94 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -431,6 +431,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": "Загрузить предыдущие сообщения", @@ -491,6 +492,7 @@ export const dict = { "common.archive": "Архивировать", "common.delete": "Удалить", "common.close": "Закрыть", + "common.open": "Открыть", "common.edit": "Редактировать", "common.loadMore": "Загрузить ещё", "common.key.esc": "ESC", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 9858f39d772..462024881e0 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -432,6 +432,7 @@ export const dict = { "session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด", "session.files.all": "ไฟล์ทั้งหมด", "session.files.binaryContent": "ไฟล์ไบนารี (ไม่สามารถแสดงเนื้อหาได้)", + "session.files.mention": "กล่าวถึง", "session.messages.renderEarlier": "แสดงข้อความก่อนหน้า", "session.messages.loadingEarlier": "กำลังโหลดข้อความก่อนหน้า...", @@ -490,6 +491,7 @@ export const dict = { "common.archive": "จัดเก็บ", "common.delete": "ลบ", "common.close": "ปิด", + "common.open": "เปิด", "common.edit": "แก้ไข", "common.loadMore": "โหลดเพิ่มเติม", "common.key.esc": "ESC", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index a8fda6f3a60..86bf1993c2a 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -466,6 +466,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": "加载更早的消息", @@ -523,6 +524,7 @@ export const dict = { "common.archive": "归档", "common.delete": "删除", "common.close": "关闭", + "common.open": "打开", "common.edit": "编辑", "common.loadMore": "加载更多", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 319f5c51d15..5a8ce0fb036 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -463,6 +463,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": "載入更早的訊息", @@ -520,6 +521,7 @@ export const dict = { "common.archive": "封存", "common.delete": "刪除", "common.close": "關閉", + "common.open": "開啟", "common.edit": "編輯", "common.loadMore": "載入更多", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index a70d4e8a275..4c98a8a0dd8 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -766,6 +766,40 @@ export default function Page() { prompt.context.add({ type: "file", path, selection, preview }) } + const mentionFile = (path: string) => { + const current = prompt.current() + const isDirty = prompt.dirty() + + const parts: typeof current = [] + const images: typeof current = [] + let pos = 0 + + for (const part of current) { + if (part.type === "image") { + images.push(part) + continue + } + if (isDirty) { + parts.push(part) + pos = part.end + } + } + + if (isDirty) { + parts.push({ type: "text", content: " ", start: pos, end: pos + 1 }) + pos += 1 + } + + const content = "@" + path + parts.push({ type: "file", path, content, start: pos, end: pos + content.length }) + pos += content.length + + parts.push({ type: "text", content: " ", start: pos, end: pos + 1 }) + pos += 1 + + prompt.set([...parts, ...images], pos) + } + const addCommentToContext = (input: { file: string selection: SelectedLineRange @@ -1712,6 +1746,7 @@ export default function Page() { kinds={kinds()} activeDiff={tree.activeDiff} focusReviewDiff={focusReviewDiff} + onFileMention={mentionFile} />
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 573680dec6d..eac5dbc4af4 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -66,6 +66,7 @@ export function SessionSidePanel(props: { kinds: Map activeDiff?: string focusReviewDiff: (path: string) => void + onFileMention?: (path: string) => void }) { return ( @@ -268,6 +269,7 @@ export function SessionSidePanel(props: { draggable={false} active={props.activeDiff} onFileClick={(node) => props.focusReviewDiff(node.path)} + onFileMention={props.onFileMention ? (node) => props.onFileMention!(node.path) : undefined} /> @@ -284,6 +286,7 @@ export function SessionSidePanel(props: { modified={props.diffFiles} kinds={props.kinds} onFileClick={(node) => props.openTab(props.file.tab(node.path))} + onFileMention={props.onFileMention ? (node) => props.onFileMention!(node.path) : undefined} /> From 09a37a773313a88ff57e3f34057da5d11e6972e9 Mon Sep 17 00:00:00 2001 From: Alex Yaroshuk Date: Sun, 8 Feb 2026 01:45:50 +0800 Subject: [PATCH 2/6] add context menu to file tabs, close others option, add localization for 'close others' --- .../session/session-sortable-tab.tsx | 75 +++++++++++++------ packages/app/src/i18n/ar.ts | 1 + packages/app/src/i18n/br.ts | 1 + packages/app/src/i18n/bs.ts | 1 + packages/app/src/i18n/da.ts | 1 + packages/app/src/i18n/de.ts | 1 + packages/app/src/i18n/en.ts | 1 + packages/app/src/i18n/es.ts | 1 + packages/app/src/i18n/fr.ts | 1 + packages/app/src/i18n/ja.ts | 1 + packages/app/src/i18n/ko.ts | 1 + packages/app/src/i18n/no.ts | 1 + packages/app/src/i18n/pl.ts | 1 + packages/app/src/i18n/ru.ts | 1 + packages/app/src/i18n/th.ts | 1 + packages/app/src/i18n/zh.ts | 1 + packages/app/src/i18n/zht.ts | 1 + packages/app/src/pages/session.tsx | 14 ++++ .../src/pages/session/session-side-panel.tsx | 4 +- 19 files changed, 85 insertions(+), 24 deletions(-) diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx index 516f3c8edeb..06996d73c4b 100644 --- a/packages/app/src/components/session/session-sortable-tab.tsx +++ b/packages/app/src/components/session/session-sortable-tab.tsx @@ -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" @@ -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() @@ -35,28 +42,50 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v // @ts-ignore
- - props.onTabClose(props.tab)} - aria-label={language.t("common.closeTab")} - /> - - } - hideCloseButton - onMiddleClick={() => props.onTabClose(props.tab)} - > - {(p) => } - + + + props.onTabClose(props.tab)} + aria-label={language.t("common.closeTab")} + /> + + } + hideCloseButton + onMiddleClick={() => props.onTabClose(props.tab)} + onClick={props.onClick} + > + {(p) => } + + + + props.onTabClose(props.tab)}> + {language.t("common.closeTab")} + + + props.onCloseOthers?.(props.tab)}> + {language.t("session.tab.closeOthers")} + + + + + props.onMention?.(props.tab)}> + {language.t("session.files.mention")} + + + + +
) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 9b09704fbf6..34f6654115c 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -416,6 +416,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": "تغيير", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index f63a4ac2f2e..8108f528c8c 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -417,6 +417,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", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 62f66a70455..945653049e5 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -444,6 +444,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", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 131e75a3b97..dd226aef330 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -418,6 +418,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", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 2149f1af5b7..5425435a3ec 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -460,6 +460,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", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index ca45a20090c..61d3be4fee4 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -481,6 +481,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", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 1ea35bf33b1..c153af36cc7 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -420,6 +420,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", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index ff7216db64f..090dd08cee8 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -425,6 +425,7 @@ export const dict = { "session.tab.session": "Session", "session.tab.review": "Revue", "session.tab.context": "Contexte", + "session.tab.closeOthers": "Fermer les autres", "session.panel.reviewAndFiles": "Revue et fichiers", "session.review.filesChanged": "{{count}} fichiers modifiés", "session.review.change.one": "Modification", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 9e743ad07f6..0a6371382e8 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -412,6 +412,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": "変更", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 12ada9c29a4..ce9245cd6b7 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -419,6 +419,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": "변경", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index e17cc9a203b..04e3dcc5d04 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -420,6 +420,7 @@ export const dict = { "session.tab.session": "Sesjon", "session.tab.review": "Gjennomgang", "session.tab.context": "Kontekst", + "session.tab.closeOthers": "Lukk andre", "session.panel.reviewAndFiles": "Gjennomgang og filer", "session.review.filesChanged": "{{count}} filer endret", "session.review.change.one": "Endring", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 13497aabb07..f7973b8b18e 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -419,6 +419,7 @@ export const dict = { "session.tab.session": "Sesja", "session.tab.review": "Przegląd", "session.tab.context": "Kontekst", + "session.tab.closeOthers": "Zamknij pozostałe", "session.panel.reviewAndFiles": "Przegląd i pliki", "session.review.filesChanged": "Zmieniono {{count}} plików", "session.review.change.one": "Zmiana", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 474abb3ea94..bbb4e71a3c0 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -421,6 +421,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": "Изменение", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 462024881e0..0dceacec5a2 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -421,6 +421,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": "การเปลี่ยนแปลง", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 86bf1993c2a..581570d2575 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -456,6 +456,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": "更改", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 5a8ce0fb036..ada2732f3e8 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -453,6 +453,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": "變更", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 4c98a8a0dd8..e48b094a160 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -910,6 +910,18 @@ export default function Page() { .filter((tab) => tab !== "context" && tab !== "review"), ) + const closeOtherTabs = (currentTab: string) => { + const others = openedTabs().filter((tab) => tab !== currentTab) + for (const tab of others) { + tabs().close(tab) + } + } + + const mentionTab = (tab: string) => { + const path = file.pathFromTab(tab) + if (path) mentionFile(path) + } + const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") const reviewTab = createMemo(() => isDesktop() && !layout.fileTree.opened()) @@ -1747,6 +1759,8 @@ export default function Page() { activeDiff={tree.activeDiff} focusReviewDiff={focusReviewDiff} onFileMention={mentionFile} + onCloseOthers={closeOtherTabs} + onMention={mentionTab} />
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index eac5dbc4af4..858a5e84825 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -67,6 +67,8 @@ export function SessionSidePanel(props: { activeDiff?: string focusReviewDiff: (path: string) => void onFileMention?: (path: string) => void + onCloseOthers?: (tab: string) => void + onMention?: (tab: string) => void }) { return ( @@ -132,7 +134,7 @@ export function SessionSidePanel(props: { - {(tab) => } + {(tab) => props.openTab(tab)} onCloseOthers={props.onCloseOthers} onMention={props.onMention} />} From 7c5779cd4bd7246c13a2981a36f74a104c25f4fa Mon Sep 17 00:00:00 2001 From: Alex Yaroshuk Date: Tue, 10 Feb 2026 05:17:42 +0800 Subject: [PATCH 3/6] ssr-safe context menu in file tree to fix tests --- packages/app/src/components/file-tree.tsx | 94 ++++++++++++++--------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index f8c91f17a48..cbaabee6257 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -1,13 +1,13 @@ import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" 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" import { createEffect, createMemo, + createResource, For, Match, on, @@ -18,7 +18,7 @@ import { type ComponentProps, type ParentProps, } from "solid-js" -import { Dynamic } from "solid-js/web" +import { Dynamic, isServer } from "solid-js/web" import type { FileNode } from "@opencode-ai/sdk/v2" function pathToFileUrl(filepath: string): string { @@ -29,6 +29,43 @@ function pathToFileUrl(filepath: string): string { return `file://${encodedPath}` } +function FileContextMenu( + props: ParentProps<{ onMention?: () => void; onOpen?: () => void; mentionLabel?: string; openLabel?: string }>, +) { + if (isServer) return props.children + + const [mod] = createResource(async () => { + const m = await import("@opencode-ai/ui/context-menu") + return m.ContextMenu + }) + + return ( + + {(ContextMenu) => ( + + + {props.children} + + + + + + {props.openLabel} + + + + + {props.mentionLabel} + + + + + + )} + + ) +} + type Kind = "add" | "del" | "mix" type Filter = { @@ -419,8 +456,11 @@ export default function FileTree(props: { open={expanded()} onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} > - - + props.onFileMention!(node) : undefined} + mentionLabel={language.t("session.files.mention")} + > +
@@ -429,16 +469,7 @@ export default function FileTree(props: { - - - - props.onFileMention?.(node)}> - {language.t("session.files.mention")} - - - - - +
- - - - props.onFileClick?.(node)}> -
- - - - - - - props.onFileClick?.(node)}> - {language.t("common.open")} - - - props.onFileMention?.(node)}> - {language.t("session.files.mention")} - - - - - + props.onFileClick!(node) : undefined} + onMention={props.onFileMention ? () => props.onFileMention!(node) : undefined} + openLabel={language.t("common.open")} + mentionLabel={language.t("session.files.mention")} + > + + props.onFileClick?.(node)}> +
+ + + + ) From af2bdfe7887a95cc19963510a6d00d9eac7cbf9e Mon Sep 17 00:00:00 2001 From: Alex Yaroshuk Date: Wed, 11 Feb 2026 07:14:21 +0800 Subject: [PATCH 4/6] fix: file-tree spacing and flashing bugs --- packages/app/src/components/file-tree.tsx | 114 +++++++++------------- 1 file changed, 48 insertions(+), 66 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 63a862be4b4..6370a25e76d 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -2,13 +2,13 @@ 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" import { createEffect, createMemo, - createResource, For, Match, on, @@ -19,50 +19,13 @@ import { type ComponentProps, type ParentProps, } from "solid-js" -import { Dynamic, isServer } from "solid-js/web" +import { Dynamic } from "solid-js/web" import type { FileNode } from "@opencode-ai/sdk/v2" function pathToFileUrl(filepath: string): string { return `file://${encodeFilePath(filepath)}` } -function FileContextMenu( - props: ParentProps<{ onMention?: () => void; onOpen?: () => void; mentionLabel?: string; openLabel?: string }>, -) { - if (isServer) return props.children - - const [mod] = createResource(async () => { - const m = await import("@opencode-ai/ui/context-menu") - return m.ContextMenu - }) - - return ( - - {(ContextMenu) => ( - - - {props.children} - - - - - - {props.openLabel} - - - - - {props.mentionLabel} - - - - - - )} - - ) -} - type Kind = "add" | "del" | "mix" type Filter = { @@ -455,20 +418,30 @@ export default function FileTree(props: { open={expanded()} onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))} > - props.onFileMention!(node) : undefined} - mentionLabel={language.t("session.files.mention")} - > - - - -
- -
-
-
-
-
+ + + + + +
+ +
+
+
+
+ + + + props.onFileMention!(node) : undefined} + > + {language.t("session.files.mention")} + + + + +
+
- props.onFileClick!(node) : undefined} - onMention={props.onFileMention ? () => props.onFileMention!(node) : undefined} - openLabel={language.t("common.open")} - mentionLabel={language.t("session.files.mention")} - > - - props.onFileClick?.(node)}> -
- - - - + + + + props.onFileClick?.(node)}> +
+ + + + + + + props.onFileClick!(node) : undefined}> + {language.t("common.open")} + + + props.onFileMention!(node) : undefined}> + {language.t("session.files.mention")} + + + + + ) From b6040dd67ba91ec21fa390650a880bb8cc7ab8ec Mon Sep 17 00:00:00 2001 From: Alex Yaroshuk Date: Wed, 11 Feb 2026 07:18:14 +0800 Subject: [PATCH 5/6] fix: mock context menu for server-side tests --- packages/app/src/components/file-tree.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/components/file-tree.test.ts b/packages/app/src/components/file-tree.test.ts index 29e20b4807c..5da3532accd 100644 --- a/packages/app/src/components/file-tree.test.ts +++ b/packages/app/src/components/file-tree.test.ts @@ -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 From 0edf159e24ca2e89161f8701e91ec80dafaf5e69 Mon Sep 17 00:00:00 2001 From: Alex Yaroshuk Date: Wed, 11 Feb 2026 07:22:21 +0800 Subject: [PATCH 6/6] fix: file-tree full-width hover area --- packages/app/src/components/file-tree.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/file-tree.tsx b/packages/app/src/components/file-tree.tsx index 6370a25e76d..876ec597e6d 100644 --- a/packages/app/src/components/file-tree.tsx +++ b/packages/app/src/components/file-tree.tsx @@ -420,7 +420,7 @@ export default function FileTree(props: { > - +
@@ -472,7 +472,7 @@ export default function FileTree(props: { - + props.onFileClick?.(node)}>