feat(desktop): per-session metadata and status icons in the tray menu
Enrich each session row in the macOS tray menu and refine its layout. - Add a "project · branch" sublabel to every session row, resolved from the session directory: project-root sessions map to their project + live/cached git branch; worktree sessions map back to their parent project and use the worktree's branch. Branch resolution falls back live VCS → git store → worktree metadata, with normalized directory keys. - Replace the inline text status glyph with a native left-aligned status icon (vertically centred across the title + sublabel). Idle rows use a transparent placeholder so every row shares the same gutter and both text lines align. - Status icons use the app icon set: pulse (busy), check (unread), error-warning (error), loop-right (retry); all rendered as tinted template images. - Drop the unread count "(n)" from the row label — the check icon already signals it and the number wasn't self-explanatory. - Show the first 8 sessions inline; the rest stay in the overflow submenu. - Subscribe the tray to the projects, worktree and git stores so subtitles stay current.
This commit is contained in:
+22
-14
@@ -26,22 +26,20 @@ const truncate = (value, max) => {
|
||||
return `${text.slice(0, Math.max(0, max - 1))}…`;
|
||||
};
|
||||
|
||||
// Glyph only for sessions that have an actual state; idle sessions get blank
|
||||
// indentation so the row reads cleanly without an empty circle.
|
||||
const statusGlyph = (session) => {
|
||||
if (session.status === 'busy') return '●';
|
||||
if (session.status === 'retry') return '⟳';
|
||||
if (session.hasError) return '▲';
|
||||
if (session.unseen > 0) return '✓';
|
||||
return '';
|
||||
// Which status icon key a session maps to. 'blank' (a transparent image)
|
||||
// reserves the same left gutter for idle rows so every row aligns.
|
||||
const statusIconKey = (session) => {
|
||||
if (session.status === 'busy') return 'busy';
|
||||
if (session.status === 'retry') return 'retry';
|
||||
if (session.hasError) return 'error';
|
||||
if (session.unseen > 0) return 'unseen';
|
||||
return 'blank';
|
||||
};
|
||||
|
||||
const sessionLabel = (session) => {
|
||||
const glyph = statusGlyph(session);
|
||||
const parts = [glyph || ' ', truncate(session.title || 'Untitled session', 38)];
|
||||
if (session.branch) parts.push(`⎇ ${truncate(session.branch, 18)}`);
|
||||
if (session.unseen > 0) parts.push(`(${session.unseen})`);
|
||||
return parts.join(' ');
|
||||
// The status is a native left icon (the ✓ already signals unread), so the
|
||||
// label is just the session title.
|
||||
return truncate(session.title || 'Untitled session', 40);
|
||||
};
|
||||
|
||||
const approvalLabel = (approval) => {
|
||||
@@ -96,7 +94,7 @@ const toTemplateImage = (p) => {
|
||||
// idleIconPath: plain outline (calm state). unseenIconPath: statically filled
|
||||
// (a finished session left unread). breathIconPaths: eased outline→fill frames
|
||||
// the busy state ping-pongs through.
|
||||
export const createTrayController = ({ idleIconPath, unseenIconPath, breathIconPaths, onAction }) => {
|
||||
export const createTrayController = ({ idleIconPath, unseenIconPath, breathIconPaths, statusIconPaths, onAction }) => {
|
||||
let tray = null;
|
||||
let lastTitle = null;
|
||||
|
||||
@@ -104,6 +102,11 @@ export const createTrayController = ({ idleIconPath, unseenIconPath, breathIconP
|
||||
const idleFrame = toTemplateImage(idleIconPath);
|
||||
const unseenFrame = toTemplateImage(unseenIconPath);
|
||||
const breathFrames = breathIconPaths.map(toTemplateImage);
|
||||
// Per-row status icons (template images, tinted + vertically centred by macOS).
|
||||
const statusIcons = {};
|
||||
for (const [key, p] of Object.entries(statusIconPaths || {})) {
|
||||
statusIcons[key] = toTemplateImage(p);
|
||||
}
|
||||
|
||||
let iconState = null;
|
||||
let animTimer = null;
|
||||
@@ -201,6 +204,11 @@ export const createTrayController = ({ idleIconPath, unseenIconPath, breathIconP
|
||||
|
||||
const sessionItem = (session) => ({
|
||||
label: sessionLabel(session),
|
||||
// Status icon on the left, centred across both lines; idle uses the blank
|
||||
// placeholder so every row keeps the same gutter.
|
||||
icon: statusIcons[statusIconKey(session)] || statusIcons.blank,
|
||||
// Secondary smaller line (macOS): project · branch.
|
||||
...(session.subtitle ? { sublabel: truncate(session.subtitle, 48) } : {}),
|
||||
click: () => onAction({ type: 'focus-session', sessionId: session.id }),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user