diff --git a/apps/desktop/src/components/Sidebar.tsx b/apps/desktop/src/components/Sidebar.tsx
index a13e14064a..d670fca862 100644
--- a/apps/desktop/src/components/Sidebar.tsx
+++ b/apps/desktop/src/components/Sidebar.tsx
@@ -2220,21 +2220,21 @@ export function Sidebar({
{isRemoteSession(session) && {t("remote:remote")}}
{taskTitle(session.title)}
- {lastReplyDate ? (
- <>
-
- |
-
- {lastReplyDate}
- >
- ) : null}
{options?.global ? (
{owningProject}
) : null}
-
+
+ {lastReplyDate ? (
+ {lastReplyDate}
+ ) : null}
setExpanded((value) => !value)}
>
- {expanded ? : }
+ {expanded ? : }
diff --git a/apps/desktop/src/lib/sidebar-session-date.ts b/apps/desktop/src/lib/sidebar-session-date.ts
index 97a2093f45..f8c44747c8 100644
--- a/apps/desktop/src/lib/sidebar-session-date.ts
+++ b/apps/desktop/src/lib/sidebar-session-date.ts
@@ -24,9 +24,13 @@ export function formatSessionLastReplyDate(
}
return new Intl.DateTimeFormat(locale || undefined, {
- dateStyle: "medium",
+ month: "short",
+ day: "numeric",
}).format(date);
} catch {
- return new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }).format(date);
+ return new Intl.DateTimeFormat(undefined, {
+ month: "short",
+ day: "numeric",
+ }).format(date);
}
}
diff --git a/apps/desktop/src/styles/composer.css b/apps/desktop/src/styles/composer.css
index 61d1054708..a3742d2257 100644
--- a/apps/desktop/src/styles/composer.css
+++ b/apps/desktop/src/styles/composer.css
@@ -737,19 +737,19 @@
display: flex;
width: 100%;
min-width: 0;
- min-height: 38px;
+ min-height: 44px;
align-items: center;
gap: 10px;
- padding: 5px 8px 5px 12px;
+ padding: 4px 8px 4px 12px;
color: var(--ds-text-primary);
text-align: left;
}
-.todo-dock-toggle {
- width: 28px;
- min-width: 28px;
- height: 28px;
- flex: 0 0 28px;
+.todo-dock-toggle.btn {
+ width: 36px;
+ min-width: 36px;
+ height: 36px;
+ flex: 0 0 36px;
padding: 0;
color: var(--ds-text-muted);
}
diff --git a/apps/desktop/src/styles/sessions.css b/apps/desktop/src/styles/sessions.css
index 13268eda95..ddadd5aa9a 100644
--- a/apps/desktop/src/styles/sessions.css
+++ b/apps/desktop/src/styles/sessions.css
@@ -406,7 +406,6 @@
color: var(--ds-accent);
}
-.sidebar-row-actions,
.sidebar-menu-wrap {
position: relative;
display: inline-flex;
@@ -414,6 +413,11 @@
flex: 0 0 auto;
}
+.thread-item-meta {
+ position: relative;
+ flex: 0 0 auto;
+}
+
.thread-item-more {
display: inline-flex;
width: 24px;
@@ -432,6 +436,7 @@
}
.thread-item:hover .thread-item-more,
+.thread-item:focus-within .thread-item-more,
.sidebar-session-group-header:hover .thread-item-more,
.sidebar-session-group-header:focus-within .thread-item-more,
.thread-item.active .thread-item-more:hover,
@@ -440,7 +445,6 @@
pointer-events: auto;
}
-.thread-item:focus-within .thread-item-more,
.thread-item-more:focus-visible {
opacity: 1;
pointer-events: auto;
@@ -455,12 +459,17 @@
/* Without hover there is nothing to reveal the row controls with, so a no-hover
* pointer gets them visible and tappable outright. */
@media (hover: none) {
- .sidebar-row-actions .thread-item-more,
+ .thread-item-meta .thread-item-more,
.sidebar-menu-wrap .thread-item-more,
.sidebar-session-group-add {
opacity: 1;
pointer-events: auto;
}
+
+ .thread-item-meta .thread-item-last-reply {
+ opacity: 0;
+ visibility: hidden;
+ }
}
.thread-item-more:hover,
diff --git a/apps/desktop/src/styles/sidebar-threads.css b/apps/desktop/src/styles/sidebar-threads.css
index def1ccacec..da54633eb2 100644
--- a/apps/desktop/src/styles/sidebar-threads.css
+++ b/apps/desktop/src/styles/sidebar-threads.css
@@ -43,23 +43,37 @@
white-space: nowrap;
}
-.thread-item-date-separator,
-.thread-item-last-reply {
+.thread-item-meta {
+ /* The action and date overlap in one slot; opacity keeps its width stable. */
+ display: grid;
+ width: max-content;
flex: 0 0 auto;
+ align-items: center;
+ justify-items: end;
+}
+
+.thread-item-meta > * {
+ grid-area: 1 / 1;
+}
+
+.thread-item-last-reply {
color: var(--ds-text-muted);
font-size: var(--text-sm);
white-space: nowrap;
+ transition:
+ opacity var(--motion-duration-fast) var(--motion-ease-out),
+ visibility var(--motion-duration-fast) var(--motion-ease-out);
}
-.thread-item-date-separator {
- margin-left: auto;
-}
-
-.thread-item:hover .thread-item-date-separator,
.thread-item:hover .thread-item-last-reply,
-.thread-item:focus-within .thread-item-date-separator,
.thread-item:focus-within .thread-item-last-reply {
- display: none;
+ opacity: 0;
+ visibility: hidden;
+}
+
+.thread-item-meta.is-menu-open .thread-item-last-reply {
+ opacity: 0;
+ visibility: hidden;
}
.thread-item:hover,
diff --git a/apps/desktop/test/sidebar-navigation.test.mjs b/apps/desktop/test/sidebar-navigation.test.mjs
index 4c439b3dfb..550429bd45 100644
--- a/apps/desktop/test/sidebar-navigation.test.mjs
+++ b/apps/desktop/test/sidebar-navigation.test.mjs
@@ -368,13 +368,17 @@ test("hidden row actions stay out of the row's click path", () => {
);
assert.match(
globalStyles,
- /\.thread-item:focus-within \.thread-item-more,\s*\n\.thread-item-more:focus-visible\s*\{[^}]*pointer-events:\s*auto;/s,
+ /\.thread-item:hover \.thread-item-more,\s*\n\.thread-item:focus-within \.thread-item-more,[\s\S]*?\{[^}]*pointer-events:\s*auto;/s,
+ );
+ assert.match(
+ globalStyles,
+ /\.thread-item-more:focus-visible\s*\{[^}]*opacity:\s*1;[^}]*pointer-events:\s*auto;/s,
);
// Without hover there is no reveal, so a no-hover pointer gets the controls
// visible and tappable instead of an invisible gutter.
assert.match(
globalStyles,
- /@media \(hover: none\)\s*\{[\s\S]*?\.sidebar-row-actions \.thread-item-more,[\s\S]*?opacity:\s*1;\s*\n\s*pointer-events:\s*auto;/,
+ /@media \(hover: none\)\s*\{[\s\S]*?\.thread-item-meta \.thread-item-more,[\s\S]*?opacity:\s*1;\s*\n\s*pointer-events:\s*auto;/,
);
// The row itself stays clickable where the hidden control used to swallow
// the click, and spelled-out controls never double-fire the row.
diff --git a/apps/desktop/test/sidebar-pinned-rendering.test.mjs b/apps/desktop/test/sidebar-pinned-rendering.test.mjs
index 99642dfffd..60f93ae035 100644
--- a/apps/desktop/test/sidebar-pinned-rendering.test.mjs
+++ b/apps/desktop/test/sidebar-pinned-rendering.test.mjs
@@ -60,6 +60,7 @@ test("sidebar renders global pins once, outside project folding and history limi
activeSessionId: null,
selectingSessionId: null,
page: "chat",
+ settings: { sidebarSessionTime: true },
projectCollapsed: {},
sessionView: { sort: "recent", archived: false },
};
@@ -152,8 +153,7 @@ test("sidebar renders global pins once, outside project folding and history limi
const datedGroup =
unpinned.match(/data-sidebar-project-group="\/dated"[\s\S]*?<\/section>/)?.[0] ?? "";
assert.deepEqual(rows(datedGroup), ["date-row"]);
- assert.match(datedGroup, /class="thread-item-date-separator" aria-hidden="true">\|<\/span>/);
- assert.match(datedGroup, /class="thread-item-last-reply">Jan 1, 2020<\/span>/);
+ assert.match(datedGroup, /class="thread-item-meta(?: is-menu-open)?"[\s\S]*?class="thread-item-last-reply">Jan 1<\/span>/);
// A retained project with no sessions is the empty state, and it has to sit
// in the same list layer so the fold animates it away too.
diff --git a/apps/desktop/test/sidebar-session-date.test.mjs b/apps/desktop/test/sidebar-session-date.test.mjs
index ffa769e8a1..885bcbc485 100644
--- a/apps/desktop/test/sidebar-session-date.test.mjs
+++ b/apps/desktop/test/sidebar-session-date.test.mjs
@@ -27,7 +27,7 @@ test("formats session activity through seven calendar days and uses a date after
);
assert.equal(
formatSessionLastReplyDate(atLocalDay(8), now, "en"),
- "Oct 8, 2026",
+ "Oct 8",
);
});
@@ -38,11 +38,11 @@ test("invalid timestamps do not render a misleading date", () => {
test("project session rows show the date until hover or focus reveals actions", () => {
assert.match(sidebarSource, /sidebarSessionTime === true/);
- assert.match(sidebarSource, /className="thread-item-date-separator"/);
+ assert.match(sidebarSource, /className=\{cx\([\s\S]*?"thread-item-meta"[\s\S]*?sessionMenu === session\.id/);
assert.match(sidebarSource, /className="thread-item-last-reply"/);
assert.match(
sidebarStyles,
- /\.thread-item:hover \.thread-item-last-reply,[\s\S]*?\.thread-item:focus-within \.thread-item-last-reply\s*\{\s*display:\s*none;/,
+ /\.thread-item:hover \.thread-item-last-reply,[\s\S]*?\.thread-item:focus-within \.thread-item-last-reply\s*\{\s*opacity:\s*0;\s*visibility:\s*hidden;/,
);
assert.match(
sidebarStyles,
diff --git a/apps/desktop/test/todo-dock-rendering.test.mjs b/apps/desktop/test/todo-dock-rendering.test.mjs
index 81b7eb8328..ef3ab14b73 100644
--- a/apps/desktop/test/todo-dock-rendering.test.mjs
+++ b/apps/desktop/test/todo-dock-rendering.test.mjs
@@ -65,6 +65,10 @@ test("TodoDock renders bounded session progress and cancelled state", async () =
assert.doesNotMatch(html, /more items/);
assert.match(html, /class="todo-dock-list" role="list" tabindex="-1"/);
assert.match(html, /lucide-chevron-up/);
+ assert.match(
+ html,
+ /