feat(desktop): macOS menu bar tray with live session state and mini-chat UX
Add an always-visible macOS status bar (tray) item that surfaces OpenChamber's
live state and acts as a quick launcher, plus a series of related desktop UX
fixes around mini-chat, window routing, notifications and shortcuts.
Tray (new):
- Monochrome template cube glyph that adapts to the menu bar light/dark.
- Icon-driven activity indicator: a smooth, eased, infinite "breathing" fill
while sessions are busy; a static filled cube when finished sessions are left
unread; a plain outline when idle. Text counters next to the icon only for
actionable states (pending approvals, errors).
- Menu lists active sessions (status glyph, branch, unread count) with overflow
rolled into a submenu; pending permission/question approvals with inline
Allow once / Allow always / Deny; quick actions (New Session, New Mini Chat,
Show OpenChamber, Quit). Header shows the active instance name
("Local OpenChamber" or the remote host label) for multi-window clarity.
- Session list sourced from the global (cross-project) sessions store, sorted by
last-updated, independent of which directories are currently open; live
status/unread/branch merged in from directory sync stores where available.
Sub-session (multi-run) activity rolls up to the parent row.
- Event-driven updates (global store + directory stores + notifications +
registry) with a short debounce; polling kept only as a slow safety net.
Tray/window routing:
- Opening a session from the tray targets the surface the user was last on: if a
mini-chat is active it switches that existing window to the session in place
(no new window); otherwise the main window (revealed without a reload).
- app.activate (dock click) restores the last-focused/minimized window instead
of spawning a new main window; only creates one when nothing is left.
- "Open in main window" and tray session-open now create the main window when
none exists, queuing the session as a pending deep-link so it opens once the
fresh renderer is ready.
Mini chat:
- New Mini Chat is now a customizable shortcut, exposed in Settings > Shortcuts,
in the File menu (hint only, renderer owns the binding), and in the tray.
- Themed splash backdrop on window open to remove the white flash / flicker;
dismissed once content is ready, leaving the content's single cube logo.
- Mini-chat can switch sessions in place via openchamber:open-session.
Notifications:
- The active/selected session only counts as "seen" when the window is focused,
so turns completing while the app is backgrounded raise an unread marker;
refocusing the window clears it.
This commit is contained in:
@@ -4,9 +4,95 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>OpenChamber Mini Chat</title>
|
||||
|
||||
<script>
|
||||
// Apply theme + splash colors before first paint so the window opens on a
|
||||
// branded splash instead of flashing white while React/CSS load.
|
||||
(function() {
|
||||
try {
|
||||
var themeMode = localStorage.getItem('themeMode');
|
||||
var variant = localStorage.getItem('selectedThemeVariant');
|
||||
var useSystem = localStorage.getItem('useSystemTheme');
|
||||
var isDark;
|
||||
|
||||
if (themeMode === 'dark') {
|
||||
isDark = true;
|
||||
} else if (themeMode === 'light') {
|
||||
isDark = false;
|
||||
} else if (themeMode === 'system' || useSystem === null || useSystem === 'true') {
|
||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
} else if (variant === 'light' || variant === 'dark') {
|
||||
isDark = variant === 'dark';
|
||||
} else {
|
||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
document.documentElement.classList.add(isDark ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-splash-variant', isDark ? 'dark' : 'light');
|
||||
document.documentElement.style.setProperty('color-scheme', isDark ? 'dark' : 'light');
|
||||
|
||||
var splashBgLight = localStorage.getItem('splashBgLight');
|
||||
var splashFgLight = localStorage.getItem('splashFgLight');
|
||||
var splashBgDark = localStorage.getItem('splashBgDark');
|
||||
var splashFgDark = localStorage.getItem('splashFgDark');
|
||||
|
||||
if (splashBgLight) document.documentElement.style.setProperty('--splash-background-light', splashBgLight);
|
||||
if (splashFgLight) document.documentElement.style.setProperty('--splash-stroke-light', splashFgLight);
|
||||
if (splashBgDark) document.documentElement.style.setProperty('--splash-background-dark', splashBgDark);
|
||||
if (splashFgDark) document.documentElement.style.setProperty('--splash-stroke-dark', splashFgDark);
|
||||
} catch (error) {
|
||||
console.warn('Failed to apply theme:', error);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* A plain themed backdrop — no logo. The mini-chat content renders its
|
||||
own (single) OpenChamber cube in its empty state, so a cube here would
|
||||
briefly show a second, differently-sized one before handing off. The
|
||||
backdrop matches bg-background, so fading it out is seamless. */
|
||||
:root {
|
||||
--splash-background-dark: #151313;
|
||||
--splash-background-light: #FFFCF0;
|
||||
--splash-background: var(--splash-background-dark);
|
||||
}
|
||||
html[data-splash-variant='light'] { --splash-background: var(--splash-background-light); }
|
||||
html[data-splash-variant='dark'] { --splash-background: var(--splash-background-dark); }
|
||||
|
||||
#initial-loading {
|
||||
background-color: var(--splash-background);
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
#initial-loading.fade-out {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="module" src="/src/mini-chat-main.tsx"></script>
|
||||
</head>
|
||||
<body class="h-full bg-background text-foreground">
|
||||
<div id="root" class="h-full"></div>
|
||||
<div id="root" class="h-full">
|
||||
<!-- Themed backdrop until the mini-chat React app is ready (dismissed in
|
||||
ElectronMiniChatApp). No logo here on purpose — the content renders
|
||||
the single OpenChamber cube itself. -->
|
||||
<div id="initial-loading"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Safety net: never let the splash hang if the app fails to dismiss it.
|
||||
setTimeout(function() {
|
||||
var loading = document.getElementById('initial-loading');
|
||||
if (loading) {
|
||||
loading.classList.add('fade-out');
|
||||
setTimeout(function() { loading.remove(); }, 300);
|
||||
}
|
||||
}, 8000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user