Skip to content

Commit eb26006

Browse files
heavygeecursoragent
andcommitted
feat(web,hub): show scheduled fire time in session-list clock tooltip
The schedule clock tooltip previously said only "Will fire when due." while the row already showed a relative updated-at pill. Extend the session-list API with nextScheduledAt (MIN future scheduled_at per session, same filter as futureScheduledMessageCount) and render: - single scheduled: "Fires in 5m · Jun 16, 1:45 PM" - multiple: "Next in 5m · Jun 16, 1:45 PM · +2 more" Extract formatScheduledTime from QueuedMessagesBar into web/lib/ scheduledTime.ts alongside formatFutureRelativeTime and the tooltip composer. SSE upsert preserves nextScheduledAt until the list refetch that already runs on schedule-related events. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5b80d59 commit eb26006

18 files changed

Lines changed: 221 additions & 20 deletions

hub/src/store/messageStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getImmediateQueuedLocalMessages,
1616
countFutureScheduledBySessionIds,
1717
countFutureScheduledLocalMessages,
18+
minFutureScheduledAtBySessionIds,
1819
countMessages,
1920
markMessagesInvoked,
2021
mergeSessionMessages,
@@ -83,6 +84,10 @@ export class MessageStore {
8384
return countFutureScheduledBySessionIds(this.db, sessionIds, now)
8485
}
8586

87+
minFutureScheduledAtBySessionIds(sessionIds: string[], now: number = Date.now()): Map<string, number> {
88+
return minFutureScheduledAtBySessionIds(this.db, sessionIds, now)
89+
}
90+
8691
countMessages(sessionId: string): number {
8792
return countMessages(this.db, sessionId)
8893
}

hub/src/store/messages.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,9 @@ describe('countFutureScheduledLocalMessages', () => {
342342
const counts = store.messages.countFutureScheduledBySessionIds([sessionA.id, sessionB.id], now)
343343
expect(counts.get(sessionA.id)).toBe(2)
344344
expect(counts.get(sessionB.id)).toBeUndefined()
345+
346+
const nextAt = store.messages.minFutureScheduledAtBySessionIds([sessionA.id, sessionB.id], now)
347+
expect(nextAt.get(sessionA.id)).toBe(now + 60_000)
348+
expect(nextAt.get(sessionB.id)).toBeUndefined()
345349
})
346350
})

hub/src/store/messages.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,35 @@ export function countFutureScheduledBySessionIds(
361361
return counts
362362
}
363363

364+
/** Earliest future scheduled_at per session (session-list clock tooltip). */
365+
export function minFutureScheduledAtBySessionIds(
366+
db: Database,
367+
sessionIds: string[],
368+
now: number
369+
): Map<string, number> {
370+
const nextAt = new Map<string, number>()
371+
if (sessionIds.length === 0) {
372+
return nextAt
373+
}
374+
375+
const placeholders = sessionIds.map(() => '?').join(',')
376+
const rows = db.prepare(`
377+
SELECT session_id, MIN(scheduled_at) AS next_at
378+
FROM messages
379+
WHERE session_id IN (${placeholders})
380+
AND invoked_at IS NULL
381+
AND local_id IS NOT NULL
382+
AND scheduled_at IS NOT NULL
383+
AND scheduled_at > ?
384+
GROUP BY session_id
385+
`).all(...sessionIds, now) as { session_id: string; next_at: number }[]
386+
387+
for (const row of rows) {
388+
nextAt.set(row.session_id, row.next_at)
389+
}
390+
return nextAt
391+
}
392+
364393
export function getMaxSeq(db: Database, sessionId: string): number {
365394
const row = db.prepare(
366395
'SELECT COALESCE(MAX(seq), 0) AS maxSeq FROM messages WHERE session_id = ?'

hub/src/sync/syncEngine.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ export class SyncEngine {
190190
return this.store.messages.countFutureScheduledBySessionIds(sessionIds, now)
191191
}
192192

193+
getNextScheduledAtBySessionIds(sessionIds: string[], now: number = Date.now()): Map<string, number> {
194+
return this.store.messages.minFutureScheduledAtBySessionIds(sessionIds, now)
195+
}
196+
193197
getSession(sessionId: string): Session | undefined {
194198
return this.sessionCache.getSession(sessionId) ?? this.sessionCache.refreshSession(sessionId) ?? undefined
195199
}

hub/src/web/routes/sessions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,13 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
8181
return b.updatedAt - a.updatedAt
8282
})
8383
const scheduledCounts = engine.getFutureScheduledMessageCounts(sessionRecords.map((session) => session.id))
84+
const nextScheduledAt = engine.getNextScheduledAtBySessionIds(sessionRecords.map((session) => session.id))
8485
const sessions = sessionRecords.map((session) => {
8586
const summary = toSessionSummary(session)
8687
return {
8788
...summary,
88-
futureScheduledMessageCount: scheduledCounts.get(session.id) ?? 0
89+
futureScheduledMessageCount: scheduledCounts.get(session.id) ?? 0,
90+
nextScheduledAt: nextScheduledAt.get(session.id) ?? null
8991
}
9092
})
9193

