?k1Ro3ZJ5
zf@OGNCIZrJd2*EqWs^=c;~mEIvozfvmcF1p&|dTAh>$39OA)o|Lb_wcG&U;gN%`oi
z#Xb%tp|T4%wkKXVi)4k3KGw|*Pc?iOg6KH@y}R^}z*37LqkK}drg5PH8b*WB=bEp2
zfa_1`yVZkGD^y%Pn8A8EZ`!+IZ>)X+1)Vg)es`vq+J~H-y}kR7E%|>vAOC;gWTf5*
zPGo2(^S|SX7~!;angK8bX5X1W1M}}Jy?s=&jQ_B1#s3qGo#f)a_+KvI|8YjYLh;4y
z_dlclZ_unNf?h_mGWb&i<$(>UM05Jz9YL>(-gNB%H~J+Qveaf+(8|U>*xR`wVn54bZ0|5%Tq(0a*HUvyOx&
z0CL6@u!Zj*=fi2Of|UB7c`*hPs5*uQ1~0P>0G-_T_W+Cl2p=PSlnNk|cD?11--{4sf+ifh_nScbDVTR#Jz4Dl0ec@ly~y
zPHbA*1JL|H$|uq`n-~iRIM{GCJD70o99ZXG1`ZbSV)enx0DzEx;q!E!33h0XFd$-V
z;PUhVg|%4(ERw*>hJhljNSIv@`cf!D*FUZq@5+GKOzR2mXucrB9d;Yz@nuX^KpL=F
z1*#i3tR}L+wWat2n5GB7q`WFHAp;>(0E7h~8UNd8q)eafIUfO>29AqxHltf0!}So!
zP4i3iwqWVFwlR<)hqfA85%Rer_o;&p3A7a80S{ZC`CYzHh;NPC_Gu^Iubs8SF&f?8
zoUQ}DYxV1E2z3d@7GSCd`T!j;@a*^Y&)c-78#j%EA@&>@X0vn=9j{AOSR6JpR
zDgMm2*ZWgN!Kl@NXtKKX_QpGqpxTc!5UVb`u1DtOz5i}8S*V{tRV20tE=xZ(Scng!
zM7!~OZ=+mW3+
zo5^nRT{UOd>p~e^=l$jWwmdg*zk!!*q^Vxpr-(^N@Qj#{c4n@$)uB5y1p^k1JGIR7
zV20V0oq=K4mGr5+yqpfqzXXQ}W^UNokpGL8@eiN)FP;F!TJ6FZxJi-~FkB#;>_-$@
z0ftKK(Mc_<-Of$!SG@550vZW9%f(bk5k#-kj?Ey_|BcfMWvsF
zW-!JtX~ujCXuQ;E#4Z2%h_g{@@RAfw1`-)1$GNE^`MHX&c2m7GLAe3!4H1KsIpBa!5z;
zpmp>BjXH39`NCHRA}`ng^CWmrhpziBIlhW>6W~^p+KeP(#}9x=vlCeOFB7-Hq+-)+
zH(ac=`HvaBgbPS6i@MpX!0+l0`nXtnJHyW8i22!=&20$j-|pK)!7N
zN;RNQp^L`TKsN+vSR=nf*`bnu`I-yvH
z9_90EeqB%*3JVKELZCf(H3FdStxh;8(!6gyY_J~b@
z6r7_vNPz%-tlnb~x&hjQ=3^m~`S|T$RrFJ+l7q=x>9Nii
z^RcubDB9f;{WF2EYY~vt-7wuilj$=b2M{y@%WPu0M3WOpQ=b1wHR47}>lCiJz{%+6z
zct5?z|I_=;F|N6fd+zJLe#`kg&y(i)h|oifg~$N5Cri&N2i8`G6Ni(S6@WszR#-x%
zhwMyYB3`GDpyRvYef}XrT^&n1lQ-kSY)}SlFqr+|Gc(Qg7?4K1$8P&YYL?;v1yc(2
zk04YB3moL19_~5znJ&&-9jNrGlFdKr@ut*uN?9*J8uBSbvOPP*w}Ci
z%t2Vf7cc9@$$>Khx)Nelnr~I31R$xdz!yVE$WA^$nWFkLM1G9B5Ei?`YO=hs3f(WD
z^10})c8+-e{w-l~CSp?s(01f+5
z(&_H1@pS;SQE&Q!;5C;D(>ni#;JMhAnZR`KsOxJ8
zD{y(FnF6*xho^eR5^K3s9&~RJamz6!imEC#YMj2zQ%mCu(tj?6;t?IQwT_`D+O
zAh9dwIcssw+;>y8ps6ocs|W8&A)-esb>TauV8~Qdns*}$YNK;+Z}@|=Nh4z8^4M%n
z>5|4V{#e8VU|nCS$Z{cTPU}ZRH4JCP>Uhr~JpS+aX1N@I>QV;``fchUeiM_u))2;%
z?jgdZ`R5maYmBI~))=53tle!)ce-}(YGv>9rOEE*=@gjas^6XKq4YSH`pMOmfy0%j
zY`nE`0by7#h8hMr722`ey)m`=l6?*V7rHhfYP1SjaU=FjU83S#i50OiUHk50hUY95
zJ@RhoRf;yBu3hqd4P~-3I)UM>r7G7H|945vmcX@!ItX25fZwj~|JD+@7CM^j09zKV
z8%NLf&e3RYFvFH4bbRiDGNwvF<9v5q2b=t>+CH)Q<(nb>qa}b}22YQ?6i73j)Al%f+|0P(m`i!Tv7IuBLL*lh2}
zuJU2jw@(@Ct5d^b_m1e!*in7n-gF+kqm7~VI=3G_T=;E*9ZDHb?XYyrWu>`Bnt{4?
zi3_i%NQ217JZ1a-RxcPRAJ>HE?CSuKG)MTJ`UFV{9GpiuOsv^Rjl2eX0p~wbUz{f#
z&Pd-zV`HOf79q$=I`hCXH0*QRNAooZ|*Yue@7e&vx3SIxKqu6_roI+9UptiqT{@eS6aIiK+
zNEzo%9Urn*kDfzyU~v(UejU(ctu|*oh0}c6=BAop@u{cTiNT6`wD5BMKz02r9WZ0Na^p2u#(Z
zO^cu&s@SWm1ao_Xas5z}BK6B;R7x>qgqCKY)a!yo<0u$BWo9Ar;gpmVfYT8*?V0Nm
z*(bca#-WQcu*dm-I0RLw!B@nc>QrV;vlUP-Jo(~aJaFk-g`6BK-enae-`X&6CkX?A
zw_eih1Qb-NAB;PN6u$x!hq1L>6Ud^ij2~%0gss7+YheryybZ@H4wb~=--4Ce!P97sM
zA)1#vUzX{g5b`-RF7GOh7I>vj*A|khPm<>2pgx&P!ey`~i2?=I{sME?#uE}T-bk}UR9-cYU!y?GS{(?!PjaJB!s9Tkwj4nlCo_B}
z_{XmiIkzv;T34eynb{!}-jyN0^O%(&6?2nm6k##FjArm=bLh0)z$-AXsU|Vs`-_Ns
z%blmGy|CS?rLS(h=U*&TufBp&nBX@(!NP&-?8*?hgWQ-EO%r)^mZ$5nnfVPiyC%%H
zAJtINJ4S#pVKIMBQag7C?UF>&mu=l6{@|s-+L1IV18n-mhz85;wI~y=DUEC@Prs^Iu(Aaf6qZ7GYO{g1ohqb_inGz(dW$G
zkMq|1iglN^c?930Ia;sL@_9S|g1=$vs{H?G5c$6V*nh*c{tq0>xD4qS4S7gOSB5xk
zNVfaJ3kr-`eu``>YTe}|hi%-r;t$p-Z0m3h?Qb6lnHrHcz_M_#Y>$9Zg)!FmIS--O
zx5c|LvZB&`QYRG*DgXLhI_igJ&0slEKL7dePERZgQR3fAJ-jcgc+8OHKMNxw7WAVc
z25OIPx5)n$nEUrf7*_a?r1VjLm1t&AGt$Z~4^F)LEiH5Gs~{=lhqbPU#8C=*P`r8p
zGeofrIh6b-aobXp=+ol(U}5}n9`19XQcrwZ`oesiZ1`a;kKMuyEhn)CO}9LvDLNNE
zC^;Id`Zt?lN+A8$&n^am%q{5I+3fK0!Qohi*3bnnNo4odrO(ucYMl(HzI{i}c}j^1
zl&OlxavS$tF`XC?U7Hl+{$yvJhIn*2yH7_1B2?ps(0nbMAzE8_}kibiFRIM6IFa
zyP$DJ8O3eucim&Ytdz}PS<&_wo-H7|gqjty-$YSOpTq>n7`3iQPgKXEF~o}_GOW6?
zX2n=}`VY7>YUZDx4(POs+zKzgsXK+x!f&&!XX+zAEq?&*>8+P+
zb;%61`ny3%oM96+*n$r6KZ{zLj5yTjVP<468*q#;P|EGp{j`zsTbD{0>5fVUN{Vgc(-X?0|SsNGcGYhnZ)icLMtl&WE{e=%@Ic&}*M#hwCW}DeiiQE5N
zpK&8|S=%ie&>xxDIIG5otY*{Loo&fQX#V*y{e*vrqvj^EPRScfWb|yXfXiF2J<=%!
zl{&(S>u1*2vJQu2CU6a7LnZ;UrcGrGr8k1H3r_Qkw&7(t^|
zx;!d3!%opnNTjMQnaP^>^qyj##g;EoV)dJ|RvD9+>+D({wWNv7V16viQ2I@oy0>EfjA`yR_+)Yb`a?O2xs@!#*-|DwUH)<9)Mpib}y&!dE
z29BJW^x2$Gw-t5$^fAeE(7eb-l&%{|Pm&7qo>ld;mOCp|XCHzo%ug$6aWk~i^W2Ao
zA%2sy>8J3cZJyrP*u&~tz75G3A4;Z@kb|%3=5(TM)=T{B7{%q$!mw6phtjYF(-T8$
zpQPmkHr~5^eJxpj=ECP(LSSIhsS4pl%Y-%0A0e$nBN0rfnIq}UGG!iO0V^q~iVQ}{
zdJ(}FJJ^~RsQ;plBa!;>Ilb`08jHKYd@l~YeG2YT+3O?C^HnK7-);}NdKOUnoJ^%m!
literal 0
HcmV?d00001
diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts
index 78fdbc21562..ad53263635f 100644
--- a/packages/app/src/runtime/i18n/en.ts
+++ b/packages/app/src/runtime/i18n/en.ts
@@ -954,6 +954,7 @@ export const dict = {
"settings.desktop.wsl.title": "WSL integration",
"settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.",
"dialog.server.authenticate.title": "Authenticate",
+ "project.settings.title": "Edit project",
"project.settings.general.description": "Manage project name and appearance",
"project.settings.scripts": "Scripts",
"project.settings.scripts.description": "Configure scripts for this project",
diff --git a/packages/app/src/session/session-identity-header.css b/packages/app/src/session/session-identity-header.css
new file mode 100644
index 00000000000..1c7d6b47b4e
--- /dev/null
+++ b/packages/app/src/session/session-identity-header.css
@@ -0,0 +1,55 @@
+/* Informational tooltips must not block menu items. Kobalte sets pointer events inline. */
+[data-popper-positioner]:has(.session-project-info-tooltip),
+.session-project-info-tooltip {
+ pointer-events: none !important;
+}
+
+.session-options-menu [data-component="menu-v2-item"] {
+ padding-inline-end: 30px;
+}
+
+.session-project-link {
+ --session-project-fade-direction: to right;
+ position: relative;
+
+ &:dir(rtl) {
+ --session-project-fade-direction: to left;
+ }
+
+ &[aria-disabled="true"] {
+ opacity: 0.5;
+ }
+}
+
+.session-project-link-content {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ align-items: center;
+ gap: 8px;
+}
+
+.session-project-link-open {
+ position: absolute;
+ inset-inline-end: 12px;
+ inset-block: 0;
+ display: flex;
+ align-items: center;
+ color: var(--v2-icon-icon-muted);
+ pointer-events: none;
+ opacity: 0;
+}
+
+.session-project-link:is(:hover, [data-highlighted]):not([aria-disabled="true"]) {
+ .session-project-link-content {
+ mask-image: linear-gradient(
+ var(--session-project-fade-direction),
+ #000 calc(100% - 40px),
+ transparent calc(100% - 24px)
+ );
+ }
+
+ .session-project-link-open {
+ opacity: 1;
+ }
+}
diff --git a/packages/app/src/session/session-identity-header.tsx b/packages/app/src/session/session-identity-header.tsx
index f0c1528f3f7..2506609a088 100644
--- a/packages/app/src/session/session-identity-header.tsx
+++ b/packages/app/src/session/session-identity-header.tsx
@@ -1,18 +1,27 @@
import type { SessionInfo } from "@opencode-ai/client/promise"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Icon } from "@opencode-ai/ui/icon"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { Menu } from "@opencode-ai/ui/menu"
import { ProjectAvatar } from "@opencode-ai/ui/project-avatar"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
+import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useNavigate } from "@solidjs/router"
import { createMemo, Show, type ParentProps } from "solid-js"
+import { createStore } from "solid-js/store"
import { useServer } from "@/runtime/server/current"
import { useLanguage } from "@/runtime/i18n/language"
-import { displayName, getProjectAvatarSource, projectForSession } from "@/shell/layout/helpers"
-import { getProjectAvatarVariant } from "@/shell/state/layout"
+import { usePlatform } from "@/runtime/platform/platform"
+import { displayName, errorMessage, getProjectAvatarSource, projectForSession } from "@/shell/layout/helpers"
+import { getProjectAvatarVariant, useLayout, type LocalProject } from "@/shell/state/layout"
import { tabKey, useTabs } from "@/shell/tabs/tabs"
import { useSettings } from "@/settings/model"
import { pathKey } from "@/workspaces/path-key"
-import { isWorkspaceDirectory } from "@/workspaces/paths"
+import { isProjectDirectory, isWorkspaceDirectory } from "@/workspaces/paths"
import { sessionHref } from "@/shell/routes/session"
+import { showToast } from "@/shell/notifications/toast"
import { sessionTitle } from "./title"
+import "./session-identity-header.css"
export function SessionTitleHeader(props: ParentProps) {
return (
@@ -25,6 +34,173 @@ export function SessionTitleHeader(props: ParentProps) {
)
}
+export function SessionProjectMenu(props: {
+ project?: Omit
+ directory?: string
+ workspace: boolean
+ showProjectIcon: boolean
+}) {
+ const server = useServer()
+ const language = useLanguage()
+ const dialog = useDialog()
+ const platform = usePlatform()
+ const layout = useLayout()
+ const navigate = useNavigate()
+ const [state, setState] = createStore({
+ open: false,
+ projectTruncated: false,
+ pathTruncated: false,
+ pathFocused: false,
+ })
+ const projectName = createMemo(() => displayName(props.project ?? { worktree: props.directory ?? "" }))
+ const canOpenPath = () =>
+ platform.platform === "desktop" && !!platform.openPath && server.isLocal && !!props.directory
+ const openPath = () => {
+ if (!canOpenPath() || !platform.openPath || !props.directory) return
+ void platform.openPath(props.directory).catch((cause: unknown) =>
+ showToast({
+ title: language.t("common.requestFailed"),
+ description: errorMessage(cause, language.t("common.requestFailed")),
+ }),
+ )
+ }
+ const openProjectSettings = async () => {
+ const current = props.project
+ if (!current) return
+ const { DialogEditProject } = await import("@/settings/workspaces/project-dialog")
+ dialog.push(() => )
+ }
+
+ return (
+
+ )
+}
+
export function SessionIdentityHeader(props: { sessionID: string; session?: SessionInfo }) {
const server = useServer()
const tabs = useTabs()
@@ -71,12 +247,17 @@ export function SessionIdentityHeader(props: { sessionID: string; session?: Sess
)
const project = createMemo(() => {
const projects = server.ctx.projects.list()
- if (props.session) return projectForSession(props.session, projects)
+ if (props.session)
+ return (
+ projectForSession(props.session, projects) ?? projectForSession(props.session, server.ctx.sync.data.project)
+ )
const value = directory()
if (!value) return undefined
const key = pathKey(value)
- return projects.find(
- (item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
+ return (
+ projects.find(
+ (item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
+ ) ?? server.ctx.sync.data.project.find((item) => isProjectDirectory(item, value))
)
})
const showProjectIcon = () =>
@@ -95,25 +276,13 @@ export function SessionIdentityHeader(props: { sessionID: string; session?: Sess
-
-
- }
- >
-
-
-
+
+
{(value) => (
|