Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
157fa87
Reclaim offscreen terminal renderer GPU memory (IOSurface) non-destru…
lawrence703 Jun 11, 2026
f827c53
Pin GhosttyKit checksum for ghostty 858e257f0 (renderer_realized API)
lawrence703 Jun 11, 2026
a0bcc73
Refresh Swift file length budget for renderer-realization additions
lawrence703 Jun 11, 2026
c6ceb19
Address review: authoritative portal-visibility gate for renderer rel…
lawrence703 Jun 11, 2026
b64af83
Merge remote-tracking branch 'origin/main' into feat-surface-leak-repro
lawrence703 Jun 11, 2026
1851c92
Address review: complete settings integration for renderer reclamation
lawrence703 Jun 11, 2026
731f22a
Address review: wake renderer controller on setting change from any s…
lawrence703 Jun 11, 2026
a071eb0
Address review: validate live Ghostty surface before renderer C calls
lawrence703 Jun 11, 2026
604e1ce
Address review: convert planner test to Swift Testing
lawrence703 Jun 11, 2026
e327286
Address review: DocC on new catalog keys + split planner into its own…
lawrence703 Jun 11, 2026
051e21d
Address review: only advance renderer state on a successful native en…
lawrence703 Jun 11, 2026
6b26d0c
docs: bump ghostty-fork pinned head to d39ba5d84 (enqueue-result API)
lawrence703 Jun 11, 2026
4730951
Pin GhosttyKit checksum for ghostty d39ba5d84 (enqueue-result API)
lawrence703 Jun 11, 2026
5fb0756
Address review: ensure re-show realize is enqueued before presenting …
lawrence703 Jun 11, 2026
66572f6
Address review: measure offscreen idle from the hide moment (P2)
lawrence703 Jun 12, 2026
e454320
Address review: don't reset offscreen-idle clock on repeated hidden u…
lawrence703 Jun 12, 2026
aef6409
Merge remote-tracking branch 'origin/main' into feat-surface-leak-repro
lawrence703 Jun 12, 2026
2d4975d
Merge remote-tracking branch 'origin/main' into feat-surface-leak-repro
lawrence703 Jun 12, 2026
5af7b67
Address review: non-blocking renderer enqueue, drop retry loop (P1)
lawrence703 Jun 12, 2026
6a342e6
Pin GhosttyKit checksum for ghostty 5697db81 (.instant enqueue) + for…
lawrence703 Jun 12, 2026
27cd310
Address review: self-heal a dropped re-show realize on the next runloop
lawrence703 Jun 12, 2026
3b3854c
Merge remote-tracking branch 'origin/main' into feat-surface-leak-repro
lawrencecchen Jun 12, 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
11 changes: 6 additions & 5 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
33285 CLI/cmux.swift
19985 Sources/Workspace.swift
19265 Sources/ContentView.swift
18117 Sources/AppDelegate.swift
16539 Sources/GhosttyTerminalView.swift
18118 Sources/AppDelegate.swift
16674 Sources/GhosttyTerminalView.swift
14774 Sources/TerminalController.swift
13606 Sources/Panels/BrowserPanel.swift
12044 cmuxTests/AppDelegateShortcutRoutingTests.swift
Expand Down Expand Up @@ -47,7 +47,7 @@
2198 Sources/SessionPersistence.swift
2123 cmuxTests/ShortcutAndCommandPaletteTests.swift
2117 cmuxTests/CmuxConfigTests.swift
1996 Sources/KeyboardShortcutSettingsFileStore.swift
2030 Sources/KeyboardShortcutSettingsFileStore.swift
1949 Sources/Panels/BrowserWebAuthnSupport.swift
1860 cmuxTests/NotificationAndMenuBarTests.swift
1793 Sources/SessionIndexStore.swift
Expand Down Expand Up @@ -93,9 +93,9 @@
924 Sources/DockPanelView.swift
913 cmuxTests/WorkspaceGroupTests.swift
905 Sources/CmuxSSHURLRequest.swift
896 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
878 Sources/WorkspaceContentView.swift
868 Sources/Panels/BrowserScreenshotSnapshotter.swift
866 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
864 Sources/Panels/TerminalPanel.swift
856 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
852 Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Workspace/ControlCommandCoordinator+Workspace.swift
Expand Down Expand Up @@ -145,8 +145,8 @@
614 cmuxTests/SessionIndexViewTests.swift
613 Sources/PortScanner.swift
611 Sources/TerminalController+ControlPaneContext.swift
603 Sources/SettingsNavigation.swift
599 Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizerPrimaryPolicies.swift
599 Sources/SettingsNavigation.swift
596 cmuxTests/CmuxEventBusTests.swift
594 Sources/SessionIndexModels.swift
594 cmuxTests/PortalTabDragRoutingTests.swift
Expand All @@ -172,6 +172,7 @@
536 cmuxTests/CmuxConfigContextMenuTests.swift
533 Packages/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlCommandCoordinator+Pane.swift
531 Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Scene/SettingsWindowScene.swift
531 Sources/App/WorkspaceRuntimeSettings.swift
530 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttyRuntime.swift
528 cmuxTests/CLINotifyProcessTestSupport.swift
528 cmuxUITests/AutomationSocketUITests.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ public struct TerminalCatalogSection: SettingCatalogSection {
userDefaultsKey: "terminal.agentHibernation.maxLiveTerminals"
)

