feat(window): unified native title bar, and a window title that is actually dynamic - #367
Conversation
On the native title bar path the system painted its own strip above the content, so the window read as two pieces: an OS bar, then the terminal. macOS exposes the client properties to fix that - fullWindowContent lets the content pane extend under the title bar and transparentTitleBar stops the system painting there - so the terminal background now runs edge to edge with the traffic lights sitting directly on it. The title stays visible, centred in that strip. Content is inset by the title bar height so the tab bar is not left underneath the traffic lights, where it would be unclickable. That height is a constant rather than a measurement: with fullWindowContent the content pane fills the frame, so the usual "window height minus content height" reports zero. Deliberately NOT transparency, and the KDoc records why so it is not re-litigated. Those are separate things and only this one is available with a native title bar: AWT allocates an alpha-capable backing store only for windows it treats as translucent and refuses that for decorated frames (IllegalComponentStateException: The frame is decorated). Forcing the NSWindow non-opaque underneath is not enough - measured, the window reports isOpaque = NO and alpha still composites onto black, because the surface has no alpha channel. macOS itself allows it, which is how Terminal.app is see-through with traffic lights, but not through an AWT window. Transparency therefore stays what it has always been here: the undecorated path's. Client properties are the supported JDK route on macOS and are ignored elsewhere; gated on macOS anyway so the intent is obvious. Not yet checked by hand: fullscreen, where macOS hides the title bar but the inset is currently unconditional, so there may be reserved space at the top.
The window title was fed from display.windowTitleFlow, the OSC 2 window title. Most shells never emit it, so the window sat on its initial "BossTerm" forever while the tab bar beside it tracked the working directory from an entirely different source - two titles for one pane, one of them dead. It now reads session.title, the value every other surface already uses: a Rename… custom title when set, otherwise the cwd label, with an app's OSC 0/1 icon title mirrored in and re-asserted on each fresh prompt so a full-screen app's name reverts on exit. Focus tracking is unchanged, so it still follows the focused split pane rather than the first one. Consequence worth naming: a shell that does emit OSC 2 with something other than the tab title will now show the tab title instead. That is the point - the two surfaces agreeing is what was missing.
ReviewNice change, and the PR description is unusually good — the transparency section in particular saves the next person from re-running that experiment. I traced the title-source change through Findings on the first commit, roughly by importance. 1. Fullscreen inset — confirmed, and the tempting fix is the wrong one
The one-line fix is close, but note if (fullWindowContent && windowState.placement != WindowPlacement.Fullscreen) {
Spacer(modifier = Modifier.height(NATIVE_TITLE_BAR_HEIGHT_DP.dp))
}Worth actually verifying that 2. Side effect inside
|
Fullscreen inset was the one real bug: the 28dp strip was reserved unconditionally, so macOS fullscreen (where the title bar is gone) left a dead band of background above the tabs. Gated on Fullscreen only, not the existing isFullscreenOrMaximized, because macOS zoom keeps the title bar and gating on that would put the tab bar back under the traffic lights whenever the window is zoomed. Also from the review: - split applyFullWindowContent so the decision and the property writes take a JRootPane, and cover them with tests. No java.awt.Window can be constructed in a headless JVM, so the Window overload is unreachable from CI; a bare JRootPane is not, which makes most of the contract executable. - resolve the root pane via RootPaneContainer rather than a JFrame/JDialog when, which is what the JDK itself uses and covers JWindow too. - log on the runCatching failure branch instead of swallowing it. - NATIVE_TITLE_BAR_HEIGHT is a Dp rather than a bare Int of implied units. - explain in the useNativeTitleBar KDoc and the settings copy WHY there is no transparency, pointing at the measurement, so it is not re-litigated. - document why the apply runs in remember rather than SideEffect: it has to land before the content measures or the window jumps a frame.
Review (second pass — commit
|
Second review pass. The global-hotkey hint is a sibling of the Column that carries the title bar spacer, anchored to the frame top, so with full window content it rendered inside the title bar strip and crowded the centred window title. It now adds the strip height to its own top padding. OSC 2 had become inert app-wide: moving the window title onto session.title left windowTitleFlow with no consumer outside EmbeddableTerminal, so the app and an embedding host derived the title from different sources and a TUI that sets only OSC 2 (vim's t_ts, a bare printf) had no effect anywhere. Merged it into the existing OSC 0/1 collector in wireCwdTitle, which already has the semantics it needs: customTitle wins, and the prompt-reset listener reverts on exit. Tests: the null check now runs before the platform check, since a window with no root pane is a decline everywhere and checking the platform first made that branch unreachable off macOS. The platform is a parameter with a live default, so all four assertions run on every runner instead of each one skipping the half it cannot see.
ReviewNice change, and the PR description does a lot of the reviewer's work for it — the "what this is not" section on AWT transparency is the kind of thing that saves the next person an afternoon, and putting it in the KDoc next to the code rather than only in the PR body is the right call. The A few things below, roughly in severity order. 1. The hotkey hint keeps its 28dp inset in fullscreen — the two inset sites disagree
if (fullWindowContent && windowState.placement != WindowPlacement.Fullscreen) {
Spacer(modifier = Modifier.height(NATIVE_TITLE_BAR_HEIGHT))
}but top = if (fullWindowContent) NATIVE_TITLE_BAR_HEIGHT + 8.dp else 8.dpIn fullscreen the Column no longer reserves the strip, so content starts at y=0 — but the hint is still pushed down 36dp, which now lands it inside the tab bar row instead of above it. Same predicate, two copies, one of them missing a term. Worth hoisting a single value next to val titleBarInset =
if (fullWindowContent && windowState.placement != WindowPlacement.Fullscreen) NATIVE_TITLE_BAR_HEIGHT else 0.dpThen the spacer is 2. Merging OSC 2 into
|
Third review pass. Two of these were my own regressions. The title bar predicate existed in two copies and they had already drifted: the spacer handled fullscreen, the hotkey hint did not, so in fullscreen the hint kept a 36dp offset and landed inside the tab bar row. Hoisted to a single titleBarInset that both sites use. Fullscreen is now measured from the window's bounds instead of trusting WindowState.placement. Compose Desktop syncs placement from AWT's extendedState, and a green-button fullscreen on macOS is not an extendedState transition, so placement can stay Floating right through it - which would have left the dead band the gate was written to prevent. Bounds equal to the whole display is the signal; zoom only fills the visible frame below the menu bar, which is exactly the case that must keep its inset. Both signals are OR-ed, so placement working is a bonus rather than a dependency. Merging OSC 2 into session.title was wrong: session.title is also the tab label, and xterm's split (which this codebase documents in ProperTerminal) is that OSC 1 names the tab and OSC 2 names the window. oh-my-zsh emits a short OSC 1 and a long OSC 2 back to back in precmd, so a merge made the tab label depend on which flow was scheduled last. OSC 1 goes back to being the only tab-label source, and the window title resolves its own precedence at its consumer: custom title, then OSC 2, then the tab title. The prompt-reset listener clears the OSC 2 title alongside the tab title so it still reverts when an app exits; EmbeddableTerminal already ignores empty titles, so its behaviour is unchanged. 8 tests on the two pure functions, all running on every runner.
ReviewNice change — the writeup and the KDoc on A few things I'd want addressed before merge, roughly in severity order. 1. The centred title will be dark-on-dark in macOS Light Mode
Options, roughly in order of effort:
Either way it is worth a line in the KDoc, since it is the same class of "measured, don't rediscover" fact as the alpha one. 2.
|
Fourth review pass. The bounds-vs-screen fullscreen heuristic is gone. It was added on the theory that WindowState.placement might not see a green-button fullscreen, and the review pointed out it cannot tell fullscreen from a zoomed window once the menu bar and Dock auto-hide - which would drop the inset and slide the tab bar under the traffic lights, the exact failure the gate exists to prevent. So placement was traced rather than assumed. Compose syncs it in a componentResized handler, with a comment saying fullscreen changes fire only componentResized and not windowStateChanged; the value it reads reaches skiko's osxIsFullscreenNative, i.e. the real NSWindow state rather than a flag set only when we request fullscreen. It does track the green button, so the heuristic was not just unnecessary but strictly worse - its only effect was false positives. The trace is recorded next to the gate. Clearing the OSC 2 title moved from prompt start to command start. Shells emit their own OSC 2 from precmd, the same hook that emits 133;A, and nothing here controls the order: oh-my-zsh registers its precmd hook before a user's BossTerm snippet, so the clear landed after the title it had just set and wiped it on every prompt. At 133;B the previous program's title is genuinely stale instead. Also: the window title precedence is lifted into resolveWindowTitle and covered by tests, which is the part that changed for every user and had none; the empty string is documented as a reset on windowTitleFlow, since the other consumers only filter it; and the tests that asserted a constant and Rectangle.equals are gone with the heuristic they belonged to. Not fixed, recorded in the KDoc: with a transparent title bar AppKit picks the title colour from the window appearance, so a light system appearance draws it dark over the dark terminal background. No supported per-window appearance property exists; both workarounds give something up.
ReviewTwo independent changes, both well-scoped: the macOS Findings below, roughly by how much they'd cost to hit. 1. Comment contradicts the implementation it describes
The clear happens at command start (133;B), not prompt start — and 2. The hook-ordering premise for choosing B over A looks inverted for BossTerm's own integration
The shipped integration is the other way round. Practical upshot: choosing B is still fine (and it is the safer of the two where the ordering is reversed), but the reasoning as written does not match the repo's own shell integration, and the hazard is symmetric — for a user who registers the snippet with 3. The reset is indistinguishable from the app emitting an empty OSC 2
4. "Reverts on exit" only holds where OSC 133 reachesThe bundled integration returns early inside tmux/screen and for 5. Fullscreen: worth one manual check of the revealed title barDropping the inset to zero in fullscreen is the right call for the steady state. The case I would verify by hand is the transient one: with 6. Smaller things
Tests
The gap is findings 2 and 3: the 133;B reset is the behaviour most likely to surprise someone (it changes what the window says for the duration of every command in some setups) and it has no test. Performance / securityNothing. Good bug to have caught while looking at something else — a window title that never moved is the kind of thing that goes unnoticed for years. Findings 1 and 3 are the two I would fix before merge; 2 and 4 are comment corrections; 5 is a five-second check on the machine. |
Fifth review pass, all four on the comments and the reset path. The rationale for clearing at 133;B claimed oh-my-zsh registers its precmd hook first. It is the other way round for the shipped integration, which is sourced from .zshenv and so registers ahead of anything .zshrc adds. Rewritten to say what is actually true: neither hook is order-safe in general, our own setup makes either choice safe, and B is the one that loses less when a user wires the snippet up after oh-my-zsh. Also noted that the reset needs OSC 133 at all, so inside tmux/screen or with no integration a title still outlives its program, which the previous wording claimed unconditionally. The reset now writes display.windowTitle directly instead of going through terminal.setWindowTitle. That call fans out to every application-title listener before touching the display, so internal bookkeeping was being published as though the program had set an empty title - including to EmbeddableTerminal's public onTitleChange, which survived it only by way of an isNotEmpty() guard. Writing the display keeps it internal and leaves the XTWINOPS title stack alone. One comment in TabbedTerminal still said prompt start; fixed.
Review —
|
Review (2/2)5. Extract the inset predicate — it is the subtlest decision here and it is untested
// NativeTitleBarStyle.kt
fun titleBarInset(styleApplied: Boolean, placement: WindowPlacement): Dp =
if (styleApplied && placement != WindowPlacement.Fullscreen) NATIVE_TITLE_BAR_HEIGHT else 0.dpThree assertions (fullscreen → 0, maximized → 28, not-applied → 0), the rationale moves to KDoc beside the constant it is about, and the composable gets ~20 lines shorter. 6.
|
…cations Sixth review pass. The clear went back to prompt start, which fixes a regression I had introduced two commits ago. Clearing at command start left the OSC 2 title empty for the whole DURATION of every command on shells that set no per-command title, and the completion notification reads that value at command finish - so notifications degraded to the literal "BossTerm" for exactly the users they matter to, since they only fire when the window is unfocused and several tabs would all say the same word. Prompt start does not have that window, and the reason I moved away from it was the hook-ordering premise that the previous pass already corrected: our integration is sourced from .zshenv, so its hooks run first and a shell's own precmd OSC 2 lands just after the clear and survives it. The window is untitled only for the instant in between. The residual reversed-order hazard is symmetric and is now written down as such rather than used to justify one hook over the other. Notifications now prefer the OSC 2 title, then the OSC 1 icon title, before falling back to the app name, so an unfocused tab says which session finished. Structural, from the same review: - titleBarInset is a real function with the Fullscreen-yes / Maximized-no rationale in its KDoc and three tests. It was the subtlest decision in the change and the only one with no coverage, in a predicate that had already drifted between two copies. - the client-property write moved from remember into SideEffect, with the predicate staying pure so the inset is still right on the first frame. A discarded composition no longer decides whether a window got styled. - distinctUntilChanged on the combined title flow: combine re-emits when either side changes and a rename writes two snapshot values, while window.title bottoms out in an AppKit call. - resolveWindowTitle treats a blank custom title as absent rather than letting whitespace win and blank the window.
|
Review Read the whole diff plus the surrounding code in Findings roughly by how much they matter. 1.
val fullWindowContent = remember { useNativeTitleBar && ShellCustomizationUtils.isMacOS() }
SideEffect { if (fullWindowContent) applyFullWindowContent(this@Window.window) } // result ignored
...
val titleBarInset = titleBarInset(fullWindowContent, windowState.placement) // predicate, not resultSo on the decline paths the function exists to protect against — no root pane, or the Cheap fix that keeps the "right on the first frame" property you argued for: var styleApplied by remember { mutableStateOf(fullWindowContent) } // optimistic, correct on frame 1
SideEffect { if (fullWindowContent) styleApplied = applyFullWindowContent(this@Window.window) }
val titleBarInset = titleBarInset(styleApplied, windowState.placement)If you would rather keep it as-is, the KDoc and test comments should say the inset is driven by the predicate rather than by the result — otherwise the next reader trusts a guarantee that is not wired up. 2. The notification fallback can name a program that already exited
tabTitle = { display.windowTitle?.ifEmpty { null } ?: display.iconTitle?.ifEmpty { null } ?: "BossTerm" }Combined with the OSC 2 clear at prompt start: run There is a gap in the other direction too: the bundled integration emits only OSC 7 and OSC 133 (
3.
4. The XTWINOPS claim in the new comment is not quite right
override fun saveWindowTitleOnStack() {
val title = myDisplay.windowTitle // now "" after any prompt startSo an app doing 5. Smaller things
Test coverage The two pure functions are well covered, and injecting
Minor: Not a concern No security issues — the OSC-derived title reaching Nothing here is a blocker except arguably 1 and 2, and both are small edits. |
…rogram Seventh review pass. applyFullWindowContent's return value was documented as the thing that decides whether the caller insets, and the tests said so too, but the call site threw it away and insetted off the predicate instead. So on the decline path the function exists for, the window would still have reserved 28dp for a title bar that was never styled. The result now drives the inset, starting optimistic so the first frame is still right. Notifications fell back to display.iconTitle, which nothing ever resets: quit vim, start a long build in an unfocused window, and the completion notification said "vim". That is the same staleness this PR fixes for OSC 2, one slot down. They now resolve through the tab with the same precedence as the window title, so the two agree on which session finished, and a shell that sets no title at all gets the cwd instead of every tab announcing the app name. Also: osc2 uses ifBlank like custom already did, since a whitespace title wins and leaves the window looking nameless for exactly the same reason; the XTWINOPS note is corrected (the clear does not TRIGGER the title stack, but saveWindowTitleOnStack reads the same field, so it does not hide from it either); the apply moved to LaunchedEffect keyed on the predicate, which says once-per-window rather than once-per-recomposition; and the inset val is renamed so it no longer shadows the function it calls. Four new tests on the notification precedence and the whitespace cases.
ReviewRead the diff against the surrounding code (
And in every one of them Splitting Findings below, roughly by impact. 1. The Light Mode title regression ships default-on, and the escape hatch is closer than the KDoc says
The KDoc frames the fix as expensive:
Both are cheaper than that reads, because the JNA machinery already exists in this same package. [nsWindow setAppearance:[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]]— a handful of lines on top of infrastructure that is already loaded, already null-guarded, and already macOS-gated, not a new dependency or a new native surface. If that's out of scope for this PR, there's a one-line mitigation available right now: flip At minimum, please update the KDoc to point at 2.
|
Eighth review pass. The three copies of the notification-title lambda are now one NotificationTitleProvider. It also fixes a visibility hole the copies had: the tab was written to a captured var on the constructing thread and read on the terminal reader thread, where onCommandFinished dispatches from, with no happens-before edge between them. A plain captured var is a non-volatile field, so the reader was not guaranteed to see the assignment at all. AtomicReference gives that edge for nothing. Consolidating also stops a sibling of the very predicate whose KDoc records that duplicating it is what caused the last drift from existing in triplicate. The failure branch logs to stderr rather than stdout, which is the one channel a terminal emulator should leave alone. AGENTS.md gets the two facts that belong there rather than only in a KDoc: that AWT cannot give a decorated frame an alpha-capable backing store (measured, with the peer-level detail), and the OSC 1 = tab / OSC 2 = window split now that it is load-bearing across three files. NativeTitleBarStyle.kt is listed under Key Files. On the light-mode title (#368): the review suggested building on the ObjC bridge in window/WindowTransparency.kt as though it were ready to use. It is not - its only consumer is commented out there as having "compatibility issues with modern macOS/Java", so the binding is unproven rather than merely unused. The KDoc now points at it AND says that, so whoever picks up #368 starts with both halves.
Review — #367 unified native title bar + dynamic window titleRead through all ten files plus the surrounding call sites ( This is a strong PR. Two things stand out as genuinely well done:
Preserving the OSC 1 / OSC 2 split rather than collapsing it is also the right call, and the oh-my-zsh Findings below, roughly by value. 1. Three comments say "command start" where the code does prompt start — including the one on the public flowThis is the PR's most carefully-argued decision (
2.
|
Ninth review pass. Three comments still said the OSC 2 title is cleared at command start, including the KDoc on windowTitleFlow that a consumer reads to learn what empty means. The code clears at prompt start and argues at length for it, so a reader found both claims and had to work out which was true. Fixed, and the root cause with it: that argument existed in several copies, so AGENTS.md is now the one that carries it and the call site keeps the decision plus a pointer. Also from the review: - resolveWindowTitle and notificationTitle move out of the 2.8k-line composable file into WindowTitle.kt. They are title policy, not part of the composable. - the local NotificationTitleProvider val no longer shadows the imported notificationTitle function, and the AtomicReference import is no longer spliced into the middle of the ai.rever block. - NotificationTitleProvider is internal, with a test pinning what it answers before its tab is attached: the app's OSC 2 title if there is one, otherwise the fallback, which is exactly pre-PR behaviour. - runCatching becomes try/catch (e: Exception), so a CancellationException crossing the LaunchedEffect cannot be swallowed and reported as a style failure. - the TerminalSettings KDoc no longer links an overloaded name Dokka would warn on as ambiguous. Two findings are hand-checks in the running app rather than code, and are on the list for the manual pass: whether the title bar macOS reveals on a top-edge hover in fullscreen swallows clicks on the first tab row, and whether a TUI that rewrites its title continuously makes the window title churn.
Review:
|
Confirmed by hand rather than predicted: with a light system appearance and the default dark terminal background, the window title was drawn near-black on near-black and was unreadable. transparentTitleBar puts the title text over our background, but AppKit still picks that text's colour from the window appearance, so following the system guarantees the mismatch whenever the two disagree. nativeTitleBarAppearance derives apple.awt.application.appearance from the terminal background using Rec. 709 luma, and main() applies it before AWT boots because the property is read once at initialisation - early enough to beat the deep-link handler, which touches java.awt.Desktop. Fixes both directions: a light background on a dark system had the mirror problem. A supported system property rather than JNA. There is no per-window appearance client property, and the ObjC bridge in window/WindowTransparency.kt is not the alternative it appears to be: its only consumer is commented out there as having "compatibility issues with modern macOS/Java". Two deliberate limits. It is app-wide, so the native context menus follow the terminal background rather than the system - for a terminal that is the more consistent answer, but it is a visible change. And it applies only on the native title bar path, since a custom title bar has no system-drawn title to keep legible. Unparseable colours are treated as dark, because guessing light is the direction that produces unreadable text.
ReviewNice piece of work. The The commentary is unusually good; most of what follows is about the seams between the new code and settings that change at runtime. 1. The derived appearance never takes effect for the user who turns the native title bar on — and goes stale on a theme switch (highest-value item)
a) The WindowManager.closeWindow(window.id)
WindowManager.createWindow()Same JVM. The new window composition re-reads b) The property genuinely can only be set pre-AWT, so this is not a "just make it reactive" fix — but the gap is worth closing on the UX side. Cheapest option: keep the value read at startup and compare, e.g. // in the window composition, or wherever settings are observed
val contrastStale = styleApplied &&
isDarkBackground(windowSettings.defaultBackground) != startupAppearanceWasDarkand route that into the same restart affordance the toggle already has, or at minimum a note under the theme/background picker. Failing that, the 2. Both appearance tests pass vacuously on CI
@Test
fun `appearance follows the background, not the system`() {
if (!ShellCustomizationUtils.isMacOS()) return
...
}
Fix is one parameter, matching the neighbour: fun nativeTitleBarAppearance(
useNativeTitleBar: Boolean,
backgroundHex: String,
isMacOS: Boolean = ShellCustomizationUtils.isMacOS(),
): String? { ... }then drop the guard and add an 3.
|
What
On the native title bar path the system painted its own strip above the content, so the window read
as two pieces: an OS bar, then the terminal. macOS exposes client properties to fix that -
apple.awt.fullWindowContentlets the content pane extend under the title bar andapple.awt.transparentTitleBarstops the system painting there - so the terminal background nowruns edge to edge with the traffic lights sitting directly on it, and the title centred in that
strip. It is the look every modern terminal has.
Content is inset by the title bar height so the tab bar is not left underneath the traffic lights,
where it would be unclickable. That height is a constant rather than a measurement: with
fullWindowContentthe content pane fills the frame, so the usual "window height minus contentheight" trick reports zero.
Second commit fixes a bug found while looking at the result: the window title was static. It was
fed from
display.windowTitleFlow, the OSC 2 window title, which most shells never emit - so thewindow sat on its initial
"BossTerm"forever while the tab bar beside it tracked the workingdirectory from an entirely different source. Two titles for one pane, one of them dead. It now reads
session.title, the value every other surface already uses: aRename…custom title when set,otherwise the cwd label, with an app's OSC 0/1 icon title mirrored in and re-asserted on each fresh
prompt so a full-screen app's name reverts on exit. Focus tracking is unchanged, so it still follows
the focused split pane.
What this is not
Transparency. Those are separate things and only this one is available with a native title bar. The
KDoc on
NativeTitleBarStylerecords why, because the existing note inTerminalSettingsreads asa platform limitation and it is not one:
refuses that for decorated frames -
IllegalComponentStateException: The frame is decorated, onsetBackground(alpha<255)andsetOpacity, before and after the window is shown.NSWindownon-opaque underneath is not enough. Measured through the peer:CPlatformWindow.setOpaque(false)runs,peer.isTextured()is false so it does clear thebackground, and the window reports
isOpaque = NO- and alpha still composites onto black,because the surface has no alpha channel.
see-through, in a decorated one it is solid black.
It is AWT's window that cannot be, so transparency stays what it already is here: the undecorated
path's, which is exactly what
useNativeTitleBar = falseselects.Scope
macOS only. The client properties are the supported JDK route there and are ignored elsewhere;
gated on
isMacOS()anyway so the intent is obvious, and every call is best-effort - a failurereturns false and the caller simply does not inset, leaving today's behaviour.
No new setting. This is how the existing native title bar mode looks; the custom title bar path is
untouched and still owns transparency, blur and rounded corners.
Fullscreen
The inset is not unconditional: it is gated on
windowState.placement != WindowPlacement.Fullscreen, because macOS hides the title bar infullscreen and reserving the strip there would leave a dead band of background above the tabs.
Deliberately Fullscreen only, never Maximized. The window already computes
isFullscreenOrMaximizednearby, but that is the wrong predicate here: macOS zoom keeps the titlebar, so gating on it would put the tab bar back under the traffic lights whenever the window is
zoomed.
Window title
The window title used to read
display.windowTitleFlowalone, so it sat on its startup name forevery shell that never emits OSC 2 while the tab bar beside it tracked the directory. It now
resolves a precedence chain (
resolveWindowTitle): a Rename… custom title, then the app's OSC 2window title, then the tab's own title.
The tab label is deliberately left on OSC 1. Folding both OSC titles into
session.titlewas triedand reverted: that field is also the tab label, and xterm's split - which this codebase already
documents in
ProperTerminal- is that OSC 1 names the TAB and OSC 2 names the WINDOW. oh-my-zshemits a short OSC 1 and a long OSC 2 back to back from
precmd, so merging them made the tab labeldepend on which flow happened to be scheduled last.
The OSC 2 title is cleared at prompt start so a program that set one stops naming the window after
it exits. Command start (133;B) was tried and reverted: it leaves the title empty for the whole
duration of every command on shells that set no per-command title, and the completion notification
reads that value at command finish, so notifications degraded to the literal "BossTerm" - for
exactly the users they matter to, since they only fire when the window is unfocused.
Prompt start is safe because the bundled integration is sourced from
.zshenv, so its hooks areregistered ahead of anything
.zshrcadds and run first: a shell that sets its own OSC 2 fromprecmd(oh-my-zsh does) emits it just after the clear, and the window is untitled only for theinstant in between. A user who wires the snippet up from
.zshrcafter oh-my-zsh gets the reverseorder and loses that title to the clear; the hazard is symmetric and no hook is order-safe in
general, which is written down at the call site rather than argued either way.
While in here, completion notifications now prefer the OSC 2 title, then the OSC 1 icon title,
before falling back to the app name - so an unfocused tab says which session finished instead of
every tab announcing "BossTerm".
The clear needs OSC 133, so it does not happen inside tmux/screen or without the shell integration;
there a title outlives its program exactly as it does today.
Verification
./gradlew buildgreen; 1041compose-uitests pass. New coverage is on the two things thatactually changed: the
applyFullWindowContentcontract and the window title precedence. The formergoes through a
JRootPaneoverload with the platform injected, because nojava.awt.Windowcan beconstructed in a headless JVM (the constructor throws
HeadlessException) and CI is headless onevery runner - so both the applied and the declined branch are asserted everywhere rather than each
runner skipping half.
Exercised by hand in the running app: traffic lights native and unchanged, title centred and
following
cd, tab bar clear of the lights.WindowState.placementwas traced rather than assumed, since the whole gate rests on it. Composesyncs it from a
componentResizedhandler specifically because "fullscreen changing doesn't firewindowStateChanged, only componentResized" (
SwingWindow.desktop.kt), and the value it readsbottoms out in skiko's
osxIsFullscreenNative- the real NSWindow state, not a flag set only whenwe request fullscreen. So a green-button fullscreen is covered. A bounds-vs-screen heuristic was
tried in place of it and removed: it cannot tell fullscreen from a zoomed window once the menu bar
and Dock auto-hide.
The title's colour became our problem, so the appearance is now derived rather than followed.
transparentTitleBarputs the title text over BossTerm's own background, but AppKit still picksthat text's colour from the window appearance. Following the system therefore guarantees an
unreadable title whenever the two disagree, and it does: confirmed by hand, a light system
appearance drew a near-black title on the near-black default background.
nativeTitleBarAppearancederivesapple.awt.application.appearancefrom the terminal background(Rec. 709 luma) and
main()applies it before AWT boots, since the property is read once atinitialisation. That fixes both directions - a light background on a dark system had the mirror
problem - through a supported system property rather than JNA. There is no per-window appearance
client property, and the ObjC bridge in
window/WindowTransparency.ktis not the alternative itlooks like: its only consumer is commented out there as having "compatibility issues with modern
macOS/Java".
Two deliberate limits: it is app-wide, so the native context menus follow the terminal background
rather than the system, which for a terminal reads as the more consistent answer; and it applies
only on the native title bar path, since a custom title bar has no system-drawn title to keep
legible.