summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-14 19:33:22 -0600
committerGitHub <[email protected]>2026-02-14 19:33:22 -0600
commit85b5f5b705e8f7852184a4ef147bdc826639d224 (patch)
tree0b0ca4bf1e8115363e854774e01be44b0b5ea937 /packages
parent460a87f359cef2cdcd4638ba49b1d7d652ddedd5 (diff)
downloadopencode-85b5f5b705e8f7852184a4ef147bdc826639d224.tar.gz
opencode-85b5f5b705e8f7852184a4ef147bdc826639d224.zip
feat(app): clear notifications action (#13668)
Co-authored-by: adamelmore <[email protected]>
Diffstat (limited to 'packages')
-rw-r--r--packages/app/e2e/selectors.ts3
-rw-r--r--packages/app/src/i18n/ar.ts1
-rw-r--r--packages/app/src/i18n/br.ts1
-rw-r--r--packages/app/src/i18n/bs.ts1
-rw-r--r--packages/app/src/i18n/da.ts1
-rw-r--r--packages/app/src/i18n/de.ts1
-rw-r--r--packages/app/src/i18n/en.ts1
-rw-r--r--packages/app/src/i18n/es.ts1
-rw-r--r--packages/app/src/i18n/fr.ts1
-rw-r--r--packages/app/src/i18n/ja.ts1
-rw-r--r--packages/app/src/i18n/ko.ts1
-rw-r--r--packages/app/src/i18n/no.ts1
-rw-r--r--packages/app/src/i18n/pl.ts1
-rw-r--r--packages/app/src/i18n/ru.ts1
-rw-r--r--packages/app/src/i18n/th.ts1
-rw-r--r--packages/app/src/i18n/zh.ts1
-rw-r--r--packages/app/src/i18n/zht.ts1
-rw-r--r--packages/app/src/pages/layout.tsx17
-rw-r--r--packages/app/src/pages/layout/sidebar-project.tsx155
19 files changed, 126 insertions, 65 deletions
diff --git a/packages/app/e2e/selectors.ts b/packages/app/e2e/selectors.ts
index 52c9007ea..1a0afbab1 100644
--- a/packages/app/e2e/selectors.ts
+++ b/packages/app/e2e/selectors.ts
@@ -30,6 +30,9 @@ export const projectMenuTriggerSelector = (slug: string) =>
export const projectCloseMenuSelector = (slug: string) => `[data-action="project-close-menu"][data-project="${slug}"]`
+export const projectClearNotificationsSelector = (slug: string) =>
+ `[data-action="project-clear-notifications"][data-project="${slug}"]`
+
export const projectWorkspacesToggleSelector = (slug: string) =>
`[data-action="project-workspaces-toggle"][data-project="${slug}"]`
diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts
index e3792a3c3..81cc92bf6 100644
--- a/packages/app/src/i18n/ar.ts
+++ b/packages/app/src/i18n/ar.ts
@@ -509,6 +509,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "قم بتوصيل أي موفر لاستخدام النماذج، بما في ذلك Claude و GPT و Gemini وما إلى ذلك.",
"sidebar.project.recentSessions": "الجلسات الحديثة",
"sidebar.project.viewAllSessions": "عرض جميع الجلسات",
+ "sidebar.project.clearNotifications": "مسح الإشعارات",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "سطح المكتب",
"settings.section.server": "الخادم",
diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts
index 07d6ce467..9ed3a9fc6 100644
--- a/packages/app/src/i18n/br.ts
+++ b/packages/app/src/i18n/br.ts
@@ -515,6 +515,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "Conecte qualquer provedor para usar modelos, incluindo Claude, GPT, Gemini etc.",
"sidebar.project.recentSessions": "Sessões recentes",
"sidebar.project.viewAllSessions": "Ver todas as sessões",
+ "sidebar.project.clearNotifications": "Limpar notificações",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Desktop",
"settings.section.server": "Servidor",
diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts
index 7d10da6ed..206aae372 100644
--- a/packages/app/src/i18n/bs.ts
+++ b/packages/app/src/i18n/bs.ts
@@ -576,6 +576,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "Poveži bilo kojeg provajdera da koristiš modele, npr. Claude, GPT, Gemini itd.",
"sidebar.project.recentSessions": "Nedavne sesije",
"sidebar.project.viewAllSessions": "Prikaži sve sesije",
+ "sidebar.project.clearNotifications": "Očisti obavijesti",
"app.name.desktop": "OpenCode Desktop",
diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts
index ac5c4d494..6bf67168f 100644
--- a/packages/app/src/i18n/da.ts
+++ b/packages/app/src/i18n/da.ts
@@ -572,6 +572,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "Forbind enhver udbyder for at bruge modeller, inkl. Claude, GPT, Gemini osv.",
"sidebar.project.recentSessions": "Seneste sessioner",
"sidebar.project.viewAllSessions": "Vis alle sessioner",
+ "sidebar.project.clearNotifications": "Ryd notifikationer",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Desktop",
diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts
index 99a950631..4b6b43a57 100644
--- a/packages/app/src/i18n/de.ts
+++ b/packages/app/src/i18n/de.ts
@@ -524,6 +524,7 @@ export const dict = {
"Verbinden Sie einen beliebigen Anbieter, um Modelle wie Claude, GPT, Gemini usw. zu nutzen.",
"sidebar.project.recentSessions": "Letzte Sitzungen",
"sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
+ "sidebar.project.clearNotifications": "Benachrichtigungen löschen",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Desktop",
"settings.section.server": "Server",
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 99513edaa..fd70f389e 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -577,6 +577,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.",
"sidebar.project.recentSessions": "Recent sessions",
"sidebar.project.viewAllSessions": "View all sessions",
+ "sidebar.project.clearNotifications": "Clear notifications",
"app.name.desktop": "OpenCode Desktop",
diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts
index 7a6c4974e..135a63fef 100644
--- a/packages/app/src/i18n/es.ts
+++ b/packages/app/src/i18n/es.ts
@@ -579,6 +579,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "Conecta cualquier proveedor para usar modelos, inc. Claude, GPT, Gemini etc.",
"sidebar.project.recentSessions": "Sesiones recientes",
"sidebar.project.viewAllSessions": "Ver todas las sesiones",
+ "sidebar.project.clearNotifications": "Borrar notificaciones",
"app.name.desktop": "OpenCode Desktop",
diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts
index fc3bf2667..1ab0c72d5 100644
--- a/packages/app/src/i18n/fr.ts
+++ b/packages/app/src/i18n/fr.ts
@@ -523,6 +523,7 @@ export const dict = {
"Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.",
"sidebar.project.recentSessions": "Sessions récentes",
"sidebar.project.viewAllSessions": "Voir toutes les sessions",
+ "sidebar.project.clearNotifications": "Effacer les notifications",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Bureau",
"settings.section.server": "Serveur",
diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts
index b597db02a..6f092a60f 100644
--- a/packages/app/src/i18n/ja.ts
+++ b/packages/app/src/i18n/ja.ts
@@ -513,6 +513,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "プロバイダーを接続して、Claude、GPT、Geminiなどのモデルを使用できます。",
"sidebar.project.recentSessions": "最近のセッション",
"sidebar.project.viewAllSessions": "すべてのセッションを表示",
+ "sidebar.project.clearNotifications": "通知をクリア",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "デスクトップ",
"settings.section.server": "サーバー",
diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts
index 525bd0356..4d814d43d 100644
--- a/packages/app/src/i18n/ko.ts
+++ b/packages/app/src/i18n/ko.ts
@@ -514,6 +514,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "Claude, GPT, Gemini 등을 포함한 모델을 사용하려면 공급자를 연결하세요.",
"sidebar.project.recentSessions": "최근 세션",
"sidebar.project.viewAllSessions": "모든 세션 보기",
+ "sidebar.project.clearNotifications": "알림 지우기",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "데스크톱",
"settings.section.server": "서버",
diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts
index 98e79e189..63bc66acf 100644
--- a/packages/app/src/i18n/no.ts
+++ b/packages/app/src/i18n/no.ts
@@ -579,6 +579,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "Koble til en leverandør for å bruke modeller, inkl. Claude, GPT, Gemini osv.",
"sidebar.project.recentSessions": "Nylige sesjoner",
"sidebar.project.viewAllSessions": "Vis alle sesjoner",
+ "sidebar.project.clearNotifications": "Fjern varsler",
"app.name.desktop": "OpenCode Desktop",
diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts
index 983c9c14a..2a3ea7bfb 100644
--- a/packages/app/src/i18n/pl.ts
+++ b/packages/app/src/i18n/pl.ts
@@ -514,6 +514,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "Połącz dowolnego dostawcę, aby używać modeli, w tym Claude, GPT, Gemini itp.",
"sidebar.project.recentSessions": "Ostatnie sesje",
"sidebar.project.viewAllSessions": "Zobacz wszystkie sesje",
+ "sidebar.project.clearNotifications": "Wyczyść powiadomienia",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Pulpit",
"settings.section.server": "Serwer",
diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts
index f2c87fe0f..93e5b2742 100644
--- a/packages/app/src/i18n/ru.ts
+++ b/packages/app/src/i18n/ru.ts
@@ -578,6 +578,7 @@ export const dict = {
"Подключите любого провайдера для использования моделей, включая Claude, GPT, Gemini и др.",
"sidebar.project.recentSessions": "Недавние сессии",
"sidebar.project.viewAllSessions": "Посмотреть все сессии",
+ "sidebar.project.clearNotifications": "Очистить уведомления",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Приложение",
diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts
index 689e82118..3b3486b5c 100644
--- a/packages/app/src/i18n/th.ts
+++ b/packages/app/src/i18n/th.ts
@@ -571,6 +571,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ",
"sidebar.project.recentSessions": "เซสชันล่าสุด",
"sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด",
+ "sidebar.project.clearNotifications": "ล้างการแจ้งเตือน",
"app.name.desktop": "OpenCode Desktop",
diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts
index 1b40013b6..6489b7025 100644
--- a/packages/app/src/i18n/zh.ts
+++ b/packages/app/src/i18n/zh.ts
@@ -569,6 +569,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。",
"sidebar.project.recentSessions": "最近会话",
"sidebar.project.viewAllSessions": "查看全部会话",
+ "sidebar.project.clearNotifications": "清除通知",
"app.name.desktop": "OpenCode Desktop",
diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts
index 34aec01b9..a01b76c05 100644
--- a/packages/app/src/i18n/zht.ts
+++ b/packages/app/src/i18n/zht.ts
@@ -567,6 +567,7 @@ export const dict = {
"sidebar.gettingStarted.line2": "連線任意提供者即可使用更多模型,如 Claude、GPT、Gemini 等。",
"sidebar.project.recentSessions": "最近工作階段",
"sidebar.project.viewAllSessions": "查看全部工作階段",
+ "sidebar.project.clearNotifications": "清除通知",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "桌面",
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 7eb064f42..7d4a5c0cb 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -1692,6 +1692,13 @@ export default function Layout(props: ParentProps) {
})
const projectId = createMemo(() => panelProps.project?.id ?? "")
const workspaces = createMemo(() => workspaceIds(panelProps.project))
+ const unseenCount = createMemo(() =>
+ workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
+ )
+ const clearNotifications = () =>
+ workspaces()
+ .filter((directory) => notification.project.unseenCount(directory) > 0)
+ .forEach((directory) => notification.project.markViewed(directory))
const workspacesEnabled = createMemo(() => {
const project = panelProps.project
if (!project) return false
@@ -1769,6 +1776,16 @@ export default function Layout(props: ParentProps) {
: language.t("sidebar.workspaces.enable")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
+ <DropdownMenu.Item
+ data-action="project-clear-notifications"
+ data-project={base64Encode(p().worktree)}
+ disabled={unseenCount() === 0}
+ onSelect={clearNotifications}
+ >
+ <DropdownMenu.ItemLabel>
+ {language.t("sidebar.project.clearNotifications")}
+ </DropdownMenu.ItemLabel>
+ </DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
data-action="project-close-menu"
diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx
index 28b129bf7..e19e6f430 100644
--- a/packages/app/src/pages/layout/sidebar-project.tsx
+++ b/packages/app/src/pages/layout/sidebar-project.tsx
@@ -10,6 +10,7 @@ import { createSortable } from "@thisbeyond/solid-dnd"
import { type LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
+import { useNotification } from "@/context/notification"
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
import { projectSelected, projectTileActive } from "./sidebar-project-helpers"
@@ -59,6 +60,7 @@ const ProjectTile = (props: {
selected: Accessor<boolean>
active: Accessor<boolean>
overlay: Accessor<boolean>
+ dirs: Accessor<string[]>
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
onProjectMouseLeave: (worktree: string) => void
onProjectFocus: (worktree: string) => void
@@ -70,73 +72,94 @@ const ProjectTile = (props: {
setMenu: (value: boolean) => void
setOpen: (value: boolean) => void
language: ReturnType<typeof useLanguage>
-}): JSX.Element => (
- <ContextMenu
- modal={!props.sidebarHovering()}
- onOpenChange={(value) => {
- props.setMenu(value)
- if (value) props.setOpen(false)
- }}
- >
- <ContextMenu.Trigger
- as="button"
- type="button"
- aria-label={displayName(props.project)}
- data-action="project-switch"
- data-project={base64Encode(props.project.worktree)}
- classList={{
- "flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
- "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": props.selected(),
- "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
- !props.selected() && !props.active(),
- "bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
- }}
- onMouseEnter={(event: MouseEvent) => {
- if (!props.overlay()) return
- props.onProjectMouseEnter(props.project.worktree, event)
- }}
- onMouseLeave={() => {
- if (!props.overlay()) return
- props.onProjectMouseLeave(props.project.worktree)
- }}
- onFocus={() => {
- if (!props.overlay()) return
- props.onProjectFocus(props.project.worktree)
+}): JSX.Element => {
+ const notification = useNotification()
+ const unseenCount = createMemo(() =>
+ props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
+ )
+
+ const clear = () =>
+ props
+ .dirs()
+ .filter((directory) => notification.project.unseenCount(directory) > 0)
+ .forEach((directory) => notification.project.markViewed(directory))
+
+ return (
+ <ContextMenu
+ modal={!props.sidebarHovering()}
+ onOpenChange={(value) => {
+ props.setMenu(value)
+ if (value) props.setOpen(false)
}}
- onClick={() => props.navigateToProject(props.project.worktree)}
- onBlur={() => props.setOpen(false)}
>
- <ProjectIcon project={props.project} notify />
- </ContextMenu.Trigger>
- <ContextMenu.Portal mount={!props.mobile ? props.nav() : undefined}>
- <ContextMenu.Content>
- <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
- <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
- </ContextMenu.Item>
- <ContextMenu.Item
- data-action="project-workspaces-toggle"
- data-project={base64Encode(props.project.worktree)}
- disabled={props.project.vcs !== "git" && !props.workspacesEnabled(props.project)}
- onSelect={() => props.toggleProjectWorkspaces(props.project)}
- >
- <ContextMenu.ItemLabel>
- {props.workspacesEnabled(props.project)
- ? props.language.t("sidebar.workspaces.disable")
- : props.language.t("sidebar.workspaces.enable")}
- </ContextMenu.ItemLabel>
- </ContextMenu.Item>
- <ContextMenu.Separator />
- <ContextMenu.Item
- data-action="project-close-menu"
- data-project={base64Encode(props.project.worktree)}
- onSelect={() => props.closeProject(props.project.worktree)}
- >
- <ContextMenu.ItemLabel>{props.language.t("common.close")}</ContextMenu.ItemLabel>
- </ContextMenu.Item>
- </ContextMenu.Content>
- </ContextMenu.Portal>
- </ContextMenu>
-)
+ <ContextMenu.Trigger
+ as="button"
+ type="button"
+ aria-label={displayName(props.project)}
+ data-action="project-switch"
+ data-project={base64Encode(props.project.worktree)}
+ classList={{
+ "flex items-center justify-center size-10 p-1 rounded-lg overflow-hidden transition-colors cursor-default": true,
+ "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": props.selected(),
+ "bg-transparent border border-transparent hover:bg-surface-base-hover hover:border-border-weak-base":
+ !props.selected() && !props.active(),
+ "bg-surface-base-hover border border-border-weak-base": !props.selected() && props.active(),
+ }}
+ onMouseEnter={(event: MouseEvent) => {
+ if (!props.overlay()) return
+ props.onProjectMouseEnter(props.project.worktree, event)
+ }}
+ onMouseLeave={() => {
+ if (!props.overlay()) return
+ props.onProjectMouseLeave(props.project.worktree)
+ }}
+ onFocus={() => {
+ if (!props.overlay()) return
+ props.onProjectFocus(props.project.worktree)
+ }}
+ onClick={() => props.navigateToProject(props.project.worktree)}
+ onBlur={() => props.setOpen(false)}
+ >
+ <ProjectIcon project={props.project} notify />
+ </ContextMenu.Trigger>
+ <ContextMenu.Portal mount={!props.mobile ? props.nav() : undefined}>
+ <ContextMenu.Content>
+ <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
+ <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
+ </ContextMenu.Item>
+ <ContextMenu.Item
+ data-action="project-workspaces-toggle"
+ data-project={base64Encode(props.project.worktree)}
+ disabled={props.project.vcs !== "git" && !props.workspacesEnabled(props.project)}
+ onSelect={() => props.toggleProjectWorkspaces(props.project)}
+ >
+ <ContextMenu.ItemLabel>
+ {props.workspacesEnabled(props.project)
+ ? props.language.t("sidebar.workspaces.disable")
+ : props.language.t("sidebar.workspaces.enable")}
+ </ContextMenu.ItemLabel>
+ </ContextMenu.Item>
+ <ContextMenu.Item
+ data-action="project-clear-notifications"
+ data-project={base64Encode(props.project.worktree)}
+ disabled={unseenCount() === 0}
+ onSelect={clear}
+ >
+ <ContextMenu.ItemLabel>{props.language.t("sidebar.project.clearNotifications")}</ContextMenu.ItemLabel>
+ </ContextMenu.Item>
+ <ContextMenu.Separator />
+ <ContextMenu.Item
+ data-action="project-close-menu"
+ data-project={base64Encode(props.project.worktree)}
+ onSelect={() => props.closeProject(props.project.worktree)}
+ >
+ <ContextMenu.ItemLabel>{props.language.t("common.close")}</ContextMenu.ItemLabel>
+ </ContextMenu.Item>
+ </ContextMenu.Content>
+ </ContextMenu.Portal>
+ </ContextMenu>
+ )
+}
const ProjectPreviewPanel = (props: {
project: LocalProject
@@ -254,6 +277,7 @@ export const SortableProject = (props: {
)
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
+ const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
const [open, setOpen] = createSignal(false)
const [menu, setMenu] = createSignal(false)
@@ -304,6 +328,7 @@ export const SortableProject = (props: {
selected={selected}
active={active}
overlay={overlay}
+ dirs={dirs}
onProjectMouseEnter={props.ctx.onProjectMouseEnter}
onProjectMouseLeave={props.ctx.onProjectMouseLeave}
onProjectFocus={props.ctx.onProjectFocus}