perf(ui): swap the session activity spinner for a dot and a turn timer
The spinner ran a CSS animation on every active row for the whole turn, repainting a composited layer at frame rate. Rows now carry a static dot — primary while running, info while unread — and the metadata slot on the right shows how long the turn has been going, updating once per second in the dot's colour. The counter is the motion the spinner used to provide, at 1 fps. Collapsed groups, folders and projects take the dot only, since one counter cannot speak for several running turns. Elapsed time is measured client-side because SessionStatus carries no timestamps, and starts are persisted so a reload resumes the same count. Two rules keep that honest. Only a liveness stamp — refreshed while a session is observed active, stamped as the page hides, and compared against the page's navigation start so a slow bootstrap is not charged to the absence — and a 90s adoption window may expire a record; a snapshot that cannot yet see a session is not evidence its turn ended. And a busy event is never read as a turn boundary, because OpenCode republishes busy at every step of the agent loop, so after a reload one of those repeats normally beats the first status snapshot. Idle and error events do end a turn, and retire the record with it. Snapshot reconciliation walks the running turns and asks whether the snapshot covers each one, rather than being handed everything it covers: only a live start can settle, so the pass scales with timing work instead of with the directory's session list, and allocates nothing per poll. Also applied to the mobile sessions sheet and session switcher. The shared duration ticker moves to hooks/ now that it has a second consumer.
This commit is contained in:
@@ -473,6 +473,8 @@ export const dict = {
|
||||
'sessions.sidebar.session.status.unread': 'Ungelesene Updates',
|
||||
'sessions.sidebar.session.status.pinned': 'Angeheftete Sitzung',
|
||||
'sessions.sidebar.session.status.permissionRequired': 'Berechtigung erforderlich',
|
||||
'sessions.sidebar.session.status.activeFor': 'Seit {duration} aktiv',
|
||||
'sessions.sidebar.session.status.lastTurnDuration': 'Letzter Durchlauf dauerte {duration}',
|
||||
'sessions.sidebar.session.subsessions.collapse': 'Untersitzungen einklappen',
|
||||
'sessions.sidebar.session.subsessions.expand': 'Untersitzungen ausklappen',
|
||||
'sessions.sidebar.dialogs.deleteSession.title': 'Sitzung löschen?',
|
||||
@@ -2728,6 +2730,9 @@ export const dict = {
|
||||
'common.relative.daysAgoCompact': '{count}d her',
|
||||
'common.relative.weeksAgoCompact': '{count}w her',
|
||||
'common.relative.yearsAgoCompact': '{count}y her',
|
||||
'common.duration.secondsCompact': '{seconds}s',
|
||||
'common.duration.minutesSecondsCompact': '{minutes}m {seconds}s',
|
||||
'common.duration.hoursMinutesCompact': '{hours}h {minutes}m',
|
||||
'contextFileOpen.failure.tooLarge': 'Datei ist zu groß zum Öffnen (>{count} Zeilen)',
|
||||
'contextFileOpen.failure.missing': 'Datei nicht gefunden',
|
||||
'contextFileOpen.failure.unreadable': 'Fehler beim Öffnen der Datei',
|
||||
|
||||
@@ -530,6 +530,8 @@ export const dict = {
|
||||
'sessions.sidebar.session.status.pinned': 'Pinned session',
|
||||
'sessions.sidebar.session.status.movingToWorktree': 'Moving session to a new worktree',
|
||||
'sessions.sidebar.session.status.permissionRequired': 'Permission required',
|
||||
'sessions.sidebar.session.status.activeFor': 'Active for {duration}',
|
||||
'sessions.sidebar.session.status.lastTurnDuration': 'Last turn took {duration}',
|
||||
'sessions.sidebar.session.subsessions.collapse': 'Collapse subsessions',
|
||||
'sessions.sidebar.session.subsessions.expand': 'Expand subsessions',
|
||||
'sessions.sidebar.dialogs.deleteSession.title': 'Delete session?',
|
||||
@@ -2900,6 +2902,9 @@ export const dict = {
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'common.duration.secondsCompact': '{seconds}s',
|
||||
'common.duration.minutesSecondsCompact': '{minutes}m {seconds}s',
|
||||
'common.duration.hoursMinutesCompact': '{hours}h {minutes}m',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
|
||||
@@ -531,6 +531,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.session.status.pinned": "Sesión anclada",
|
||||
"sessions.sidebar.session.status.movingToWorktree": "Moviendo la sesión a un worktree nuevo",
|
||||
"sessions.sidebar.session.status.permissionRequired": "Permiso requerido",
|
||||
"sessions.sidebar.session.status.activeFor": "Activa desde hace {duration}",
|
||||
"sessions.sidebar.session.status.lastTurnDuration": "El último turno duró {duration}",
|
||||
"sessions.sidebar.session.subsessions.collapse": "Colapsar subsesiones",
|
||||
"sessions.sidebar.session.subsessions.expand": "Expandir subsesiones",
|
||||
"sessions.sidebar.dialogs.deleteSession.title": "¿Eliminar sesión?",
|
||||
@@ -2901,6 +2903,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.relative.daysAgoCompact": "{count}d ago",
|
||||
"common.relative.weeksAgoCompact": "{count}w ago",
|
||||
"common.relative.yearsAgoCompact": "{count}y ago",
|
||||
"common.duration.secondsCompact": "{seconds}s",
|
||||
"common.duration.minutesSecondsCompact": "{minutes}m {seconds}s",
|
||||
"common.duration.hoursMinutesCompact": "{hours}h {minutes}m",
|
||||
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
|
||||
"contextFileOpen.failure.missing": "File not found",
|
||||
"contextFileOpen.failure.unreadable": "Failed to open file",
|
||||
|
||||
@@ -366,6 +366,8 @@ export const dict = {
|
||||
'sessions.sidebar.session.status.pinned': 'Session épinglée',
|
||||
'sessions.sidebar.session.status.movingToWorktree': 'Déplacement de la session vers un nouveau worktree',
|
||||
'sessions.sidebar.session.status.permissionRequired': 'Autorisation requise',
|
||||
'sessions.sidebar.session.status.activeFor': 'Active depuis {duration}',
|
||||
'sessions.sidebar.session.status.lastTurnDuration': 'Le dernier tour a duré {duration}',
|
||||
'sessions.sidebar.session.subsessions.collapse': 'Réduire les sous-sessions',
|
||||
'sessions.sidebar.session.subsessions.expand': 'Développer les sous-sessions',
|
||||
'sessions.sidebar.dialogs.deleteSession.title': 'Supprimer la session ?',
|
||||
@@ -2648,6 +2650,9 @@ export const dict = {
|
||||
'common.relative.daysAgoCompact': '{count} j',
|
||||
'common.relative.weeksAgoCompact': '{count} sem',
|
||||
'common.relative.yearsAgoCompact': '{count} a',
|
||||
'common.duration.secondsCompact': '{seconds}s',
|
||||
'common.duration.minutesSecondsCompact': '{minutes}m {seconds}s',
|
||||
'common.duration.hoursMinutesCompact': '{hours}h {minutes}m',
|
||||
'contextFileOpen.failure.tooLarge': 'Le fichier est trop volumineux pour être ouvert (> {count} lignes)',
|
||||
'contextFileOpen.failure.missing': 'Fichier introuvable',
|
||||
'contextFileOpen.failure.unreadable': 'Impossible d’ouvrir le fichier',
|
||||
|
||||
@@ -531,6 +531,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.status.pinned': 'ピン留めされたセッション',
|
||||
'sessions.sidebar.session.status.movingToWorktree': 'セッションを新しいworktreeへ移動中',
|
||||
'sessions.sidebar.session.status.permissionRequired': '権限が必要です',
|
||||
'sessions.sidebar.session.status.activeFor': 'アクティブ時間 {duration}',
|
||||
'sessions.sidebar.session.status.lastTurnDuration': '前回のターンの所要時間 {duration}',
|
||||
'sessions.sidebar.session.subsessions.collapse': 'サブセッションを折りたたむ',
|
||||
'sessions.sidebar.session.subsessions.expand': 'サブセッションを展開',
|
||||
'sessions.sidebar.dialogs.deleteSession.title': 'セッションを削除しますか?',
|
||||
@@ -2896,6 +2898,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.relative.daysAgoCompact': '{count}日前',
|
||||
'common.relative.weeksAgoCompact': '{count}週前',
|
||||
'common.relative.yearsAgoCompact': '{count}年前',
|
||||
'common.duration.secondsCompact': '{seconds}秒',
|
||||
'common.duration.minutesSecondsCompact': '{minutes}分{seconds}秒',
|
||||
'common.duration.hoursMinutesCompact': '{hours}時間{minutes}分',
|
||||
'contextFileOpen.failure.tooLarge': 'ファイルが大きすぎて開けません(>{count}行)',
|
||||
'contextFileOpen.failure.missing': 'ファイルが見つかりません',
|
||||
'contextFileOpen.failure.unreadable': 'ファイルを開けませんでした',
|
||||
|
||||
@@ -531,6 +531,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.status.pinned': '고정된 세션',
|
||||
'sessions.sidebar.session.status.movingToWorktree': '세션을 새 worktree로 이동하는 중',
|
||||
'sessions.sidebar.session.status.permissionRequired': '권한 필요',
|
||||
'sessions.sidebar.session.status.activeFor': '{duration} 동안 활성 상태',
|
||||
'sessions.sidebar.session.status.lastTurnDuration': '마지막 턴 소요 시간 {duration}',
|
||||
'sessions.sidebar.session.subsessions.collapse': '하위 세션 접기',
|
||||
'sessions.sidebar.session.subsessions.expand': '하위 세션 펼치기',
|
||||
'sessions.sidebar.dialogs.deleteSession.title': '세션 삭제?',
|
||||
@@ -2900,6 +2902,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'common.duration.secondsCompact': '{seconds}초',
|
||||
'common.duration.minutesSecondsCompact': '{minutes}분 {seconds}초',
|
||||
'common.duration.hoursMinutesCompact': '{hours}시간 {minutes}분',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
|
||||
@@ -531,6 +531,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.status.pinned': 'Przypięta sesja',
|
||||
'sessions.sidebar.session.status.movingToWorktree': 'Przenoszenie sesji do nowego worktree',
|
||||
'sessions.sidebar.session.status.permissionRequired': 'Wymagane uprawnienie',
|
||||
'sessions.sidebar.session.status.activeFor': 'Aktywna od {duration}',
|
||||
'sessions.sidebar.session.status.lastTurnDuration': 'Ostatnia tura trwała {duration}',
|
||||
'sessions.sidebar.session.subsessions.collapse': 'Zwiń pod-sesje',
|
||||
'sessions.sidebar.session.subsessions.expand': 'Rozwiń pod-sesje',
|
||||
'sessions.sidebar.dialogs.deleteSession.title': 'Usunąć sesję?',
|
||||
@@ -2917,6 +2919,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'common.duration.secondsCompact': '{seconds}s',
|
||||
'common.duration.minutesSecondsCompact': '{minutes}m {seconds}s',
|
||||
'common.duration.hoursMinutesCompact': '{hours}h {minutes}m',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
|
||||
@@ -531,6 +531,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.session.status.pinned": "Sessão fixada",
|
||||
"sessions.sidebar.session.status.movingToWorktree": "Movendo a sessão para um novo worktree",
|
||||
"sessions.sidebar.session.status.permissionRequired": "Permissão obrigatória",
|
||||
"sessions.sidebar.session.status.activeFor": "Ativa há {duration}",
|
||||
"sessions.sidebar.session.status.lastTurnDuration": "O último turno levou {duration}",
|
||||
"sessions.sidebar.session.subsessions.collapse": "Recolher subsessões",
|
||||
"sessions.sidebar.session.subsessions.expand": "Expandir subsessões",
|
||||
"sessions.sidebar.dialogs.deleteSession.title": "Excluir sessão?",
|
||||
@@ -2901,6 +2903,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.relative.daysAgoCompact": "{count}d ago",
|
||||
"common.relative.weeksAgoCompact": "{count}w ago",
|
||||
"common.relative.yearsAgoCompact": "{count}y ago",
|
||||
"common.duration.secondsCompact": "{seconds}s",
|
||||
"common.duration.minutesSecondsCompact": "{minutes}m {seconds}s",
|
||||
"common.duration.hoursMinutesCompact": "{hours}h {minutes}m",
|
||||
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
|
||||
"contextFileOpen.failure.missing": "File not found",
|
||||
"contextFileOpen.failure.unreadable": "Failed to open file",
|
||||
|
||||
@@ -531,6 +531,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.session.status.pinned": "Закріплений сесія",
|
||||
"sessions.sidebar.session.status.movingToWorktree": "Перенесення сесії в новий worktree",
|
||||
"sessions.sidebar.session.status.permissionRequired": "Потрібен дозвіл",
|
||||
"sessions.sidebar.session.status.activeFor": "Активна вже {duration}",
|
||||
"sessions.sidebar.session.status.lastTurnDuration": "Останній хід тривав {duration}",
|
||||
"sessions.sidebar.session.subsessions.collapse": "Згорнути підсесії",
|
||||
"sessions.sidebar.session.subsessions.expand": "Розгорнути підсесії",
|
||||
"sessions.sidebar.dialogs.deleteSession.title": "Видалити сесія?",
|
||||
@@ -2901,6 +2903,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"common.relative.daysAgoCompact": "{count}d ago",
|
||||
"common.relative.weeksAgoCompact": "{count}w ago",
|
||||
"common.relative.yearsAgoCompact": "{count}y ago",
|
||||
"common.duration.secondsCompact": "{seconds}с",
|
||||
"common.duration.minutesSecondsCompact": "{minutes}хв {seconds}с",
|
||||
"common.duration.hoursMinutesCompact": "{hours}год {minutes}хв",
|
||||
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
|
||||
"contextFileOpen.failure.missing": "File not found",
|
||||
"contextFileOpen.failure.unreadable": "Failed to open file",
|
||||
|
||||
@@ -531,6 +531,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.status.pinned': '已置顶会话',
|
||||
'sessions.sidebar.session.status.movingToWorktree': '正在将会话移至新工作树',
|
||||
'sessions.sidebar.session.status.permissionRequired': '需要权限',
|
||||
'sessions.sidebar.session.status.activeFor': '已活动 {duration}',
|
||||
'sessions.sidebar.session.status.lastTurnDuration': '上一轮耗时 {duration}',
|
||||
'sessions.sidebar.session.subsessions.collapse': '折叠子会话',
|
||||
'sessions.sidebar.session.subsessions.expand': '展开子会话',
|
||||
'sessions.sidebar.dialogs.deleteSession.title': '删除会话?',
|
||||
@@ -2901,6 +2903,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'common.duration.secondsCompact': '{seconds}秒',
|
||||
'common.duration.minutesSecondsCompact': '{minutes}分{seconds}秒',
|
||||
'common.duration.hoursMinutesCompact': '{hours}小时{minutes}分',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
|
||||
@@ -544,6 +544,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.status.pinned': '已釘選會話',
|
||||
'sessions.sidebar.session.status.movingToWorktree': '正在將會話移至新工作樹',
|
||||
'sessions.sidebar.session.status.permissionRequired': '需要權限',
|
||||
'sessions.sidebar.session.status.activeFor': '已活動 {duration}',
|
||||
'sessions.sidebar.session.status.lastTurnDuration': '上一輪耗時 {duration}',
|
||||
'sessions.sidebar.session.subsessions.collapse': '摺疊子會話',
|
||||
'sessions.sidebar.session.subsessions.expand': '展開子會話',
|
||||
'sessions.sidebar.dialogs.deleteSession.title': '刪除會話?',
|
||||
@@ -2900,6 +2902,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'common.relative.daysAgoCompact': '{count}d ago',
|
||||
'common.relative.weeksAgoCompact': '{count}w ago',
|
||||
'common.relative.yearsAgoCompact': '{count}y ago',
|
||||
'common.duration.secondsCompact': '{seconds}秒',
|
||||
'common.duration.minutesSecondsCompact': '{minutes}分{seconds}秒',
|
||||
'common.duration.hoursMinutesCompact': '{hours}小時{minutes}分',
|
||||
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
|
||||
'contextFileOpen.failure.missing': 'File not found',
|
||||
'contextFileOpen.failure.unreadable': 'Failed to open file',
|
||||
|
||||
Reference in New Issue
Block a user