Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 51 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -212710,6 +212710,57 @@
}
}
}
},
"titlebar.home.accessibilityLabel": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Home"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ホーム"
}
}
}
},
"titlebar.home.tooltip": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Show Home"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ホームを表示"
}
}
}
},
"home.workspace.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Home"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ホーム"
}
}
}
}
}
}

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import{c as h,j as e,i as s,b as m}from"./vendor.mjs";import{i as d}from"./installWebviewStyles.mjs";const p=":root{color-scheme:light dark;--home-bg: light-dark(#f6f7f9, #111214);--home-fg: light-dark(#15171a, #f3f4f5);--home-muted: light-dark(#626973, #a9b0ba);--home-border: light-dark(rgb(0 0 0 / 12%), rgb(255 255 255 / 14%));--home-card: light-dark(rgb(255 255 255 / 82%), rgb(255 255 255 / 7%));--home-card-hover: light-dark(#fff, rgb(255 255 255 / 11%));--home-shadow: light-dark(rgb(20 26 35 / 10%), rgb(0 0 0 / 20%));--home-blue: #0a84ff;--home-green: #25a244;--home-violet: #9b5cff;--home-orange: #ff9500;background:transparent;color:var(--home-fg);font-family:system-ui,-apple-system,BlinkMacSystemFont,SF Pro Text,Helvetica Neue,Arial,sans-serif;font-size:13px;line-height:1.4}html,body,#root{width:100%;height:100%;margin:0;overflow:hidden;background:transparent}*{box-sizing:border-box}button{font:inherit}.home-shell{min-height:100%;padding:34px 32px;display:grid;align-content:start;gap:26px;background:linear-gradient(135deg,color-mix(in lab,var(--home-blue) 10%,transparent),transparent 42%),var(--home-bg);color:var(--home-fg)}.home-header{display:flex;align-items:center;gap:14px;min-width:0}.home-mark,.home-action-icon{display:grid;place-items:center;flex:0 0 auto}.home-mark{width:48px;height:48px;border-radius:8px;color:#fff;background:linear-gradient(135deg,#0a84ff,#14b8a6);box-shadow:0 10px 24px var(--home-shadow)}.home-mark svg{width:25px;height:25px}.home-heading{min-width:0}.home-heading h1{margin:0;font-size:28px;line-height:1.05;font-weight:720;letter-spacing:0}.home-heading p{margin:5px 0 0;color:var(--home-muted);font-size:13px;font-weight:500}.home-actions{width:min(100%,920px);display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}.home-action{min-height:78px;display:flex;align-items:center;gap:12px;padding:14px;border:1px solid var(--home-border);border-radius:8px;background:var(--home-card);color:inherit;text-align:left;cursor:default;box-shadow:0 1px #ffffff1f inset}.home-action:hover{background:var(--home-card-hover);border-color:color-mix(in lab,var(--home-fg) 24%,transparent)}.home-action:focus-visible{outline:2px solid color-mix(in lab,var(--home-blue) 80%,white);outline-offset:2px}.home-action:disabled{opacity:.62}.home-action-icon{width:36px;height:36px;border-radius:8px}.home-action-icon svg{width:20px;height:20px}.home-action-icon-newWorkspace{color:var(--home-green);background:color-mix(in lab,var(--home-green) 16%,transparent)}.home-action-icon-newBrowser{color:var(--home-blue);background:color-mix(in lab,var(--home-blue) 16%,transparent)}.home-action-icon-commandPalette{color:var(--home-violet);background:color-mix(in lab,var(--home-violet) 16%,transparent)}.home-action-icon-settings{color:var(--home-orange);background:color-mix(in lab,var(--home-orange) 16%,transparent)}.home-action svg{fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}.home-action-copy{display:grid;min-width:0;gap:2px}.home-action-title{font-size:14px;font-weight:680;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.home-action-subtitle{color:var(--home-muted);font-size:12px;font-weight:500}.home-error{margin:0;width:min(100%,920px);color:light-dark(#b42318,#ffb4ab);font-size:12px}@media(max-width:560px){.home-shell{padding:22px 18px}.home-actions{grid-template-columns:minmax(0,1fr)}}",x=[{id:"newWorkspace",title:"New Workspace",subtitle:"Start a terminal workspace",icon:"terminal"},{id:"newBrowser",title:"Browser",subtitle:"Open a browser workspace",icon:"browser"},{id:"commandPalette",title:"Command Palette",subtitle:"Run commands and switch workspaces",icon:"command"},{id:"settings",title:"Settings",subtitle:"Open preferences",icon:"settings"}];function g(){const t=window.webkit?.messageHandlers?.cmuxHome;return t&&typeof t.postMessage=="function"?t:null}async function f(t){const a=g();if(!a)throw new Error("Home bridge is unavailable.");const i=await a.postMessage({action:t});if(!i?.ok)throw new Error(i?.error?.code??"action_failed")}function u(){const[t,a]=s.useState(null),[i,o]=s.useState(null);async function c(r){a(r),o(null);try{await f(r)}catch(n){o(n instanceof Error?n.message:"Action failed.")}finally{a(null)}}return e.jsxs("main",{className:"home-shell","aria-label":"Home",children:[e.jsxs("section",{className:"home-header",children:[e.jsx("div",{className:"home-mark","aria-hidden":"true",children:e.jsx(l,{name:"home"})}),e.jsxs("div",{className:"home-heading",children:[e.jsx("h1",{children:"Home"}),e.jsx("p",{children:"Choose a workspace action."})]})]}),e.jsx("section",{className:"home-actions","aria-label":"Workspace actions",children:x.map(r=>e.jsxs("button",{type:"button",className:"home-action",disabled:t!=null,onClick:()=>{c(r.id)},children:[e.jsx("span",{className:`home-action-icon home-action-icon-${r.id}`,"aria-hidden":"true",children:e.jsx(l,{name:r.icon})}),e.jsxs("span",{className:"home-action-copy",children:[e.jsx("span",{className:"home-action-title",children:r.title}),e.jsx("span",{className:"home-action-subtitle",children:r.subtitle})]})]},r.id))}),i?e.jsx("p",{className:"home-error",role:"status",children:i}):null]})}function l(t){const a=m.c(5),{name:i}=t;switch(i){case"browser":{let o;return a[0]===Symbol.for("react.memo_cache_sentinel")?(o=e.jsxs("svg",{viewBox:"0 0 20 20",focusable:"false",children:[e.jsx("circle",{cx:"10",cy:"10",r:"7"}),e.jsx("path",{d:"M3.5 10h13"}),e.jsx("path",{d:"M10 3a10 10 0 0 1 0 14"}),e.jsx("path",{d:"M10 3a10 10 0 0 0 0 14"})]}),a[0]=o):o=a[0],o}case"command":{let o;return a[1]===Symbol.for("react.memo_cache_sentinel")?(o=e.jsx("svg",{viewBox:"0 0 20 20",focusable:"false",children:e.jsx("path",{d:"M7 7H5.5a2.5 2.5 0 1 1 2.5-2.5V7h4V4.5A2.5 2.5 0 1 1 14.5 7H13v4h1.5A2.5 2.5 0 1 1 12 13.5V12H8v1.5A2.5 2.5 0 1 1 5.5 11H7V7Z"})}),a[1]=o):o=a[1],o}case"settings":{let o;return a[2]===Symbol.for("react.memo_cache_sentinel")?(o=e.jsxs("svg",{viewBox:"0 0 20 20",focusable:"false",children:[e.jsx("circle",{cx:"10",cy:"10",r:"2.5"}),e.jsx("path",{d:"M10 2.8v2"}),e.jsx("path",{d:"M10 15.2v2"}),e.jsx("path",{d:"m4.9 4.9 1.4 1.4"}),e.jsx("path",{d:"m13.7 13.7 1.4 1.4"}),e.jsx("path",{d:"M2.8 10h2"}),e.jsx("path",{d:"M15.2 10h2"}),e.jsx("path",{d:"m4.9 15.1 1.4-1.4"}),e.jsx("path",{d:"m13.7 6.3 1.4-1.4"})]}),a[2]=o):o=a[2],o}case"terminal":{let o;return a[3]===Symbol.for("react.memo_cache_sentinel")?(o=e.jsxs("svg",{viewBox:"0 0 20 20",focusable:"false",children:[e.jsx("rect",{x:"3",y:"4",width:"14",height:"12",rx:"2"}),e.jsx("path",{d:"m6 8 2.5 2L6 12"}),e.jsx("path",{d:"M10 12h4"})]}),a[3]=o):o=a[3],o}default:{let o;return a[4]===Symbol.for("react.memo_cache_sentinel")?(o=e.jsxs("svg",{viewBox:"0 0 20 20",focusable:"false",children:[e.jsx("path",{d:"M3.5 9.5 10 4l6.5 5.5"}),e.jsx("path",{d:"M5 8.5V16h10V8.5"})]}),a[4]=o):o=a[4],o}}}function v(t){d("home",p),document.title="Home",h.createRoot(t).render(e.jsx(u,{}))}export{v as mountHomeSurface};
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import{e as u,f as a,g as i,d,O as f}from"./vendor.mjs";function l(){const t=globalThis.crypto;if(typeof t?.randomUUID=="function")return t.randomUUID();const o=t?.getRandomValues?.bind(t);if(typeof o=="function"){const e=new Uint8Array(16);return o(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128,[...e].map((r,s)=>{const n=r.toString(16).padStart(2,"0");return[4,6,8,10].includes(s)?`-${n}`:n}).join("")}return`cmux-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}function p(t){const o=u({component:f,notFoundComponent:t}),e=a({getParentRoute:()=>o,path:"/",component:t}),r=a({getParentRoute:()=>o,path:"/diff",component:t}),s=a({getParentRoute:()=>o,path:"/cmux-diff-viewer",component:t}),n=a({getParentRoute:()=>o,path:"/agent-session",component:t}),c=o.addChildren([e,r,s,n]);return i({history:d(),routeTree:c})}function R(t,o){const e=document.createElement("style");e.dataset.cmuxWebviewStyle=t,e.textContent=o,document.head.append(e)}export{p as c,R as i,l as m};
function a(t,n){const e=document.createElement("style");e.dataset.cmuxWebviewStyle=t,e.textContent=n,document.head.append(e)}export{a as i};
1 change: 1 addition & 0 deletions Resources/markdown-viewer/webviews-app/chunks/router.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import{e as i,f as n,g as d,d as f,O as R}from"./vendor.mjs";function p(){const t=globalThis.crypto;if(typeof t?.randomUUID=="function")return t.randomUUID();const e=t?.getRandomValues?.bind(t);if(typeof e=="function"){const o=new Uint8Array(16);return e(o),o[6]=o[6]&15|64,o[8]=o[8]&63|128,[...o].map((a,c)=>{const r=a.toString(16).padStart(2,"0");return[4,6,8,10].includes(c)?`-${r}`:r}).join("")}return`cmux-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}function g(t){const e=i({component:R,notFoundComponent:t}),o=n({getParentRoute:()=>e,path:"/",component:t}),a=n({getParentRoute:()=>e,path:"/diff",component:t}),c=n({getParentRoute:()=>e,path:"/cmux-diff-viewer",component:t}),r=n({getParentRoute:()=>e,path:"/agent-session",component:t}),s=n({getParentRoute:()=>e,path:"/home",component:t}),u=e.addChildren([o,a,c,r,s]);return d({history:f(),routeTree:u})}export{g as c,p as m};
12 changes: 12 additions & 0 deletions Resources/markdown-viewer/webviews-app/home.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en" data-cmux-webview-kind="home">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home</title>
</head>
<body data-cmux-webview-kind="home">
<main id="root"></main>
<script type="module" src="./main.mjs"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion Resources/markdown-viewer/webviews-app/main.mjs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import{_ as n}from"./chunks/vendor.mjs";function o(){return document.documentElement.dataset.cmuxWebviewKind==="agent-session"||document.body.dataset.cmuxWebviewKind==="agent-session"||document.getElementById("cmux-agent-session-config")?"agent-session":"diff"}const t=document.getElementById("root");if(!t)throw new Error("Missing cmux webview root");o()==="agent-session"?n(()=>import("./chunks/agentSessionSurface.mjs"),[]).then(e=>{e.mountAgentSessionSurface(t)}):n(()=>import("./chunks/diffSurface.mjs"),[]).then(e=>{e.mountDiffSurface(t)});
import{_ as o}from"./chunks/vendor.mjs";function n(){return document.documentElement.dataset.cmuxWebviewKind==="home"||document.body.dataset.cmuxWebviewKind==="home"?"home":document.documentElement.dataset.cmuxWebviewKind==="agent-session"||document.body.dataset.cmuxWebviewKind==="agent-session"||document.getElementById("cmux-agent-session-config")?"agent-session":"diff"}const t=document.getElementById("root");if(!t)throw new Error("Missing cmux webview root");n()==="agent-session"?o(()=>import("./chunks/agentSessionSurface.mjs"),[]).then(e=>{e.mountAgentSessionSurface(t)}):n()==="home"?o(()=>import("./chunks/homeSurface.mjs"),[]).then(e=>{e.mountHomeSurface(t)}):o(()=>import("./chunks/diffSurface.mjs"),[]).then(e=>{e.mountDiffSurface(t)});
99 changes: 99 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6637,6 +6637,105 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
return false
}