/// Whether off-screen terminals release their GPU renderer memory while
/// idle (rebuilt instantly on re-show). Non-destructive; on by default.
public let rendererRealizationEnabled = DefaultsKey<Bool>(
id: "terminal.rendererRealization.enabled",
defaultValue: true,
userDefaultsKey: "terminal.rendererRealization.enabled"
)

/// Seconds a terminal must stay off-screen before its renderer memory is
/// reclaimed.
public let rendererRealizationIdleSeconds = DefaultsKey<Double>(
id: "terminal.rendererRealization.idleSeconds",
defaultValue: 30,
userDefaultsKey: "terminal.rendererRealization.idleSeconds"
)

/// Most-recently-visible terminals to keep renderer-ready so switching stays
/// instant. Extra off-screen renderers are reclaimed oldest first.
public let rendererRealizationMaxWarmRenderers = DefaultsKey<Int>(
id: "terminal.rendererRealization.maxWarmRenderers",
defaultValue: 12,
userDefaultsKey: "terminal.rendererRealization.maxWarmRenderers"
)

public let showTextBoxOnNewTerminals = DefaultsKey<Bool>(
id: "terminal.showTextBoxOnNewTerminals",
defaultValue: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ extension Array where Element == CuratedSettingEntry {
.init(section: .terminal, id: "agent-hibernation", title: "Agent Hibernation", synonyms: "terminal.agentHibernation.enabled idle hibernate suspend background agents claude code codex opencode live terminals"),
.init(section: .terminal, id: "agent-hibernation-idle", title: "Hibernate After Idle Seconds", synonyms: "terminal.agentHibernation.idleSeconds idle seconds timeout delay hibernate suspend"),
.init(section: .terminal, id: "agent-hibernation-max", title: "Max Live Agent Terminals", synonyms: "terminal.agentHibernation.maxLiveTerminals max live agent terminals limit count hibernate"),
.init(section: .terminal, id: "renderer-realization", title: "Reclaim Offscreen Terminal Memory", synonyms: "terminal.rendererRealization.enabled renderer reclaim offscreen memory iosurface gpu idle warm release background terminals"),
.init(section: .terminal, id: "renderer-realization-idle", title: "Reclaim After Idle Seconds", synonyms: "terminal.rendererRealization.idleSeconds renderer reclaim idle seconds timeout delay offscreen memory"),
.init(section: .terminal, id: "renderer-realization-max", title: "Max Warm Renderers", synonyms: "terminal.rendererRealization.maxWarmRenderers max warm renderers limit count reclaim offscreen gpu"),
Comment on lines +71 to +73

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 Localize the new settings search titles

These new curated entries feed SettingsSearchIndex.Entry.title, which SettingsSidebarEntryRow renders as the user-facing Settings search result title; adding English literals here means the Japanese Settings search sidebar shows these renderer settings in English even though the actual rows were localized in Localizable.xcstrings. Please use localized strings for the new titles (and add the corresponding catalog entries) so search results match the localized settings UI.

Useful? React with 👍 / 👎.

.init(section: .terminal, id: "resume-commands", title: "Resume Commands", synonyms: "terminal.resumeCommands surface resume command approvals prefixes auto restore prompt manual tmux hibernation"),

// TextBox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public struct TerminalSection: View {
@State private var hibernation: DefaultsValueModel<Bool>
@State private var idleSeconds: DefaultsValueModel<Double>
@State private var maxLive: DefaultsValueModel<Int>
@State private var rendererReclaim: DefaultsValueModel<Bool>
@State private var rendererIdleSeconds: DefaultsValueModel<Double>
@State private var rendererMaxWarm: DefaultsValueModel<Int>

public init(
defaultsStore: UserDefaultsSettingsStore,
Expand All @@ -37,6 +40,9 @@ public struct TerminalSection: View {
_hibernation = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.agentHibernationEnabled))
_idleSeconds = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.agentHibernationIdleSeconds))
_maxLive = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.agentHibernationMaxLiveTerminals))
_rendererReclaim = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.rendererRealizationEnabled))
_rendererIdleSeconds = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.rendererRealizationIdleSeconds))
_rendererMaxWarm = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.terminal.rendererRealizationMaxWarmRenderers))
}

