feat(sidebar): add last activity task sorting (#2762)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# ADR-2026-08-17-separate-task-activity-from-summary-freshness: Separate Task Activity From Summary Freshness
|
||||
|
||||
**Status:** accepted
|
||||
**Date:** 2026-08-17
|
||||
**Area:** backend, frontend, protocol
|
||||
|
||||
## Context
|
||||
|
||||
Task sidebar rows currently use `TaskStatusSummary.updated_at` as their visible
|
||||
time and saved-view sort value. That field records when any projected status
|
||||
changed. A pull-request poll can therefore make many idle tasks look recently
|
||||
active even when no user opened them and no agent ran.
|
||||
|
||||
Users need both concepts. Projection freshness is useful for status diagnostics,
|
||||
while task activity is useful for finding recent work.
|
||||
|
||||
## Decision
|
||||
|
||||
Kandev will keep `TaskStatusSummary.updated_at` as transport and projection
|
||||
freshness. It will add semantic field `last_activity_at` to the same bounded,
|
||||
rebuildable summary.
|
||||
|
||||
`last_activity_at` is the latest durable time from these sources:
|
||||
|
||||
- task creation or a persisted task mutation
|
||||
- a user-authored prompt, including a prompt that enters the queue
|
||||
- an agent turn start or completion
|
||||
|
||||
The timestamp does not advance for task focus, session subscription, Git or
|
||||
pull-request polling, queue bookkeeping, status-summary repair, or session
|
||||
metadata maintenance. Live projection uses source timestamps and applies a
|
||||
monotonic maximum. Rebuilds batch the same durable task, message, and turn
|
||||
sources and preserve a newer stored value.
|
||||
|
||||
The saved-view wire key is `lastActivityAt`. The existing `updatedAt` key keeps
|
||||
its behavior, so stored views remain compatible and users can choose either
|
||||
meaning.
|
||||
|
||||
This decision extends
|
||||
[ADR-2026-08-01-separate-task-summary-session-stream-traffic](2026-08-01-separate-task-summary-session-stream-traffic.md).
|
||||
Task-list consumers continue to use the bounded task summary and do not load
|
||||
background session streams.
|
||||
|
||||
## Consequences
|
||||
|
||||
Idle tasks stay stable when background provider status changes. Desktop and
|
||||
mobile saved views can sort by meaningful work without opening task sessions.
|
||||
|
||||
The backend needs a batched activity query and more bounded projector inputs.
|
||||
Turn milestones prevent per-chunk agent output from producing task-list update
|
||||
traffic. A running turn becomes recent when it starts and advances again when
|
||||
it completes.
|
||||
|
||||
Older summaries need a one-time semantic repair. During partial rollout, the
|
||||
frontend falls back to the task update or creation time when
|
||||
`last_activity_at` is absent.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Reuse summary `updated_at`.** Rejected because provider and Git status
|
||||
changes are not user or agent activity.
|
||||
- **Use only `tasks.updated_at`.** Rejected because prompts and agent turns do
|
||||
not always mutate the task row.
|
||||
- **Derive activity in each browser.** Rejected because background session
|
||||
history is not loaded and task switchers must not subscribe to it.
|
||||
- **Write every activity to the task row.** Rejected because message and turn
|
||||
domains gain extra task writes and mix activity with task edits.
|
||||
@@ -172,4 +172,5 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
|
||||
| 2026-08-12-task-bound-fork-destinations | [Bind Fork Push Destinations to Tasks](2026-08-12-task-bound-fork-destinations.md) | accepted (amended 2026-08-13) | backend, frontend, workflow, security, GitHub | 2026-08-12 |
|
||||
| 2026-08-13-hard-delete-task-contribution-links | [Hard delete owns task contribution links](2026-08-13-hard-delete-task-contribution-links.md) | accepted | backend | 2026-08-13 |
|
||||
| 2026-08-15-office-mode-follows-active-workspace | [Office Mode Follows the Active Workspace](2026-08-15-office-mode-follows-active-workspace.md) | accepted | frontend, backend | 2026-08-15 |
|
||||
| 2026-08-17-separate-task-activity-from-summary-freshness | [Separate Task Activity From Summary Freshness](2026-08-17-separate-task-activity-from-summary-freshness.md) | accepted | backend, frontend, protocol | 2026-08-17 |
|
||||
| 2026-08-17-release-pr-ruleset-bypass | [Give Stable Release PRs an Administrator Token Bypass](2026-08-17-release-pr-ruleset-bypass.md) | accepted | infra, workflow, security | 2026-08-17 |
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
spec: docs/specs/ui/sidebar-last-activity-sort.md
|
||||
created: 2026-08-17
|
||||
status: completed
|
||||
---
|
||||
|
||||
# Implementation Plan: Sidebar Last Activity Sort
|
||||
|
||||
## Overview
|
||||
|
||||
Add a durable task activity timestamp to the bounded task-status summary. Keep
|
||||
summary freshness separate, then expose the activity timestamp through a new
|
||||
saved-view sort on desktop and mobile.
|
||||
|
||||
The work also closes the observed GitHub refresh loop. REST and batched GraphQL
|
||||
status snapshots must converge on one semantic task-PR value, and diagnostics
|
||||
must name fields that cause a real update.
|
||||
|
||||
## Confirmed current behavior
|
||||
|
||||
- `apps/web/components/task/task-session-sidebar-item.ts` maps summary
|
||||
`updated_at` to `TaskSwitcherItem.updatedAt`.
|
||||
- `apps/web/lib/sidebar/apply-view.ts` uses `updatedAt` for the saved-view
|
||||
`updatedAt` comparator.
|
||||
- `apps/web/components/task/task-item-stats-row.tsx` renders the same value as
|
||||
the row's relative time.
|
||||
- `apps/backend/internal/task/statussummary/projector.go` stamps
|
||||
`TaskStatusSummary.UpdatedAt` when any semantic summary field changes.
|
||||
- The projector subscribes to `github.task_pr.updated`, so provider status can
|
||||
refresh the visible time of an idle task.
|
||||
- Sidebar views persist arbitrary sort strings through portable user settings.
|
||||
Frontend `KNOWN_SORT_KEYS` controls which keys survive migration.
|
||||
- Desktop and phone surfaces share `SortPicker`, `applySort`, saved-view state,
|
||||
and user-settings synchronization.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Timestamp ownership
|
||||
|
||||
Extend `TaskStatusSummary` with semantic `LastActivityAt`. Keep `UpdatedAt` as
|
||||
transport and projection freshness.
|
||||
|
||||
Add a narrow `TaskActivityRepository` to the task service. Its batch query
|
||||
returns the maximum of:
|
||||
|
||||
- `tasks.created_at` and `tasks.updated_at`
|
||||
- user-authored `task_session_messages.created_at`
|
||||
- non-reserved user-owned `queued_messages.queued_at`
|
||||
- `task_session_turns.started_at` and non-null `completed_at`
|
||||
|
||||
The query accepts task IDs, returns one map, and uses the existing task/message
|
||||
and turn indexes. Add an index only if query-plan evidence shows a missing
|
||||
leading task key on a production-sized fixture.
|
||||
|
||||
### Live projection and repair
|
||||
|
||||
Add `lastActivityAt` to projector state and persisted semantic JSON. Rehydrate
|
||||
it from the stored summary and a narrow activity loader.
|
||||
|
||||
Subscribe to `task.created`, `task.state_changed`, `turn.started`, and
|
||||
`turn.completed` in addition to the existing task, message, and queue events.
|
||||
Apply timestamps as follows:
|
||||
|
||||
- `task.created`, `task.updated`, and `task.state_changed`: payload
|
||||
`updated_at`, with `created_at` as the creation fallback
|
||||
- `message.added`: `created_at` only for `author_type=user`
|
||||
- successful user-owned queue admission: `queued_at`
|
||||
- `turn.started`: `started_at`
|
||||
- `turn.completed`: `completed_at`
|
||||
|
||||
Use a monotonic maximum. Foreground-activity task events carry the task row's
|
||||
unchanged timestamp and therefore cannot advance task activity.
|
||||
|
||||
Task-list reconciliation batch-loads activity for all requested tasks. It
|
||||
repairs missing fields and preserves a newer stored value during compare-and-set
|
||||
retries. The complete replacement summary continues to use the existing event.
|
||||
|
||||
### GitHub refresh stability
|
||||
|
||||
Normalize GraphQL check-rollup states to the documented `success`, `failure`,
|
||||
`pending`, or empty contract before `SyncTaskPR` compares fields. Exercise the
|
||||
same semantic fixture through REST feedback and batched GraphQL conversion.
|
||||
|
||||
Refactor the `SyncTaskPR` comparison into a helper that returns changed field
|
||||
names. Publish only when that list is non-empty. Log the names at debug level
|
||||
with task and PR identity, without old or new values.
|
||||
|
||||
### Saved-view and row behavior
|
||||
|
||||
Add `lastActivityAt` to `SortKey`, `KNOWN_SORT_KEYS`, `SORT_OPTIONS`, and the
|
||||
comparator table. Thread `lastActivityAt` from `status_summary` into
|
||||
`TaskSwitcherItem`, with task update and creation fallbacks.
|
||||
|
||||
When the effective view uses this key, rows display activity time. Other views
|
||||
retain the existing time selection. Keep stable input order for equal values.
|
||||
|
||||
Add localized **Last activity** copy to English, Portuguese, Simplified
|
||||
Chinese, Hong Kong Chinese, Taiwan Chinese, and pseudo catalogs. Run the
|
||||
Traditional Chinese generator instead of translating both variants by hand.
|
||||
|
||||
## Mobile design contract
|
||||
|
||||
- **Outcome and entry:** desktop opens the Tasks section view editor. Phones
|
||||
open `SessionTaskSwitcherSheet`, then the existing filter gear.
|
||||
- **Exemplar:** keep
|
||||
`apps/web/components/task/mobile/session-task-switcher-sheet.tsx` and the
|
||||
shared `SidebarFilterPopover` composition.
|
||||
- **Hierarchy and presentation:** the sort remains a short temporary choice in
|
||||
the existing selector. No route, drawer, or nested overlay is added.
|
||||
- **Interaction:** the existing selector row is the touch target. Selection
|
||||
updates the shared draft and live task order.
|
||||
- **Scroll and geometry:** the task list remains the only vertical scroll body.
|
||||
The existing drawer and portaled selector remain inside the Pixel 5 viewport
|
||||
with no document horizontal overflow.
|
||||
- **Shared state:** saved-view state, activity mapping, comparator, direction,
|
||||
persistence, and rollback are shared between desktop and mobile.
|
||||
|
||||
## Tests
|
||||
|
||||
Use Red-Green-Refactor for each implementation task. Run each focused test
|
||||
before production edits and record the expected failure. Run the same test
|
||||
after the smallest implementation, then refactor while it remains green.
|
||||
|
||||
### Backend
|
||||
|
||||
- GitHub conversion tests cover every GraphQL rollup state and prove an
|
||||
equivalent REST/GraphQL sequence emits no second task-PR update.
|
||||
- Activity repository tests cover tasks without sessions, queued user prompts,
|
||||
active turns, completed turns, multiple sessions, and batched task IDs.
|
||||
- Summary model tests cover semantic JSON, equality, compatibility with absent
|
||||
activity, and monotonic timestamps.
|
||||
- Projector tests cover every included source, including state-only task
|
||||
transitions. They prove focus-adjacent status, PR, Git, queue, agent-message,
|
||||
and older replay events do not advance activity.
|
||||
- Rebuild tests cover field backfill, no N+1 reads, compare-and-set retry, and
|
||||
preservation of a newer stored timestamp.
|
||||
|
||||
### Frontend
|
||||
|
||||
- Comparator tests cover ascending, descending, equal times, and fallback.
|
||||
- Migration, wire, hydration, and live user-settings tests preserve
|
||||
`lastActivityAt` in saved views and drafts.
|
||||
- Sidebar mapping and rendered-row tests select activity time only for the new
|
||||
sort.
|
||||
- Locale checks cover the new label in every required catalog.
|
||||
|
||||
### E2E
|
||||
|
||||
- Desktop creates a view, selects **Last activity**, verifies order and row
|
||||
time, saves it, reloads, and verifies persistence.
|
||||
- The backend regression proves a provider status refresh does not change the
|
||||
activity timestamp. The desktop flow verifies that resulting sort order.
|
||||
- Mobile selects the same option from the task-switcher drawer and verifies
|
||||
order, persistence, touch reachability, viewport containment, scroll
|
||||
ownership, and zero horizontal overflow.
|
||||
|
||||
## Verification commands
|
||||
|
||||
```bash
|
||||
make -C apps/backend test lint build
|
||||
cd apps && pnpm install --frozen-lockfile
|
||||
cd apps && pnpm --filter @kandev/web exec vitest run lib/sidebar/apply-view.test.ts lib/state/slices/ui/ui-slice-migration.test.ts lib/state/slices/ui/sidebar-view-wire.test.ts components/task/task-session-sidebar-item.test.ts
|
||||
cd apps/web && pnpm run typecheck
|
||||
cd apps && pnpm --filter @kandev/web lint
|
||||
cd apps/web && pnpm run i18n:zh-hant && pnpm run i18n:check && pnpm run i18n:ratchet
|
||||
cd apps/web && pnpm e2e:run tests/task/sidebar-filter.spec.ts -- --grep "last activity"
|
||||
cd apps/web && pnpm e2e:run --no-build --project mobile-chrome tests/task/mobile-sidebar-views.spec.ts -- --grep "last activity"
|
||||
```
|
||||
|
||||
## Implementation waves
|
||||
|
||||
- [Task 01: Stabilize GitHub task-PR refreshes](task-01-stabilize-github-pr-refresh.md), wave 1
|
||||
- [Task 02: Add durable activity reconstruction](task-02-add-activity-reconstruction.md), wave 1
|
||||
- [Task 03: Project task activity live](task-03-project-live-task-activity.md), wave 2, after Task 02
|
||||
- [Task 04: Add the saved-view sort](task-04-add-sidebar-sort.md), wave 3, after Task 03
|
||||
- [Task 05: Prove desktop and mobile behavior](task-05-prove-sidebar-activity-sort.md), wave 4, after Tasks 01 and 04
|
||||
|
||||
This package does not authorize implementation subagents. Implementation stays
|
||||
in the user-controlled primary session.
|
||||
|
||||
## Implementation result
|
||||
|
||||
All five tasks are complete. The implementation includes durable activity
|
||||
reconstruction, live bounded projection, GitHub refresh normalization, the
|
||||
desktop/mobile saved-view sort, localized catalogs, and production-build
|
||||
browser coverage.
|
||||
|
||||
Targeted checks passed during implementation:
|
||||
|
||||
- Backend focused GitHub, activity repository, summary, rebuild, projector, and
|
||||
backend-app tests.
|
||||
- Frontend focused tests, typecheck, lint, i18n completeness, and i18n ratchet.
|
||||
- One desktop Chromium E2E test and one mobile Chromium E2E test, both through
|
||||
the managed production-build runner.
|
||||
|
||||
The full backend test, lint, and build matrix passed as the final pre-PR gate.
|
||||
|
||||
## Risks and boundaries
|
||||
|
||||
- `updated_at` and `last_activity_at` must remain separate in semantic equality,
|
||||
persistence, DTOs, and UI mapping.
|
||||
- Event timestamps can arrive out of order. Every live and rebuild path needs a
|
||||
monotonic maximum.
|
||||
- Counting all agent messages recreates high-frequency task-list traffic.
|
||||
Turn start and completion are the bounded agent milestones.
|
||||
- Existing saved views and default view creation must not change.
|
||||
- The GitHub normalization test must exercise both real conversion paths. A
|
||||
handcrafted `PRStatus` pair does not detect future adapter drift.
|
||||
- Public documentation does not describe sidebar sort choices today. No public
|
||||
documentation update is required.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
id: "01-stabilize-github-pr-refresh"
|
||||
title: "Stabilize GitHub task-PR refreshes"
|
||||
status: completed
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/sidebar-last-activity-sort.md"
|
||||
---
|
||||
|
||||
# Task 01: Stabilize GitHub task-PR refreshes
|
||||
|
||||
Make REST feedback and batched GraphQL polling converge on one task-PR status.
|
||||
Add safe evidence for any future semantic update.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- GraphQL check-rollup values map to the documented task-PR check states.
|
||||
- Equivalent REST and GraphQL snapshots do not persist or publish a second
|
||||
semantic task-PR update.
|
||||
- A real update logs only changed field names with task and PR identity, then
|
||||
publishes the existing event once.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/backend && go test ./internal/github -run 'CheckRollup|Equivalent.*Status|SyncTaskPR.*ChangedFields'
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/github/graphql.go`
|
||||
- `apps/backend/internal/github/graphql_test.go`
|
||||
- `apps/backend/internal/github/service_pr_watch.go`
|
||||
- `apps/backend/internal/github/service_pr_feedback_sync_test.go`
|
||||
- `apps/backend/internal/github/service_pr_watch_batched_budget_test.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- The regression must call both conversion paths before `SyncTaskPR`.
|
||||
- Logs must not include provider field values, comments, titles, or tokens.
|
||||
- Normalization must preserve the existing empty state for absent checks.
|
||||
|
||||
## Result
|
||||
|
||||
- RED: the focused tests failed because the normalization and changed-field
|
||||
helper were not present.
|
||||
- GREEN: `go test ./internal/github -run
|
||||
'CheckRollup|Equivalent.*Status|SyncTaskPR.*ChangedFields'` passed (10 tests).
|
||||
- Regression: `go test ./internal/github` passed (1572 tests).
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
id: "02-add-activity-reconstruction"
|
||||
title: "Add durable activity reconstruction"
|
||||
status: completed
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/sidebar-last-activity-sort.md"
|
||||
---
|
||||
|
||||
# Task 02: Add durable activity reconstruction
|
||||
|
||||
Create the authoritative batched read that reconstructs task activity from
|
||||
task, user-message, queued-prompt, and turn records.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- One repository call returns the latest activity for every requested task ID.
|
||||
- The result includes task creation and mutation, user prompts including
|
||||
non-reserved queued prompts, and turn start or completion. It excludes agent
|
||||
messages, reserved queue entries, and session metadata timestamps.
|
||||
- SQLite and Postgres behavior is covered without an N+1 query.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/backend && go test ./internal/task/repository/sqlite -run 'TaskLastActivity'
|
||||
cd apps/backend && go test ./internal/task/service -run 'StatusSummary.*Activity.*Rebuild'
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/task/repository/interface.go`
|
||||
- `apps/backend/internal/task/repository/sqlite/task_status_summary.go`
|
||||
- `apps/backend/internal/task/repository/sqlite/task_status_summary_test.go`
|
||||
- `apps/backend/internal/task/repository/sqlite/task_status_summary_postgres_test.go`
|
||||
- `apps/backend/internal/task/service/service.go`
|
||||
- `apps/backend/internal/backendapp/services.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- The task-ID filter must apply inside every union branch.
|
||||
- Null completion times must not erase a turn start.
|
||||
- Add an index only after query-plan evidence shows the existing indexes are
|
||||
insufficient.
|
||||
|
||||
## Result
|
||||
|
||||
- RED: the repository test failed before `LoadTaskLastActivity` existed.
|
||||
- GREEN: `go test ./internal/task/repository/sqlite -run 'TaskLastActivity'`
|
||||
passed (1 SQLite test). The Postgres counterpart is environment-gated.
|
||||
- GREEN: `go test ./internal/task/service -run
|
||||
'StatusSummary.*Activity.*Rebuild'` passed (1 rebuild test).
|
||||
- GREEN: queued user prompts are included in the same batched max query while
|
||||
agent-owned queued rows remain excluded; affected repository, projector, and
|
||||
handler packages passed 1,031 tests.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
id: "03-project-live-task-activity"
|
||||
title: "Project task activity live"
|
||||
status: completed
|
||||
wave: 2
|
||||
depends_on: ["02-add-activity-reconstruction"]
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/sidebar-last-activity-sort.md"
|
||||
---
|
||||
|
||||
# Task 03: Project task activity live
|
||||
|
||||
Extend the bounded task summary with a monotonic activity time and repair older
|
||||
summary rows from the new batched source.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Summary JSON, equality, persistence, DTOs, and replacement events carry
|
||||
optional `last_activity_at` without changing `updated_at`.
|
||||
- Task creation, task update, state-only task transition, user-message, user
|
||||
queue admission, and turn events advance activity by source time. Queue
|
||||
bookkeeping, excluded background status, and older replay events do not
|
||||
advance it.
|
||||
- Initial hydration repairs missing values in batch and preserves newer stored
|
||||
activity through compare-and-set retries.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/backend && go test ./internal/task/statussummary -run 'LastActivity'
|
||||
cd apps/backend && go test ./internal/task/service -run 'StatusSummary.*LastActivity'
|
||||
cd apps/backend && go test ./internal/backendapp -run 'StatusSummary'
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/task/statussummary/model.go`
|
||||
- `apps/backend/internal/task/statussummary/model_test.go`
|
||||
- `apps/backend/internal/task/statussummary/projector.go`
|
||||
- `apps/backend/internal/task/statussummary/projector_events.go`
|
||||
- `apps/backend/internal/task/statussummary/projector_derive.go`
|
||||
- `apps/backend/internal/task/statussummary/projector_test.go`
|
||||
- `apps/backend/internal/task/statussummary/projector_restart_rehydration_test.go`
|
||||
- `apps/backend/internal/task/statussummary/rebuild.go`
|
||||
- `apps/backend/internal/task/statussummary/rebuild_test.go`
|
||||
- `apps/backend/internal/task/service/service_status_summary_rebuild.go`
|
||||
- `apps/backend/internal/task/service/service_status_summary_rebuild_test.go`
|
||||
- `apps/backend/internal/backendapp/gateway.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 02.
|
||||
|
||||
## Risks
|
||||
|
||||
- A status-only event must not copy projection time into activity time.
|
||||
- State transitions publish `task.state_changed` without a matching
|
||||
`task.updated` on several service paths, so the projector must consume both.
|
||||
- Message events count only user-authored additions.
|
||||
- Repair and live projection must share the same monotonic rule.
|
||||
|
||||
## Result
|
||||
|
||||
- Added optional semantic `last_activity_at` to live summary replacement
|
||||
payloads and projector state, with durable loader rehydration.
|
||||
- Subscribed to task creation, state-only transitions, user queue admission,
|
||||
and turn start and completion. User-authored message additions and queued
|
||||
admissions use their persisted source time; background status, provider,
|
||||
queue bookkeeping, agent-message, and older replay events do not advance the
|
||||
monotonic maximum. Queue merges retain the newest admission time.
|
||||
- Focused verification passed:
|
||||
- `go test ./internal/task/statussummary -run 'LastActivity'` (3 tests)
|
||||
- `go test ./internal/task/service -run 'StatusSummary.*LastActivity'` (no matching tests)
|
||||
- `go test ./internal/backendapp -run 'StatusSummary'` (7 tests)
|
||||
- `go test ./internal/task/statussummary` (64 tests)
|
||||
- Review fixup verification: affected backend packages passed 1,031 tests,
|
||||
including live queued-prompt projection and SQLite/Postgres reconstruction
|
||||
coverage.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
id: "04-add-sidebar-sort"
|
||||
title: "Add the sidebar activity sort"
|
||||
status: completed
|
||||
wave: 3
|
||||
depends_on: ["03-project-live-task-activity"]
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/sidebar-last-activity-sort.md"
|
||||
---
|
||||
|
||||
# Task 04: Add the sidebar activity sort
|
||||
|
||||
Expose `lastActivityAt` through the shared saved-view picker, comparator, row
|
||||
time, portable settings, and required locales.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Desktop and mobile view editors offer localized **Last activity** with both
|
||||
directions. The option updates the live order and row time.
|
||||
- Saved views and drafts retain `lastActivityAt` through wire conversion,
|
||||
migration, hydration, settings events, sync failure rollback, and reload.
|
||||
- Existing **Updated** views and new-view defaults remain unchanged. Missing
|
||||
activity uses task update, then creation, as fallback.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps && pnpm --filter @kandev/web exec vitest run lib/sidebar/apply-view.test.ts lib/state/slices/ui/ui-slice-migration.test.ts lib/state/slices/ui/sidebar-view-wire.test.ts components/task/task-session-sidebar-item.test.ts
|
||||
cd apps/web && pnpm run typecheck
|
||||
cd apps && pnpm --filter @kandev/web lint
|
||||
cd apps/web && pnpm run i18n:zh-hant && pnpm run i18n:check && pnpm run i18n:ratchet
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/lib/types/task-status-summary.ts`
|
||||
- `apps/web/lib/state/slices/ui/sidebar-view-types.ts`
|
||||
- `apps/web/lib/state/slices/ui/ui-slice.ts`
|
||||
- `apps/web/components/task/sidebar-filter/sort-picker.tsx`
|
||||
- `apps/web/lib/sidebar/apply-view.ts`
|
||||
- `apps/web/lib/sidebar/apply-view.test.ts`
|
||||
- `apps/web/components/task/task-switcher-types.ts`
|
||||
- `apps/web/components/task/task-session-sidebar-item.ts`
|
||||
- `apps/web/components/task/task-session-sidebar-item.test.ts`
|
||||
- `apps/web/components/task/task-item-stats-row.tsx`
|
||||
- `apps/web/lib/state/slices/ui/ui-slice-migration.test.ts`
|
||||
- `apps/web/lib/state/slices/ui/sidebar-view-wire.test.ts`
|
||||
- `apps/web/lib/state/hydration/hydrator.test.ts`
|
||||
- `apps/web/lib/ws/handlers/users.test.ts`
|
||||
- `apps/web/src/locales/*/task.json`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 03.
|
||||
|
||||
## Risks
|
||||
|
||||
- Do not translate or rename the persisted key.
|
||||
- Do not call `t()` at module scope in the option registry.
|
||||
- The effective draft controls both sorting and displayed row time.
|
||||
|
||||
## Result
|
||||
|
||||
- Added the `lastActivityAt` sort key, migration allowlist entry, shared
|
||||
comparator with update/creation fallbacks, and wire round-trip coverage.
|
||||
- Threaded `last_activity_at` from summary payloads into desktop and mobile
|
||||
task rows. Relative time uses activity only when the effective view selects
|
||||
the new sort; existing Updated behavior and defaults remain unchanged.
|
||||
- Added the localized label to English, Portuguese, Simplified Chinese,
|
||||
Traditional Chinese, Hong Kong Chinese, and pseudo catalogs.
|
||||
- Verification passed:
|
||||
- focused Vitest suite: 119 tests
|
||||
- `pnpm run typecheck`
|
||||
- `pnpm --filter @kandev/web lint`
|
||||
- `pnpm run i18n:zh-hant`, `i18n:check`, and `i18n:ratchet`
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
id: "05-prove-sidebar-activity-sort"
|
||||
title: "Prove sidebar activity sorting"
|
||||
status: completed
|
||||
wave: 4
|
||||
depends_on: ["01-stabilize-github-pr-refresh", "04-add-sidebar-sort"]
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/sidebar-last-activity-sort.md"
|
||||
---
|
||||
|
||||
# Task 05: Prove sidebar activity sorting
|
||||
|
||||
Add production-build browser regressions for the saved option, stable activity
|
||||
order, persistence, and phone geometry. Finish the change-aware verification
|
||||
gates.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Desktop proves **Last activity** order and row time survive save and reload.
|
||||
A provider-only refresh does not reorder activity.
|
||||
- Mobile proves the same saved option and order from the task-switcher drawer.
|
||||
- Mobile assertions cover touch interaction, viewport containment, the task-list
|
||||
scroll owner, and zero document horizontal overflow.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps && pnpm install --frozen-lockfile
|
||||
cd apps/web && pnpm e2e:run tests/task/sidebar-filter.spec.ts -- --grep "last activity"
|
||||
cd apps/web && pnpm e2e:run --no-build --project mobile-chrome tests/task/mobile-sidebar-views.spec.ts -- --grep "last activity"
|
||||
make -C apps/backend test lint build
|
||||
cd apps/web && pnpm run typecheck && pnpm run i18n:check && pnpm run i18n:ratchet
|
||||
cd apps && pnpm --filter @kandev/web lint
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/e2e/helpers/api-client.ts`
|
||||
- `apps/web/e2e/pages/sidebar-filter-popover.ts`
|
||||
- `apps/web/e2e/tests/task/sidebar-filter.spec.ts`
|
||||
- `apps/web/e2e/tests/task/mobile-sidebar-views.spec.ts`
|
||||
- `apps/web/components/task/task-item-stats-row.tsx`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Tasks 01 and 04.
|
||||
|
||||
## Risks
|
||||
|
||||
- Seed distinct activity milestones instead of relying on fixed sleeps.
|
||||
- Scope portaled selectors to the active desktop or mobile surface.
|
||||
- Confirm Playwright discovers one desktop test and one mobile test before
|
||||
treating the runs as evidence.
|
||||
|
||||
## Result
|
||||
|
||||
- Desktop discovery found one targeted test, then the managed production E2E
|
||||
run passed. It verified Last activity ordering, row timestamp mapping, saved
|
||||
view persistence after reload, and stable order after two provider-only PR
|
||||
refreshes.
|
||||
- Mobile discovery found one targeted test, then the managed mobile-chrome E2E
|
||||
run passed. It verified touch selection, saved-view persistence, drawer
|
||||
containment, the task-list scroll owner, and zero document horizontal
|
||||
overflow.
|
||||
- The desktop fixture explicitly uses the existing None group so provider
|
||||
association does not intentionally change repository grouping while the test
|
||||
measures activity ordering.
|
||||
@@ -233,6 +233,7 @@ Subscription quota tracking and per-agent cheap-model profile routing.
|
||||
| [external-vcs-file-links](ui/external-vcs-file-links.md) | shipped |
|
||||
| [task-listing-display-preferences](ui/task-listing-display-preferences.md) | shipped |
|
||||
| [sidebar-archived-filter](ui/sidebar-archived-filter.md) | draft |
|
||||
| [sidebar-last-activity-sort](ui/sidebar-last-activity-sort.md) | draft |
|
||||
| [task-workspace-content-search](ui/task-workspace-content-search.md) | shipped |
|
||||
| [file-tree-chat-context](ui/file-tree-chat-context.md) | shipped |
|
||||
| [task-review-shortcut](ui/task-review-shortcut.md) | approved |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
status: approved
|
||||
created: 2026-08-01
|
||||
updated: 2026-08-15
|
||||
updated: 2026-08-17
|
||||
owner: kandev
|
||||
---
|
||||
|
||||
@@ -52,6 +52,7 @@ The initial contract is:
|
||||
| Field | Meaning | Bound |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `revision`, `updated_at` | Monotonic task-local version and projection time | Constant |
|
||||
| `last_activity_at` | Latest durable task, user-prompt, or turn milestone | Constant |
|
||||
| `primary_session` | Primary session ID and lifecycle state | One session |
|
||||
| `foreground_activity`, `active_subagent_count` | Existing task-level busy aggregate | Constant |
|
||||
| `pending_action` | `permission`, `clarification`, or absent | Constant |
|
||||
@@ -115,6 +116,14 @@ remain during migration, but switchers use the summary when present.
|
||||
- A semantic no-op does not increment `revision` or emit an update.
|
||||
- Clients ignore a summary delta whose revision is not newer than the stored
|
||||
revision.
|
||||
- `last_activity_at` is separate from projection freshness. Task creation,
|
||||
persisted task mutations, user-authored prompts, and turn start or completion
|
||||
advance it by source time. Focus, subscriptions, Git or pull-request polling,
|
||||
queue bookkeeping, summary repair, and streamed chunks do not advance it.
|
||||
- Missing and older summaries rebuild `last_activity_at` in one batch from
|
||||
task, user-message, and turn records. Live and rebuilt values use a monotonic
|
||||
maximum, so replay and repair cannot move activity backward. See
|
||||
[ADR-2026-08-17-separate-task-activity-from-summary-freshness](../../decisions/2026-08-17-separate-task-activity-from-summary-freshness.md).
|
||||
|
||||
## API and event surface
|
||||
|
||||
@@ -254,6 +263,9 @@ intermediate replacement.
|
||||
changes its Git tree, or receives a PR update, **WHEN** its summary revision
|
||||
arrives, **THEN** desktop and mobile rows update without a session
|
||||
subscription.
|
||||
- **GIVEN** an idle task receives Git or pull-request summary changes, **WHEN**
|
||||
its replacement summary arrives, **THEN** `updated_at` can advance while
|
||||
`last_activity_at` remains unchanged.
|
||||
- **GIVEN** a recoverable error is dismissed or followed by a newer agent
|
||||
response, **WHEN** the projector processes that occurrence, **THEN** the
|
||||
independent error indicator clears on both task switchers.
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
status: draft
|
||||
created: 2026-08-17
|
||||
updated: 2026-08-17
|
||||
owner: kandev
|
||||
---
|
||||
|
||||
# Sidebar Last Activity Sort
|
||||
|
||||
## Why
|
||||
|
||||
Saved sidebar views can sort tasks by **Updated**. That value currently follows
|
||||
task-status summary freshness. Background pull-request updates can refresh many
|
||||
idle rows together, so this order does not answer which tasks a user or agent
|
||||
worked on most recently.
|
||||
|
||||
Users need a separate **Last activity** option that remains stable during
|
||||
background status refreshes.
|
||||
|
||||
## What
|
||||
|
||||
- Desktop and mobile sidebar view editors offer **Last activity** as a sort
|
||||
option.
|
||||
- The saved-view wire key is `lastActivityAt`. The key persists in saved views
|
||||
and in-flight drafts through user settings, boot hydration, and live settings
|
||||
updates.
|
||||
- Ascending order places the oldest activity first. Descending order places the
|
||||
newest activity first.
|
||||
- Equal timestamps retain the existing input order. Sorting does not introduce
|
||||
row movement for a tie.
|
||||
- The existing `updatedAt` key and **Updated** label remain valid. Existing
|
||||
saved views keep their current behavior.
|
||||
- The built-in view and new-view defaults do not change.
|
||||
- When a view sorts by **Last activity**, each row displays that activity time.
|
||||
Other sort modes retain the existing row time behavior.
|
||||
|
||||
Decision:
|
||||
[ADR-2026-08-17-separate-task-activity-from-summary-freshness](../../decisions/2026-08-17-separate-task-activity-from-summary-freshness.md).
|
||||
|
||||
## Activity definition
|
||||
|
||||
`last_activity_at` is the greatest known timestamp from these durable actions:
|
||||
|
||||
| Source | Activity time |
|
||||
|---|---|
|
||||
| Task creation | `tasks.created_at` |
|
||||
| Persisted task mutation | `tasks.updated_at` |
|
||||
| User-authored prompt, including a queued prompt | message `created_at` |
|
||||
| Agent turn starts | turn `started_at` |
|
||||
| Agent turn completes | turn `completed_at` |
|
||||
|
||||
The following events do not advance activity:
|
||||
|
||||
- opening, focusing, or subscribing to a task or session
|
||||
- Git or pull-request polling and reconciliation
|
||||
- queued-prompt count changes and other queue bookkeeping
|
||||
- status-summary rebuilds or transport delivery
|
||||
- session metadata maintenance without a user prompt or agent turn
|
||||
- streamed agent chunks inside an already-started turn
|
||||
|
||||
A task-state or workflow-placement change counts because it is a persisted task
|
||||
mutation. A foreground-activity publication that carries an unchanged task
|
||||
timestamp does not advance activity.
|
||||
|
||||
## Data model and derivation
|
||||
|
||||
- `TaskStatusSummary` gains optional semantic field
|
||||
`last_activity_at: RFC3339 timestamp`.
|
||||
- `revision` and `updated_at` keep their existing meanings: task-local summary
|
||||
version and projection freshness.
|
||||
- A change to `last_activity_at` is semantic. It advances the summary revision
|
||||
and uses the existing complete-replacement event.
|
||||
- Live projection consumes both `task.updated` and `task.state_changed`, because
|
||||
some persisted state transitions publish only the latter. It takes the
|
||||
monotonic maximum of the stored value and a source event timestamp. An older
|
||||
replay cannot move activity backward.
|
||||
- Initial load and repair use one batched query for all requested task IDs. The
|
||||
query combines task, user-message, and turn timestamps without one query per
|
||||
task.
|
||||
- Repair updates older summaries that lack the field. It preserves a stored
|
||||
activity time that is newer than the rebuilt durable maximum.
|
||||
- A missing summary or an older backend falls back to task `updated_at`, then
|
||||
task `created_at`. The row remains sortable during partial recovery.
|
||||
|
||||
## API and persistence
|
||||
|
||||
The existing task surfaces carry the additive field:
|
||||
|
||||
- boot state and task-list snapshots use `status_summary.last_activity_at`
|
||||
- `task.status_summary.updated` uses the same complete summary payload
|
||||
- sidebar views and drafts persist sort key `lastActivityAt` through the
|
||||
existing user-settings JSON contract
|
||||
|
||||
No new endpoint or WebSocket action is required. The summary remains bounded
|
||||
and contains no transcript, file, or provider payload.
|
||||
|
||||
## Provider refresh stability
|
||||
|
||||
Equivalent GitHub status snapshots from the REST feedback path and the batched
|
||||
GraphQL poller must produce one canonical task-PR state. A semantic no-op does
|
||||
not emit `github.task_pr.updated`.
|
||||
|
||||
GitHub sync diagnostics record the names of changed semantic fields without
|
||||
recording field values or provider content. This evidence makes future
|
||||
cross-path oscillation visible without exposing sensitive data.
|
||||
|
||||
Provider changes can advance summary `updated_at`. They never advance
|
||||
`last_activity_at`.
|
||||
|
||||
## Responsive behavior
|
||||
|
||||
Desktop uses the existing Tasks section view editor. Phones use the existing
|
||||
task-switcher drawer and its shared view editor. Both surfaces use the same
|
||||
sort key, comparator, draft, and saved setting.
|
||||
|
||||
The option stays inside the existing sort selector. It adds no mobile route,
|
||||
drawer, popover, or scroll owner. The current task list remains the only
|
||||
vertical scroll body.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- If activity backfill fails, Kandev keeps the last valid summary and uses the
|
||||
frontend fallback. A later list load or source event retries convergence.
|
||||
- If a summary event is dropped, normal task-list hydration restores the
|
||||
complete value.
|
||||
- If user-settings sync fails, the existing saved-view rollback and error
|
||||
treatment restores the previous view and draft.
|
||||
- An unknown stored sort key keeps the existing migration fallback to Status.
|
||||
`lastActivityAt` is added to the known-key set before it is offered in the
|
||||
editor.
|
||||
|
||||
## Scenarios
|
||||
|
||||
- **GIVEN** several idle tasks receive pull-request status refreshes, **WHEN** a
|
||||
view sorts by **Last activity**, **THEN** their order and displayed activity
|
||||
times do not change.
|
||||
- **GIVEN** a task with an older summary refresh but a newer user prompt,
|
||||
**WHEN** the view sorts descending by **Last activity**, **THEN** that task
|
||||
appears before a task with newer provider status but older activity.
|
||||
- **GIVEN** a user sends a queued prompt, **WHEN** the prompt is stored,
|
||||
**THEN** the task activity advances before the agent begins that turn.
|
||||
- **GIVEN** an agent begins and completes a turn, **WHEN** each milestone is
|
||||
projected, **THEN** activity advances at start and again at completion but
|
||||
not for every streamed chunk.
|
||||
- **GIVEN** an existing saved view sorted by **Updated**, **WHEN** Kandev is
|
||||
upgraded, **THEN** the view keeps that sort and its current order.
|
||||
- **GIVEN** a saved view sorted by **Last activity**, **WHEN** the page reloads,
|
||||
**THEN** the selection, direction, order, and row timestamps persist.
|
||||
- **GIVEN** a phone viewport, **WHEN** the user selects **Last activity** in the
|
||||
task-switcher drawer, **THEN** the same order is applied and the selector
|
||||
remains touch-reachable and viewport-contained.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Replacing or renaming the existing **Updated** sort.
|
||||
- Changing the built-in view or new-view defaults.
|
||||
- Sorting the Kanban board, Office task list, command panel, or quick-chat tabs.
|
||||
- Treating passive focus, provider polling, or raw stream chunks as activity.
|
||||
- Adding a general task activity feed or audit log.
|
||||
|
||||
## Implementation plan
|
||||
|
||||
- [Sidebar Last Activity Sort](../../plans/sidebar-last-activity-sort/plan.md)
|
||||
Reference in New Issue
Block a user