shared/src/sessionSummary.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export type SessionSummary = {
5656
pendingRequests: PendingRequest[]
5757
backgroundTaskCount: number
5858
futureScheduledMessageCount: number
59+
/** Epoch ms of the soonest uninvoked future scheduled message, or null. */
60+
nextScheduledAt: number | null
5961
model: string | null
6062
effort: string | null
6163
}
@@ -141,6 +143,7 @@ export function toSessionSummary(session: Session): SessionSummary {
141143
pendingRequests: getPendingRequests(session),
142144
backgroundTaskCount: session.backgroundTaskCount ?? 0,
143145
futureScheduledMessageCount: 0,
146+
nextScheduledAt: null,
144147
model: session.model,
145148
effort: session.effort
146149
}

web/src/components/AssistantChat/QueuedMessagesBar.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import type { DecryptedMessage } from '@/types/api'
3-
import { computeCanCancel, computeEditPendingSchedule, formatScheduledTime, getQueuedMessageEditText, getQueuedMessagePreview, sortQueuedMessages } from './QueuedMessagesBar'
3+
import { computeCanCancel, computeEditPendingSchedule, getQueuedMessageEditText, getQueuedMessagePreview, sortQueuedMessages } from './QueuedMessagesBar'
4+
import { formatScheduledTime } from '@/lib/scheduledTime'
45

56
/**
67
* Unit tests for computeCanCancel — the race guard that prevents sending

web/src/components/AssistantChat/QueuedMessagesBar.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useCancelQueuedMessage } from '@/hooks/mutations/useCancelQueuedMessage
1010
import { useTranslation } from '@/lib/use-translation'
1111
import { useToast } from '@/lib/toast-context'
1212
import type { PendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker'
13+
import { formatScheduledTime } from '@/lib/scheduledTime'
1314

1415
function ClockIcon() {
1516
return (
@@ -147,22 +148,6 @@ export function computeCanCancel({
147148
* Edit = client-side cancel + prefill composer with message text (Codex dialect).
148149
* Cancel = DELETE /sessions/:id/messages/:messageId with optimistic removal.
149150
*/
150-
/** @internal Exported for unit testing. */
151-
export function formatScheduledTime(scheduledAt: number): string {
152-
const date = new Date(scheduledAt)
153-
const now = new Date()
154-
const opts: Intl.DateTimeFormatOptions = {
155-
month: 'short',
156-
day: 'numeric',
157-
hour: '2-digit',
158-
minute: '2-digit',
159-
}
160-
if (date.getFullYear() !== now.getFullYear()) {
161-
opts.year = 'numeric'
162-
}
163-
return date.toLocaleString(undefined, opts)
164-
}
165-
166151
export function QueuedMessagesBar({
167152
sessionId,
168153
api,

web/src/components/SessionAttentionIndicator.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function makeSummary(overrides: Partial<SessionSummary> & { id: string }): Sessi
2525
pendingRequests: [],
2626
backgroundTaskCount: 0,
2727
futureScheduledMessageCount: 0,
28+
nextScheduledAt: null,
2829
model: null,
2930
effort: null,
3031
...overrides

web/src/components/SessionList.directory-action.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function makeSession(overrides: Partial<SessionSummary> & { id: string }): Sessi
2121
pendingRequests: [],
2222
backgroundTaskCount: 0,
2323
futureScheduledMessageCount: 0,
24+
nextScheduledAt: null,
2425
model: null,
2526
effort: null,
2627
...overrides

0 commit comments

Comments
 (0)