@discardableResult
func showHomeInActiveMainWindow(preferredWindow: NSWindow? = nil) -> Bool {
func showHome(_ context: MainWindowContext) -> Bool {
guard let window = resolvedWindow(for: context) else {
discardOrphanedMainWindowContext(context)
return false
}
guard BrowserAvailabilitySettings.isEnabled(),
let homeURL = Self.homeWebViewURL() else {
NSSound.beep()
return false
Comment on lines +6647 to +6650

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the beep from this shared helper.

ContentView.showHome() and the minimal-titlebar callers already beep when this returns false, so this path produces two alert sounds when Home cannot open.

Suggested fix
             guard BrowserAvailabilitySettings.isEnabled(),
                   let homeURL = Self.homeWebViewURL() else {
-                NSSound.beep()
                 return false
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
guard BrowserAvailabilitySettings.isEnabled(),
let homeURL = Self.homeWebViewURL() else {
NSSound.beep()
return false
guard BrowserAvailabilitySettings.isEnabled(),
let homeURL = Self.homeWebViewURL() else {
return false
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 6647 - 6650, Remove the
NSSound.beep() call from the shared helper that guards
BrowserAvailabilitySettings.isEnabled() and Self.homeWebViewURL(), since the
false return is already handled by callers like ContentView.showHome() and the
minimal-titlebar path. Keep the guard’s return value behavior unchanged, and
leave the beep responsibility to those caller methods so the failure only alerts
once.

}
setActiveMainWindow(window)
context.sidebarSelectionState.selection = .tabs
ensureHomeWorkspace(in: context.tabManager, homeURL: homeURL)
return true
}

if let preferredWindow,
let preferredContext = contextForMainTerminalWindow(preferredWindow),
showHome(preferredContext) {
return true
}
if let keyWindow = shortcutRoutingKeyWindow,
let keyContext = contextForMainTerminalWindow(keyWindow),
showHome(keyContext) {
return true
}
if let mainWindow = NSApp.mainWindow,
let mainContext = contextForMainTerminalWindow(mainWindow),
showHome(mainContext) {
return true
}
if let activeManager = tabManager,
let activeContext = mainWindowContexts.values.first(where: { $0.tabManager === activeManager }),
showHome(activeContext) {
return true
}
for fallbackContext in Array(mainWindowContexts.values) where showHome(fallbackContext) {
return true
}
return false
}

private static func homeWebViewURL() -> URL? {
CmuxBundledWebViewURLSchemeHandler.homeURL()
}

@discardableResult
func ensureHomeWorkspace(in tabManager: TabManager, homeURL: URL) -> Workspace {
let title = String(localized: "home.workspace.title", defaultValue: "Home")
let workspace = tabManager.tabs.first { workspace in
workspace.customTitle == title || workspace.title == title
} ?? tabManager.addWorkspace(
Comment on lines +6691 to +6693

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a stable identifier for Home workspace

When any existing workspace has the localized display title “Home” (for example a user-renamed project or a browser page whose title is Home), this treats that user workspace as the dashboard and the following code then pins/reorders it and navigates or adds a browser surface. That can overwrite the user's current workspace instead of opening the Home dashboard; match a stable marker such as the bundled Home URL or dedicated metadata rather than the mutable display title.

Useful? React with 👍 / 👎.

title: title,
Comment on lines +6689 to +6694

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use a stable Home workspace identifier instead of the localized title.

This lookup can hijack any user workspace renamed to the localized Home title, and it will also miss the existing Home workspace after a locale change, creating or mutating the wrong tab before Lines 6707-6712 pin/reorder it. Home needs a non-user-editable identifier/flag persisted with the workspace, not a title comparison.

As per path instructions, "flag deriving the value from a window/pane/terminal title, name, or process-argv heuristic" for correctness-critical detection/identity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 6689 - 6694, The Home workspace
lookup in ensureHomeWorkspace currently relies on comparing the localized title,
which can collide with user-renamed workspaces and fail across locale changes.
Update the workspace identity logic to use a stable, non-user-editable
identifier or persisted flag on the Workspace created via
tabManager.addWorkspace, and have the tabManager.tabs.first lookup match that
identifier instead of title/customTitle. Keep the pin/reorder behavior
unchanged, but ensure the Home workspace is uniquely recognized regardless of
localization or user edits.

Source: Path instructions

workingDirectory: FileManager.default.homeDirectoryForCurrentUser.path,
initialSurface: .browser,
initialBrowserURL: homeURL,
initialBrowserOmnibarVisible: false,
initialBrowserTransparentBackground: true,
initialBrowserBypassRemoteProxy: true,
inheritWorkingDirectory: false,
select: false,
autoWelcomeIfNeeded: false,
autoRefreshMetadata: false
)
workspace.setCustomTitle(title)
if !workspace.isPinned {
tabManager.setPinned(workspace, pinned: true)
}
_ = tabManager.reorderWorkspace(tabId: workspace.id, toIndex: 0)
ensureHomeBrowserSurface(in: workspace, homeURL: homeURL)
tabManager.selectedTabId = workspace.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Collapse sidebar multi-selection when showing Home

Fresh evidence in this revision is that Home now selects its pinned workspace here by assigning selectedTabId directly. When the sidebar currently has multiple selected workspaces, ContentView's onChange(of: tabManager.selectedTabId) deliberately skips syncing selectedTabIds while the set size is greater than 1, so clicking the Home button leaves the old multi-selection active and subsequent sidebar batch/context-menu actions can still target those stale workspaces instead of Home. Route this through the shared workspace selection/collapse path or post the same collapse event used by keyboard navigation.

Useful? React with 👍 / 👎.

return workspace
}

private func ensureHomeBrowserSurface(in workspace: Workspace, homeURL: URL) {
let browserPanel = workspace.panels.values
.compactMap { $0 as? BrowserPanel }
.first ?? workspace.bonsplitController.allPaneIds.first.flatMap { paneId in
workspace.newBrowserSurface(
inPane: paneId,
url: homeURL,
focus: true,
creationPolicy: .automationPreload,
omnibarVisible: false,
transparentBackground: true,
bypassRemoteProxy: true
)
}
guard let browserPanel else { return }
browserPanel.setOmnibarVisible(false)
let currentURL = browserPanel.currentURLForTabDuplication ?? browserPanel.webView.url
if currentURL?.absoluteString != homeURL.absoluteString {
browserPanel.navigate(to: homeURL)
}
workspace.focusPanel(browserPanel.id)
}

@discardableResult
func toggleRightSidebarInActiveMainWindow(preferredWindow: NSWindow? = nil) -> Bool {
guard let context = preferredRegisteredMainWindowContext(preferredWindow: preferredWindow) else {
Expand Down
Loading
Loading