diff --git a/CHANGELOG.md b/CHANGELOG.md index c9724bdbd5..30cda58c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ This file tracks product releases for Roomote (single monorepo version). Automated release entries are prepended by `pnpm run version`. +## 1.19.6 (2026-10-31) + +Roomote 1.19.6 restores reliable all-repository automations across GitHub organizations and makes it easier to clean up sessions without losing your place. + +### Highlights + +- Run all-repository automations when multiple GitHub organizations are connected. +- Select and delete sessions in bulk from either overview, keeping your current view and filters. + +### Patch changes + +- All-repository automations now start reliably when connected GitHub repositories span multiple organizations, instead of failing to create source-control credentials after another GitHub App installation is added. +- Select and delete multiple sessions from the Sessions list or board again, with page-scoped selection and failed deletions retained for retry. Deleting one or several sessions keeps you in the current overview with its filters and page instead of redirecting home. + ## 1.19.5 (2026-10-29) Roomote 1.19.5 improves GitHub work across organizations and restores integration-key access for scheduled and manually run custom automations. diff --git a/apps/docs/fast-sessions.mdx b/apps/docs/fast-sessions.mdx index 247da4a234..0c4d65cbfd 100644 --- a/apps/docs/fast-sessions.mdx +++ b/apps/docs/fast-sessions.mdx @@ -158,6 +158,15 @@ reversible and hides the session and its linked tasks from ordinary lists and search until it is unarchived. Deleting a session uses the existing confirmation and cleanup safeguards. +In the Sessions list or board, choose **Select sessions** to show checkboxes. +Select sessions individually, or choose **All** to select every session you can +manage on the current page. **Delete (n)** confirms and deletes the selection +using the same cleanup safeguards as individual deletion. If some deletions +fail, those sessions stay selected for retry. Exit selection mode to resume +opening sessions normally; changing views, filters, or pages clears selection. +Deleting one or several sessions from either overview keeps you on that view +with its current filters and page. + On desktop, you can keep several task workspaces open beside the session, resize them, and switch between the conversation and task panels without losing your place. On smaller screens, Roomote shows one workspace at a time so the diff --git a/apps/docs/self-hosting/operations.mdx b/apps/docs/self-hosting/operations.mdx index 4de75ee2af..3a704a681c 100644 --- a/apps/docs/self-hosting/operations.mdx +++ b/apps/docs/self-hosting/operations.mdx @@ -95,6 +95,11 @@ one. Check the running application version and applied schema migration at any time under **Settings → Deployment → Diagnostics**; both are also recorded in every backup bundle's manifest. +When upgrading from Roomote 1.19.5 or earlier to 1.19.6 or later, upgrade all +servers handling sign-in and account linking together. Restart any sign-in or +account-linking attempts begun before the upgrade. Existing signed-in sessions +are retained, and registered OAuth callback URLs do not need to change. + Roomote surfaces release history and available updates in the web app: - **Admins** on self-hosted deployments see an update notice in the sidenav when diff --git a/apps/web/package.json b/apps/web/package.json index 42a3a6cf66..d46b264dfe 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,7 +28,7 @@ "dependencies": { "@aws-sdk/client-s3": "3.1000.0", "@aws-sdk/s3-request-presigner": "3.1000.0", - "@better-auth/drizzle-adapter": "^1.6.22", + "@better-auth/drizzle-adapter": "^1.7.12", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/merge": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -92,7 +92,7 @@ "@zxcvbn-ts/core": "^4.1.2", "ai": "^6.0.116", "ansi-to-react": "^6.2.6", - "better-auth": "^1.6.22", + "better-auth": "^1.7.12", "better-sse": "^0.16.1", "change-case": "^5.4.4", "class-variance-authority": "^0.7.1", diff --git a/apps/web/src/app/(authenticated)/sessions/SessionBoard.tsx b/apps/web/src/app/(authenticated)/sessions/SessionBoard.tsx index ee42e25c4b..89711bb09a 100644 --- a/apps/web/src/app/(authenticated)/sessions/SessionBoard.tsx +++ b/apps/web/src/app/(authenticated)/sessions/SessionBoard.tsx @@ -41,6 +41,7 @@ import { import { BasicTooltip, Button, Funnel } from '@/components/system'; import { useSessionStatusMutation } from '@/components/sessions/use-session-status-mutation'; +import { useSessionSelectionActive } from '@/components/sessions/SessionSelection'; import { canDropSessionBoardCard, getSessionBoardDropStatus, @@ -454,6 +455,8 @@ export function SessionBoardCard({ }) { const reducedMotion = useReducedMotion() ?? false; const registry = useContext(SessionBoardCardRegistryContext); + const selecting = useSessionSelectionActive(); + const canDrag = canManage && !selecting; const dnd = useContext(SessionBoardDndContext); const [isFlying, setIsFlying] = useState(false); const dragData = useRef({ @@ -470,7 +473,7 @@ export function SessionBoardCard({ const draggable = useDraggable({ id: sessionId, data: dragData.current, - disabled: !canManage || dnd?.statusMutationPending === true, + disabled: !canDrag || dnd?.statusMutationPending === true, }); useLayoutEffect(() => { @@ -529,14 +532,14 @@ export function SessionBoardCard({
{children}
diff --git a/apps/web/src/app/(authenticated)/sessions/SessionCard.tsx b/apps/web/src/app/(authenticated)/sessions/SessionCard.tsx index 86fc568a15..56be723041 100644 --- a/apps/web/src/app/(authenticated)/sessions/SessionCard.tsx +++ b/apps/web/src/app/(authenticated)/sessions/SessionCard.tsx @@ -1,4 +1,3 @@ -import Link from 'next/link'; import { format, formatDistanceToNow, isSameDay, isSameYear } from 'date-fns'; import type { SessionManualStatus } from '@roomote/types'; @@ -21,6 +20,11 @@ import { SessionInferenceCostBreakdown } from '@/components/sessions/SessionInfe import { PrivateSessionIcon } from '@/components/sessions/PrivateSessionIcon'; import { getSessionSurfaceLabel } from '@/components/sessions/session-surfaces'; import { TaskAutomationIcon } from '@/components/tasks/TaskAutomationIcon'; +import { + SessionSelectionControl, + SessionSelectionLink, + SessionSelectionRow, +} from '@/components/sessions/SessionSelection'; import { SessionActions } from '../../(sandbox)/sessions/[sessionId]/SessionDeleteAction'; type SessionCardData = { @@ -108,41 +112,53 @@ export function SessionCard({ const accessibleActivityTimestamp = activityDate.toLocaleString(); return ( -
- + {session.title} - +
- {session.ownerKind === 'automation' ? ( - - ) : ( - - )} - {session.unread && session.ownerUserId === viewerUserId ? ( - - ) : null} + )} + {session.unread && session.ownerUserId === viewerUserId ? ( + + ) : null} +
@@ -230,6 +246,6 @@ export function SessionCard({ />
) : null} -
+ ); } diff --git a/apps/web/src/app/(authenticated)/sessions/page.client.test.tsx b/apps/web/src/app/(authenticated)/sessions/page.client.test.tsx index fde8fdb283..b774137df3 100644 --- a/apps/web/src/app/(authenticated)/sessions/page.client.test.tsx +++ b/apps/web/src/app/(authenticated)/sessions/page.client.test.tsx @@ -1,20 +1,47 @@ -import { render, screen, waitFor, within } from '@testing-library/react'; - -const { getSessionsMock, sessionStatusState } = vi.hoisted(() => ({ - getSessionsMock: vi.fn(), - sessionStatusState: { - current: ['active', 'needs_input', 'blocked', 'ready', 'done'] as Array< - 'active' | 'needs_input' | 'blocked' | 'ready' | 'done' - >, - }, -})); +import { + act, + fireEvent, + render, + screen, + waitFor, + within, +} from '@testing-library/react'; + +const { getSessionsMock, sessionStatusState, remove, refresh, push } = + vi.hoisted(() => ({ + remove: vi.fn(), + refresh: vi.fn(), + push: vi.fn(), + getSessionsMock: vi.fn(), + sessionStatusState: { + current: ['active', 'needs_input', 'blocked', 'ready', 'done'] as Array< + 'active' | 'needs_input' | 'blocked' | 'ready' | 'done' + >, + }, + })); vi.mock('next/navigation', () => ({ usePathname: () => '/sessions', - useRouter: () => ({ replace: vi.fn() }), + useRouter: () => ({ replace: vi.fn(), refresh, push }), useSearchParams: () => new URLSearchParams(), })); +vi.mock('@/trpc/client', () => ({ + useTRPC: () => ({ sessions: { delete: { mutationOptions: () => ({}) } } }), +})); +vi.mock('@tanstack/react-query', () => ({ + useMutation: () => ({ mutateAsync: remove }), +})); +vi.mock( + '../../(sandbox)/sessions/[sessionId]/SessionDeleteAction', + async (importOriginal) => ({ + ...(await importOriginal()), + SessionActions: () => ( + + + + ) : ( + + )} + + {children} + { + if (!pending) setConfirming(open); + }} + > + + + + Delete {selected.size} session{selected.size === 1 ? '' : 's'}? + + + {SESSION_DELETION_DESCRIPTION} + + + + + + + + + + ); +} + +export function SessionSelectionRow({ + sessionId, + className, + children, +}: { + sessionId: string; + className: string; + children: ReactNode; +}) { + const selection = useContext(SessionSelectionContext); + return ( +
+ {children} +
+ ); +} + +export function SessionSelectionLink({ + sessionId, + title, + canManage, + href, + className, + children, +}: { + sessionId: string; + title: string; + canManage: boolean; + href: string; + className: string; + children: ReactNode; +}) { + const selection = useContext(SessionSelectionContext); + if (selection?.active) { + return ( +