public var body: some View {
Expand Down Expand Up @@ -210,6 +216,49 @@ public struct TerminalSection: View {
)
.accessibilityIdentifier("SettingsTerminalAgentHibernationMaxLiveStepper")
}
SettingsCardDivider()
SettingsCardRow(
configurationReview: .json("terminal.rendererRealization.enabled"),
String(localized: "settings.terminal.rendererRealization", defaultValue: "Reclaim Offscreen Terminal Memory"),
subtitle: rendererReclaim.current
? String(localized: "settings.terminal.rendererRealization.subtitleOn", defaultValue: "Off-screen terminals release their GPU renderer memory while idle and rebuild it instantly when you switch back. The process keeps running.")
: String(localized: "settings.terminal.rendererRealization.subtitleOff", defaultValue: "Every visited terminal keeps its full GPU renderer allocated until you close it, even when off-screen.")
) {
Toggle("", isOn: Binding(get: { rendererReclaim.current }, set: { rendererReclaim.set($0) }))

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 Notify the controller when enabling from Settings

When this toggle is changed in the Settings UI, DefaultsValueModel.set only writes UserDefaults; it does not post RendererRealizationSettings.didChangeNotification, which is the only notification RendererRealizationController.start() observes before creating its timer. In the common case where a user previously disabled terminal.rendererRealization.enabled and then turns it back on here, no timer is started until some other notification path or an app restart, so the setting appears enabled but offscreen renderer reclamation never runs.

Useful? React with 👍 / 👎.

.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsTerminalRendererRealizationToggle")
}
SettingsCardDivider()
SettingsCardRow(
configurationReview: .json("terminal.rendererRealization.idleSeconds"),
String(localized: "settings.terminal.rendererRealization.idleSeconds", defaultValue: "Reclaim After Idle Seconds"),
subtitle: String(localized: "settings.terminal.rendererRealization.idleSeconds.subtitle", defaultValue: "An off-screen terminal must stay off-screen this long before its renderer memory is reclaimed."),
controlWidth: 140
) {
Stepper(
"\(Int(rendererIdleSeconds.current))",
value: Binding(get: { rendererIdleSeconds.current }, set: { rendererIdleSeconds.set($0) }),
in: 5...604_800,
step: 10
)
.accessibilityIdentifier("SettingsTerminalRendererRealizationIdleSecondsStepper")
}
SettingsCardDivider()
SettingsCardRow(
configurationReview: .json("terminal.rendererRealization.maxWarmRenderers"),
String(localized: "settings.terminal.rendererRealization.maxWarmRenderers", defaultValue: "Max Warm Renderers"),
subtitle: String(localized: "settings.terminal.rendererRealization.maxWarmRenderers.subtitle", defaultValue: "The most recently visible terminals keep their renderer ready so switching stays instant. Extra off-screen renderers are reclaimed oldest first."),
controlWidth: 120
) {
Stepper(
"\(rendererMaxWarm.current)",
value: Binding(get: { rendererMaxWarm.current }, set: { rendererMaxWarm.set($0) }),
in: 1...256,
step: 1
)
.accessibilityIdentifier("SettingsTerminalRendererRealizationMaxWarmStepper")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ struct SettingsRowAnchorResolutionTests {
"terminal.agentHibernation.enabled",
"terminal.agentHibernation.idleSeconds",
"terminal.agentHibernation.maxLiveTerminals",
"terminal.rendererRealization.enabled",
"terminal.rendererRealization.idleSeconds",
"terminal.rendererRealization.maxWarmRenderers",
"terminal.autoResumeAgentSessions",
"terminal.copyOnSelect",
"terminal.resumeCommands",
Expand Down
119 changes: 119 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -152112,6 +152112,125 @@
}
}
},
"settings.terminal.rendererRealization": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Reclaim Offscreen Terminal Memory"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "オフスクリーン端末のメモリを回収"
}
}
}
},
"settings.terminal.rendererRealization.subtitleOn": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Off-screen terminals release their GPU renderer memory while idle and rebuild it instantly when you switch back. The process keeps running."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "オフスクリーンの端末はアイドル中にGPUレンダラーのメモリを解放し、切り替え時に即座に再構築します。プロセスは動作し続けます。"
}
}
}
},
"settings.terminal.rendererRealization.subtitleOff": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Every visited terminal keeps its full GPU renderer allocated until you close it, even when off-screen."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "訪問した端末は、オフスクリーンでも閉じるまでGPUレンダラーをすべて確保し続けます。"
}
}
}
},
"settings.terminal.rendererRealization.idleSeconds": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Reclaim After Idle Seconds"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "アイドル秒数経過後に回収"
}
}
}
},
"settings.terminal.rendererRealization.idleSeconds.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "An off-screen terminal must stay off-screen this long before its renderer memory is reclaimed."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "オフスクリーンの端末は、レンダラーのメモリが回収されるまでこの時間オフスクリーンのままである必要があります。"
}
}
}
},
"settings.terminal.rendererRealization.maxWarmRenderers": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Max Warm Renderers"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ウォームレンダラーの最大数"
}
}
}
},
"settings.terminal.rendererRealization.maxWarmRenderers.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The most recently visible terminals keep their renderer ready so switching stays instant. Extra off-screen renderers are reclaimed oldest first."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "最近表示した端末はレンダラーを準備状態に保ち、切り替えを高速に保ちます。余分なオフスクリーンのレンダラーは古いものから回収されます。"
}
}
}
},
"settings.terminal.agentHibernation.subtitleOff": {
"extractionState": "manual",
"localizations": {
Expand Down
Loading
Loading