Skip to content
Merged
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
45 changes: 37 additions & 8 deletions lib/public/css/session-folders.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@
background: rgba(var(--overlay-rgb), 0.05); color: var(--text-dimmer); font-size: 9.5px; font-weight: 600;
text-align: center; font-variant-numeric: tabular-nums;
}
.session-count-summary { position: relative; }
.session-count-summary::after {
content: attr(data-count-detail); position: absolute; right: -4px; bottom: calc(100% + 6px); z-index: 20;
width: max-content; max-width: min(220px, calc(100vw - 24px)); padding: 5px 7px; border: 1px solid var(--border);
border-radius: 7px; background: var(--bg-alt, var(--sidebar-bg)); color: var(--text); box-shadow: 0 4px 14px rgba(var(--shadow-rgb), 0.3);
font-size: 10.5px; font-weight: 500; line-height: 1.25; letter-spacing: 0; white-space: nowrap;
opacity: 0; visibility: hidden; transform: translateY(2px); pointer-events: none;
transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s;
}
.session-folder-toggle:hover .session-count-summary::after,
.session-folder-toggle:focus-visible .session-count-summary::after,
.session-date-group-count:hover::after, .session-date-group-count:focus-visible::after,
.session-search-count:hover::after, .session-search-count:focus-visible::after {
opacity: 1; visibility: visible; transform: translateY(0);
}
.session-date-group-count:focus-visible, .session-search-count:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.session-folder-body[hidden] { display: none; }
.session-folder-body { padding-left: 4px; border-left: 1px solid var(--border-subtle, var(--border)); margin-left: 7px; }
Expand Down Expand Up @@ -84,7 +100,7 @@
.session-search-count {
position: absolute; right: 30px; top: 50%; transform: translateY(-50%); min-width: 16px; padding: 0 5px; height: 15px; line-height: 15px;
border-radius: 999px; background: rgba(var(--overlay-rgb), 0.08); color: var(--text-dimmer); font-size: 9.5px; font-weight: 600; text-align: center;
pointer-events: none;
cursor: help;
}
.session-search-count:empty { display: none; }
.session-search-clear {
Expand Down Expand Up @@ -162,8 +178,9 @@
.session-folder-tool svg { width: 12px; height: 12px; display: block; }
.session-folder-tool:hover { color: var(--text); background: rgba(var(--overlay-rgb), 0.08); }

/* The Favorites star: unfilled and revealed on hover or focus, filled and
always visible once the session is a favorite. */
/* The Favorites star: unfilled and revealed on hover or focus, filled when
selected. Desktop rows reveal it in the shared trailing slot; mobile keeps
the selected star visible as a persistent touch affordance. */
.session-folder-star-btn {
position: absolute; right: 52px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; z-index: 1;
display: flex; align-items: center; justify-content: center; border: 0; border-radius: 999px; background: transparent;
Expand All @@ -172,20 +189,29 @@
.session-folder-star-btn svg, .mobile-session-star svg { width: 12px; height: 12px; display: block; }
.session-item:hover .session-folder-star-btn, .session-item:focus-within .session-folder-star-btn { opacity: 0.82; pointer-events: auto; }
.session-folder-star-btn:hover { opacity: 1; color: var(--text); background: rgba(var(--overlay-rgb), 0.08); }
.session-folder-star-btn.is-favorite, .mobile-session-star.is-favorite { color: var(--accent); opacity: 1; pointer-events: auto; }
.session-folder-star-btn.is-favorite, .mobile-session-star.is-favorite { color: var(--accent); }
.mobile-session-star.is-favorite { opacity: 1; pointer-events: auto; }
.session-folder-star-btn.is-favorite svg, .mobile-session-star.is-favorite svg { fill: currentColor; }
.session-folder-star-btn:focus-visible, .mobile-session-star:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Desktop row actions are a real flex column in the row, not three unrelated
offsets over the age and linked-work metadata. Keeping the full cluster's
footprint at rest also prevents the title from jumping when actions appear. */
/* Age and actions share one intrinsic trailing slot. CSS grid takes the wider
of the two without adding their widths, so hover/focus swaps content without
moving the title, linked work, presence, or row controls. */
.session-item .session-row-trailing {
flex: 0 0 auto; display: grid; align-items: center; justify-items: end; min-width: 0;
}
.session-item .session-row-trailing > .session-item-age,
.session-item .session-row-trailing > .session-row-actions { grid-area: 1 / 1; }
.session-item .session-row-actions {
flex: 0 0 auto; display: flex; align-items: center; justify-content: flex-end; gap: 4px;
}
.session-item .session-row-actions > .session-folder-star-btn,
.session-item .session-row-actions > .session-close-btn {
position: static; flex: 0 0 18px; transform: none;
}
.session-item .session-row-trailing .session-folder-star-btn.is-favorite { opacity: 0; pointer-events: none; }
.session-item:hover .session-row-trailing .session-folder-star-btn.is-favorite,
.session-item:focus-within .session-row-trailing .session-folder-star-btn.is-favorite { opacity: 0.82; pointer-events: auto; }

/* Mobile: touch-sized targets, always-visible actions, no drag affordances. */
.session-folder.is-mobile .session-folder-header { min-height: 40px; margin-top: 8px; }
Expand Down Expand Up @@ -237,7 +263,10 @@
.session-folder-view-note { font-size: 12px; line-height: 1.45; color: var(--text-muted); }

@media (prefers-reduced-motion: reduce) {
.session-folder, .session-folder-chevron, .session-folder-toggle, .session-folder-tool { transition: none; }
.session-folder, .session-folder-chevron, .session-folder-toggle, .session-folder-tool, .session-count-summary::after { transition: none; }
.session-row-trailing .session-item-age,
.session-row-trailing .session-folder-star-btn,
.session-row-trailing .session-close-btn { transition: none; }
.session-folder-body.folder-collapse-animating { will-change: auto; }
}

Expand Down
95 changes: 88 additions & 7 deletions lib/public/css/session-github.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.session-work-column {
display: flex;
flex-direction: column;
gap: 3px;
gap: 2px;
flex: 1 1 auto;
min-width: 0;
overflow: visible;
Expand All @@ -28,10 +28,71 @@
line-height: 18px;
}
.session-work-column .session-row-github-links {
flex-wrap: wrap;
gap: 3px 5px;
overflow: visible;
flex-wrap: nowrap;
gap: 3px;
overflow: hidden;
line-height: 16px;
}
.session-row-github-links .session-github-link {
min-width: 0;
height: 18px;
box-sizing: border-box;
gap: 3px;
padding: 0 4px;
font-size: 10.5px;
}
.session-row-github-links .session-linear-link {
flex: 1 1 0;
min-width: 0;
max-width: 100%;
overflow: hidden;
}
.session-row-github-links:has(.github-kind-pr, .github-kind-issue) .session-linear-link { max-width: calc(100% - 76px); }
.linear-work-state-compact { display: none; }
.session-row-github-links .linear-work-id {
flex: 0 1 auto;
min-width: 0;
max-width: 52%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-row-github-links .linear-work-state {
display: block;
flex: 1 4 0;
min-width: 0;
max-width: 52px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-row-github-links .linear-work-state-full { display: none; }
.session-row-github-links .linear-work-state-compact {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-row-github-links .linear-work-title {
flex: 1 5 0;
min-width: 0;
max-width: 34px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-row-github-links .github-kind-pr,
.session-row-github-links .github-kind-issue { flex: 0 0 auto; }
.session-row-github-links .github-work-brand { display: none; }
.session-row-github-links .github-work-brand svg,
.session-row-github-links .github-work-status svg { width: 11px; height: 11px; }
.session-row-github-links .github-work-more { height: 18px; line-height: 16px; padding: 0 3px; font-size: 10.5px; }
.session-row-github-links .linear-work-brand svg { width: 11px; height: 11px; }
.session-row-github-links .linear-work-brand { flex: none; }
.session-row-github-links .session-linear-link,
.session-row-github-links .session-github-link,
.session-row-github-links .github-work-more { touch-action: manipulation; }
.session-row-github-links { min-height: 18px; }
.session-github-link {
display: inline-flex;
align-items: center;
Expand Down Expand Up @@ -60,10 +121,25 @@
}

.session-item:has(.session-work-column) {
position: relative;
container-type: inline-size;
height: auto;
min-height: 52px;
min-height: 46px;
contain: layout;
}
@container (max-width: 280px) {
.session-row-github-links .linear-work-title { display: none; }
}
.session-item:has(.session-work-column) > .session-vendor-mark,
.session-item:has(.session-work-column) > .session-work-column,
.session-item:has(.session-work-column) > .session-presence,
.session-item:has(.session-work-column) > .session-row-trailing { align-self: flex-start; margin-top: 5px; }
.session-item:has(.session-work-column) .session-row-github-links {
position: absolute;
left: 30px;
right: 44px;
bottom: 3px;
}

.github-kind-pr.github-state-closed .github-work-status svg { color: #f85149; }
@media (max-width: 600px) {
Expand Down Expand Up @@ -117,6 +193,11 @@
.github-work-entry-meta { color: var(--text-muted, #888); font-size: 11px; overflow-wrap: anywhere; }
@media (max-width: 600px) {
#header-github-links { max-width: 120px; }
.mobile-session-item:has(.session-work-column) { align-items: flex-start; }
.mobile-session-item .github-work-more { min-height: 28px; }
.mobile-session-item:has(.session-work-column) { position: relative; container-type: inline-size; align-items: flex-start; min-height: 70px; padding-block: 8px; }
.mobile-session-item:has(.session-work-column) .session-work-column { position: static; }
.mobile-session-item:has(.session-work-column) .session-row-github-links { position: absolute; left: 44px; right: 54px; bottom: 8px; }
.mobile-session-item .session-row-github-links .session-github-link,
.mobile-session-item .session-row-github-links .github-work-more { min-height: 28px; height: 28px; }
.mobile-session-item .session-row-github-links .github-work-more { padding-inline: 2px; }
.mobile-session-item .session-row-github-links { min-height: 28px; }
}
8 changes: 3 additions & 5 deletions lib/public/modules/session-date-group-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getWs } from './ws-ref.js';
import { showConfirm } from './app-misc.js';
import { describeGroupDelete } from './session-delete-scope.js';
import { showToast } from './utils.js';
import { renderSidebarCount } from './sidebar-session-counts.js';

function confirmClear(label, ids, sessions) {
if (!ids.length) return;
Expand All @@ -22,7 +23,7 @@ function confirmClear(label, ids, sessions) {
});
}

export function renderDateGroupHeader(dateLabel, folderLabel, ids, sessions, surface, searching) {
export function renderDateGroupHeader(dateLabel, folderLabel, ids, sessions, surface, searching, displayCounts) {
var header = document.createElement("div");
header.className = "session-group-header session-date-group-header" + (surface === "mobile" ? " is-mobile" : "");
header.setAttribute("role", "heading");
Expand All @@ -33,10 +34,7 @@ export function renderDateGroupHeader(dateLabel, folderLabel, ids, sessions, sur
label.textContent = dateLabel;
header.appendChild(label);

var count = document.createElement("span");
count.className = "session-date-group-count";
count.textContent = String(ids.length);
count.setAttribute("aria-label", ids.length + (ids.length === 1 ? " session" : " sessions"));
var count = renderSidebarCount("session-date-group-count", displayCounts || { roots: ids.length, workers: 0 }, true);
header.appendChild(count);

if ((!store.get('permissions') || store.get('permissions').sessionDelete !== false) && ids.length) {
Expand Down
12 changes: 6 additions & 6 deletions lib/public/modules/session-folder-toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
openListSearch, closeListSearch, clearListSearchText, setListSearchQuery, rememberListSearchCaret,
getListSearchQuery, getListSearchMatchIds, listSearchNeedsRender
} from './session-list-search.js';
import { renderSidebarCount } from './sidebar-session-counts.js';

// Visible one wins: the desktop list and the mobile sheet can both be in the DOM.
function visible(selector) {
Expand Down Expand Up @@ -44,7 +45,7 @@ export function folderToolbarNeedsRender(state, previous) {
return folderDraftNeedsRender(state, previous) || listSearchNeedsRender(state, previous);
}

function renderSearchBar(bar, surface) {
function renderSearchBar(bar, surface, displayCounts) {
var state = store.get('sessionSearch');
var query = getListSearchQuery();
var matches = getListSearchMatchIds();
Expand Down Expand Up @@ -92,9 +93,8 @@ function renderSearchBar(bar, surface) {
}, 0);
});

var count = document.createElement("span");
count.className = "session-search-count";
if (query && matches !== null) count.textContent = String(matches.size);
var count = renderSidebarCount("session-search-count", displayCounts || { roots: matches ? matches.size : 0, workers: 0 }, true);
if (!query || matches === null) count.textContent = "";
var clear = document.createElement("button");
clear.type = "button";
clear.className = "session-search-clear";
Expand Down Expand Up @@ -130,7 +130,7 @@ function renderSearchBar(bar, surface) {
});
}

export function renderFolderToolbar(surface) {
export function renderFolderToolbar(surface, displayCounts) {
var create = store.get('sessionFolderCreate');
var search = store.get('sessionSearch');
var wrap = document.createElement("div");
Expand All @@ -140,7 +140,7 @@ export function renderFolderToolbar(surface) {
wrap.appendChild(bar);

if (search && search.open) {
renderSearchBar(bar, surface);
renderSearchBar(bar, surface, displayCounts);
return wrap;
}

Expand Down
8 changes: 7 additions & 1 deletion lib/public/modules/session-github.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ function safeLinks(session) {
function label(link) {
return (link.kind === 'pr' ? 'PR #' : 'Issue #') + link.number;
}
function compactLinearState(state) {
var value = String(state || '');
if (/^in progress$/i.test(value)) return 'Active';
if (/^cancell?ed$/i.test(value)) return 'Canceled';
return value.length > 9 ? value.slice(0, 8) + '\u2026' : value;
}
function anchor(url, title, content, className) {
return '<a class="' + className + '" href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '">' + content + '</a>';
}
Expand All @@ -43,7 +49,7 @@ export function githubWorkMarkup(session, detailed, expanded, sessionRow) {
return '<span class="session-github-links' + (detailed ? ' github-work-details' : '') + (sessionRow ? ' session-row-github-links' : '') + '">' + displayed.map(function (link) {
if (link.provider === 'linear') {
var description = link.identifier + ' · ' + link.title + ' · ' + link.stateName + (link.stale ? ' · Last known status' : '');
return anchor(link.url, description, LINEAR_MARK + '<span class="linear-work-id">' + escapeHtml(link.identifier) + '</span><span class="linear-work-title">' + escapeHtml(link.title) + '</span><span class="linear-work-state">' + escapeHtml(link.stateName) + '</span>', 'session-github-link session-linear-link');
return anchor(link.url, description, LINEAR_MARK + '<span class="linear-work-id">' + escapeHtml(link.identifier) + '</span><span class="linear-work-title">' + escapeHtml(link.title) + '</span><span class="linear-work-state"><span class="linear-work-state-full">' + escapeHtml(link.stateName) + '</span><span class="linear-work-state-compact" aria-hidden="true">' + escapeHtml(compactLinearState(link.stateName)) + '</span></span>', 'session-github-link session-linear-link');
}
var state = ['open', 'closed', 'merged', 'draft'].indexOf(link.state) >= 0 ? link.state : 'unknown';
var icon = link.kind === 'pr' ? (state === 'merged' ? 'git-merge' : state === 'closed' ? 'git-pull-request-closed' : state === 'draft' ? 'git-pull-request-draft' : 'git-pull-request') : (state === 'closed' ? 'circle-check' : 'circle-dot');
Expand Down
2 changes: 1 addition & 1 deletion lib/public/modules/session-presence-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function renderSessionPresence(row, sessionId) {
container.appendChild(more);
}

var anchor = row.querySelector(".session-item-age, .mobile-worker-generation, .mobile-session-unread, .session-row-actions, .mobile-session-star");
var anchor = row.querySelector(".session-row-trailing, .session-item-age, .mobile-worker-generation, .mobile-session-unread, .session-row-actions, .mobile-session-star");
row.insertBefore(container, anchor || null);
return container;
}
17 changes: 11 additions & 6 deletions lib/public/modules/sidebar-mobile.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import { store } from './store.js';
import { getWs } from './ws-ref.js';
import { dismissOverlayPanels, closeSidebar } from './sidebar.js';
import { openWorkerWithDriver } from './split-view.js';
import { switchProject, getCachedProjects } from './app-projects.js';
import { showHomeHub } from './app-home-hub.js';
import { openHomeChat } from './home-mate-chat.js';
Expand All @@ -45,6 +46,7 @@ import {
renderMobileDriverHierarchy,
renderMobileOrphanHierarchy
} from './sidebar-session-hierarchy.js';
import { summarizeSidebarItems } from './sidebar-session-counts.js';
import { cancelSessionInlineRename, closeSessionContextMenu } from './session-context-menu.js';

// --- Mobile state ---
Expand Down Expand Up @@ -443,15 +445,17 @@ function createMobileSessionItem(s, options) {
openSessionContextMenu(el, s);
});

(function (id) {
(function (session, worker) {
el.addEventListener("click", function () {
if (getWs() && store.get('connected')) {
getWs().send(JSON.stringify({ type: "switch_session", id: id }));
if (!worker || !openWorkerWithDriver(session)) {
getWs().send(JSON.stringify({ type: "switch_session", id: session.id }));
}
}
if (dismissOverlayPanels) dismissOverlayPanels();
closeMobileSheet();
});
})(s.id);
})(s, !!itemOptions.worker);

return el;
}
Expand Down Expand Up @@ -745,15 +749,16 @@ export function renderMobileSessionsInto(container) {
var folderCtx = {
surface: "mobile",
searching: matchIds !== null,
matchIds: matchIds,
sessions: getCachedSessions(),
renderItem: function (it) {
if (it.type === "loop") return createMobileLoopGroup(it.loopId, it.children, it.groupKey);
if (it.type === "driver-hierarchy") return renderMobileDriverHierarchy(it.root, createMobileSessionItem, refreshMobileChatSheet);
if (it.type === "orphan-workers") return renderMobileOrphanHierarchy(it.workers, createMobileSessionItem, refreshMobileChatSheet);
if (it.type === "driver-hierarchy") return renderMobileDriverHierarchy(it.root, createMobileSessionItem, refreshMobileChatSheet, matchIds);
if (it.type === "orphan-workers") return renderMobileOrphanHierarchy(it.workers, createMobileSessionItem, refreshMobileChatSheet, matchIds);
return createMobileSessionItem(it.data);
},
};
container.appendChild(renderFolderToolbar("mobile"));
container.appendChild(renderFolderToolbar("mobile", summarizeSidebarItems(filterItemsBySearch(items, matchIds), matchIds)));
container.appendChild(renderFavoritesSection(layout, folderCtx));
renderOrdinarySections(layout, folderCtx, container);
}
Expand Down
Loading
Loading